akwitoEU VAT proof

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:

FieldRequiredMeaning
fileyesthe CSV file, 2 MiB and 5 000 data rows maximum
modenolookup (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:

json
{ "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.

http
POST /v1/batches
Authorization: Bearer ak_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
json
{
  "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 statuserror.codeWhen
400EMPTY_FILEno usable data row (empty file, or an empty numbers list)
400TOO_MANY_ROWSmore than 5 000 data rows
400INVALID_CSVthe file could not be read as CSV at all
400MISSING_CANDIDATEmode=verify without a name column
413FILE_TOO_LARGEthe upload exceeds 2 MiB
429TOO_MANY_BATCHES5 batches are already queued or running on this account; carries active

GET /v1/batches/{id} #

json
{
  "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.

ParameterMeaning
statuspending, done or failed; omitted returns all
limit1 to 100, default 20
cursornext_cursor from the previous page — the last line number returned
json
{
  "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:

errorMeaning
INVALID_FORMATthe number could not be normalised; the line never reached an upstream
INVALID_REFERENCEthe 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_EXCEEDEDthe account's monthly quota ran out; the remaining lines of the batch are closed with it
UPSTREAM_UNAVAILABLEVIES never answered within the batch's retry window
ACCOUNT_DISABLEDthe account was disabled while the batch was running
TIMEOUTthe check exceeded its time budget on every attempt
INTERNALunexpected 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.