---
title: "Submit up to 50 000 EU VAT IDs for background validation"
method: POST
path: "/v1/vat/bulk"
tags: ["VAT validation"]
---

# Submit up to 50 000 EU VAT IDs for background validation

`POST /v1/vat/bulk`

## Request

- Base URL: `https://api.vatnode.dev`
- URL: `https://api.vatnode.dev/v1/vat/bulk`
- Auth: HTTP bearer

Records the batch and answers `202 Accepted` with a job id. **Nothing is
validated during this request** – the positions are worked through in
the background and read back through the three endpoints below.

This is why the endpoint is asynchronous: a cold-cache VAT ID costs a
real VIES round trip, so even a hundred of them cannot honestly be
answered inside one HTTP request, let alone fifty thousand. A job runs
at a deliberately limited concurrency – five checks at a time, whatever
the batch size – and steps aside from member states VIES is currently
struggling with, so a batch takes a bounded share of upstream capacity
rather than as much as it can get. There is no priority queue: your live
`GET /v1/vat/{vatId}` calls are not routed ahead of your batches, they
simply are not competing with an unbounded one. Stepping aside applies
to the affected positions only: the job keeps working every other member
state in the batch and comes back to the degraded one later. If it is
still degraded when the job runs out of retries, ONLY that member
state’s positions are answered `VIES_UNAVAILABLE` – a position is never
given an error for a check that was not attempted.

**Duplicates are collapsed.** VAT IDs are normalised (trimmed,
uppercased, whitespace, `-` and `.` removed) and de-duplicated, preserving first-seen order, so a list
containing the same ID twice produces ONE position and is charged ONE
quota unit. `duplicatesDropped` reports how many entries that removed.

**Send an `Idempotency-Key`.** It is optional, and this is the endpoint
where it matters most: if the connection drops before the `202` arrives,
you cannot tell whether up to 50 000 billable positions were accepted,
and a plain retry buys them twice. Repeating the request with the same
key AND THE SAME LIST OF VAT IDS returns the SAME job – no second job,
no second charge. Keys are scoped to your account; a key you have not
used before starts a new batch. Repeating a key with a DIFFERENT list is
rejected with `409 IDEMPOTENCY_KEY_CONFLICT` rather than silently
answered with the old job (which would hide part of the new batch) or
bought as a second job (which would defeat the key). 8–200 characters of
letters, digits, `.`, `-` or `_`; a key that does not parse is refused
with `IDEMPOTENCY_KEY_INVALID` rather than ignored, because ignoring it
would leave you believing you were protected.

**Quota is spent per position, as it is answered**, exactly as on the
single-ID endpoint – not reserved for the whole batch up front. A
position that ends in an upstream error is refunded, and so is one
whose answer could not be stored. That per-position accounting has
one gap: if the process running the job dies between reserving a
position’s quota and recording its answer, the reservation is not
returned. The position stays pending, is answered again when the job
resumes, and its quota unit is spent twice – bounded by the five
positions a pass works on at once, so a crash costs at most a
handful of positions, not the batch. What running out of quota
mid-job does depends on the plan: a plan with overage enabled keeps
going and bills the extra positions at its overage rate, while a
hard-capped plan answers every remaining position with
`RATE_LIMITED`. Either way the job finishes.

In test mode (a `vat_test_` key) the job is real and the polling
contract is identical, but every position must start with `XX` and is
answered from fixtures – no quota, no VIES call, no stored check.

Jobs and their positions are kept for **30 days** (`expiresAt`), after
which they are deleted. The checks themselves live on under their own
`checkId` in your check history.

## Headers

- `Idempotency-Key` string

## Request body

- BulkVatRequest
  - `vatIds` string[], required: The VAT IDs to validate. De-duplicated after normalisation, so repeated entries yield one position and cost one quota unit.

## Response `202`

The batch was recorded. Poll the job for progress. A repeat of a
request that carried an `Idempotency-Key` and the SAME `vatIds`
answers `202` with the job that key already created – `status` and
the counters are then whatever that job has reached by now, not
recomputed from this request, and not necessarily `queued`.

- BulkJobAccepted
  - `jobId` string, uuid, required
  - `status` 'queued' | 'processing' | 'finished' | 'cancelled' | 'failed', required: `queued` for a batch just accepted. A replay under an `Idempotency-Key` returns the existing job, which may have moved on already – including to one of the terminal states described on `BulkJob`.
  - `totalItems` integer, required: Unique VAT IDs recorded – the number of positions the job has.
  - `duplicatesDropped` integer, required: Entries removed as duplicates of an earlier one in the same batch. On a fresh accept this is counted off the list you just sent; on a replay of the same `Idempotency-Key` and batch it is the count the original accept stored, not recomputed from this request.
  - `createdAt` string, date-time, required

## Other responses

- `400`: The request body, query string or path parameters failed structural validation (wrong type, missing required field, out-of-range length, malformed enum, etc.). Returns the standard error envelope with code `INVALID_REQUEST`. This is distinct from a semantically well-formed request that is rejected later (e.g. `INVALID_FORMAT` for a VAT ID that parses but is not a valid VAT number).
- `401`: Missing or invalid API key.
- `409`: This `Idempotency-Key` was already used for a different batch (`IDEMPOTENCY_KEY_CONFLICT`). The job it originally bought is untouched; resubmit with a new key to run this batch.

---

[API](https://skmtc.dev/vatnode/apis/vatnode-api.md) · [All operations](https://skmtc.dev/vatnode/apis/vatnode-api/llms.txt) · [OpenAPI document](https://skmtc.dev/vatnode/apis/vatnode-api/revisions/c65828db2037?raw)
