Verinode Developer
Core concepts
Conventions that hold across every endpoint.
#Base URL and formats
| Base URL | https://api.verinode.ai/v1 |
| IDs | UUID strings. |
| Timestamps | ISO 8601 UTC. |
| Money | Decimal strings ("18450.00"), never floats. |
| Enums | Every enum value ships a humanized _label sibling on reads. |
#Pagination
List endpoints are cursor-paginated. Pass ?limit= (1–100, default 25) and follow next_cursor:
GET /v1/jobs?limit=50
→ { "data": [...], "next_cursor": "…", "has_more": true, "synced_through": null }
GET /v1/jobs?cursor=<next_cursor>
The cursor is a keyset over (updated_at, id). Pass next_cursor back verbatim — don't parse or construct one. See Reading data for incremental sync.
#Idempotency
Idempotency-Key is honored on exactly four endpoints: POST /v1/documents, /v1/jobs, /v1/clients, and /v1/webhook-endpoints. On those, a retry replays the first response instead of re-executing it — no second extraction, no second Intelligence Unit charge, no duplicate record. Reuse a key with a different body and you get 422 idempotency_key_reuse rather than the earlier record's response: replaying there would silently drop your second write. Generate the key per payload, not per worker.
The header is ignored on POST /v1/events, /v1/events/batch, and /v1/exports. Sending it there does nothing at all:
- Events dedupe on
external_idinstead. Give each event your own id and re-sending it is a no-op that returnsstatus: "duplicate". An event without anexternal_idis written again on every retry, so a retried batch double-writes exactly those events. Treatexternal_idas required for anything you might retry. - Exports are guarded by state: a second create while one is in flight returns
409 export_in_progress. PollGET /v1/exports/{id}rather than re-posting.
#Rate limits and request IDs
Every authenticated response carries RateLimit-Limit / -Remaining / -Reset. Requests rejected before the key resolves — 401, 403 insufficient_scope, and the pre-auth per-address 429 — carry none, because there's no operator whose quota we could report. Exceeding a limit returns 429 with Retry-After.
Limits are hourly, per operator, per endpoint:
| 300/hr | POST /v1/documents, /v1/jobs, /v1/clients |
| 5,000/hr | POST /v1/events |
| 500/hr | POST /v1/events/batch (calls, not events — 1,000 events each) |
| 600/hr | List reads (/v1/jobs, /v1/vendors, /v1/clients), /v1/ping |
| 1,000/hr | Single-record reads (/v1/jobs/{id} etc.), /v1/ingestions/{id} |
| 20/hr | POST /v1/exports (and 4/day) |
| 60/hr | Webhook endpoint create/delete, export download |
A separate pre-auth ceiling applies per IP address before any key is read; a normal integration never meets it.
Every response — including errors — carries a Verinode-Request-Id. Quote it in any support conversation.
#Where you can call it from
Server-side only. The API returns no CORS headers, so a browser fetch cannot call it from a web page. That's deliberate: an API key in frontend code is a leaked credential. Call it from your backend.