Documentation / Batches
Batches (CSV)
A batch checks a whole file of VAT numbers through the same engine as POST /v1/checks, without holding a connection open. Each line becomes its own check, deduplicated and billed exactly like a direct call.
POST /v1/batches #
Two request forms are accepted.
File upload — multipart/form-data:
| Field | Required | Meaning |
|---|---|---|
file | yes | the CSV file, 2 MiB and 5 000 data rows maximum |
mode | no | lookup (default) or verify |
The separator (, or ;) is detected on the first line, a UTF-8 BOM is ignored, and a header row is recognised when it names at least one known column: vat/vat_number/tva/numero_tva/number for the number, name/nom/raison_sociale/company, street/rue/adresse/address, postcode/cp/code_postal/zip, city/ville, reference/ref/facture/invoice for the line's own business reference (same rule as reference on POST /v1/checks; a line whose reference is invalid is closed as failed with INVALID_REFERENCE, never reaching an upstream, exactly like INVALID_FORMAT). Without a recognised header the first column is the VAT number and the rest is ignored.
mode=verify compares the declared identity to the upstream one, so the file must carry a name column with at least one non-empty value: a blank name column has nothing to confirm, and such a file is rejected with 400 MISSING_CANDIDATE rather than silently downgraded to a lookup. In mode=lookup the identity columns are ignored.
JSON list — for callers that already hold the numbers:
{ "numbers": ["FR40303265045", "DE811907980"], "mode": "lookup" }
The JSON form carries no identity, so mode must be lookup; verify is rejected with 400 MISSING_CANDIDATE. numbers accepts up to 5 000 entries; beyond that the request is rejected with 400 TOO_MANY_ROWS. The body of this route is capped at 524 288 bytes (512 KiB) rather than the 64 KiB of the other JSON routes, so that 5 000 numbers always fit; a larger body is rejected with 400 BAD_REQUEST.
POST /v1/batches
Authorization: Bearer ak_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
{
"batch_id": "bat_01J9X0000000000000000000",
"status": "queued",
"total": 1200,
"invalid": 3,
"url": "https://api.akwito.eu/v1/batches/bat_01J9X0000000000000000000"
}
202 Accepted. total counts the data rows read, invalid those already closed as failed without reaching an upstream — number that could not be normalised (INVALID_FORMAT) or reference that failed validation (INVALID_REFERENCE).
| HTTP status | error.code | When |
|---|---|---|
| 400 | EMPTY_FILE | no usable data row (empty file, or an empty numbers list) |
| 400 | TOO_MANY_ROWS | more than 5 000 data rows |
| 400 | INVALID_CSV | the file could not be read as CSV at all |
| 400 | MISSING_CANDIDATE | mode=verify without a name column |
| 413 | FILE_TOO_LARGE | the upload exceeds 2 MiB |
| 429 | TOO_MANY_BATCHES | 5 batches are already queued or running on this account; carries active |
GET /v1/batches/{id} #
{
"batch_id": "bat_01J9X0000000000000000000",
"status": "running",
"filename": "clients.csv",
"total": 1200,
"done": 840,
"failed": 3,
"pending": 357,
"created_at": "2026-09-16T09:00:00Z",
"started_at": "2026-09-16T09:00:12Z",
"finished_at": null,
"results_url": "https://api.akwito.eu/v1/batches/bat_01J9X0000000000000000000/results.csv"
}
status is queued, running, done, or failed when the batch finished without producing a single check. pending is derived (total - done - failed). started_at and finished_at are null until they happen. A batch belonging to another account is 404 NOT_FOUND, never 403.
A batch is background work. One worker tick processes at most 50 lines of a single batch, but it is also bounded by a background budget shared with the other background steps — about half the worker interval, 10 s by default — so real throughput is a few lines per tick, not 50. A 5 000-line file can take several hours, more when several batches are running. Poll GET /v1/batches/{id} no more than once a minute; the webhook batch.completed saves you from polling at all.
GET /v1/batches/{id}/items #
Lists the batch's lines in file order.
| Parameter | Meaning |
|---|---|
status | pending, done or failed; omitted returns all |
limit | 1 to 100, default 20 |
cursor | next_cursor from the previous page — the last line number returned |
{
"items": [
{
"line": 1,
"raw": "FR 40 303 265 045",
"vat_number": "FR40303265045",
"status": "done",
"check_id": "chk_01J9X0000000000000000000",
"proof_url": "https://api.akwito.eu/v1/checks/chk_01J9X0000000000000000000"
},
{ "line": 2, "raw": "nope", "vat_number": "", "status": "failed", "error": "INVALID_FORMAT" }
],
"next_cursor": "2"
}
line is the rank of the data row, 1 being the first row after the header. vat_number is always present and is "" when the number could not be normalised. check_id and proof_url appear once the line produced a check, error once it failed. next_cursor is "" when there is no further page.
error carries the contractual cause of a line's failure:
error | Meaning |
|---|---|
INVALID_FORMAT | the number could not be normalised; the line never reached an upstream |
INVALID_REFERENCE | the line's reference fails the same rule as on POST /v1/checks (over 128 characters, or a control character); the line never reached an upstream |
QUOTA_EXCEEDED | the account's monthly quota ran out; the remaining lines of the batch are closed with it |
UPSTREAM_UNAVAILABLE | VIES never answered within the batch's retry window |
ACCOUNT_DISABLED | the account was disabled while the batch was running |
TIMEOUT | the check exceeded its time budget on every attempt |
INTERNAL | unexpected server-side failure on that line |
GET /v1/batches/{id}/results.csv #
Returns the whole batch as a CSV file: ; separator, UTF-8 BOM, CRLF line endings — the combination French and German Excel installations open without a prompt. One row per line of the original file, in line order.
line;raw;vat_number;status;valid;confidence;name;address;vies_ref;response_hash;check_url;error;reference
The columns from valid onwards come from the stored check and are empty when the line has no check yet, failed, or had its proof purged by retention. vies_ref is the VIES consultation reference, response_hash the lowercase hex SHA-256 of the body returned by GET /v1/checks/{check_id}, and check_url that same URL. reference is the line's own business reference (the file's reference/ref/facture/invoice column, see POST /v1/batches above) when the file carried one, in the existing column order's fixed last place — empty otherwise.
Any cell whose first character is =, +, -, @, a tab or a carriage return is prefixed with a single quote ('). Spreadsheets treat such a cell as a formula, so this neutralises formula injection carried by a file you uploaded or by an upstream company name. The quote is a display artefact of the spreadsheet; the value that follows it is the original one, untouched. The file can be downloaded while the batch is still running; lines not yet processed simply carry pending.
GET /v1/batches #
Lists the account's batches, newest first. limit (1 to 100, default 20) and cursor behave as on the other lists; items have the shape of GET /v1/batches/{id}.
DELETE /v1/batches/{id} #
Deletes the batch and its lines, 204 No Content. The checks it produced are kept: they are evidence, and they remain readable at their own proof_url. A batch that is still queued or running is refused with 409 BATCH_RUNNING — wait for it to finish, it cannot be cancelled.