The whole of Veil's second front door: authenticate, open a case, upload documents, read what came back and hear about it when it changes. Every mutating endpoint is a thin adapter over the same function the dashboard calls, so there is no second implementation of any rule.
Base URLhttps://veil.pritect.ai/api/v1
Version1.0.0
OpenAPI3.1.0
Authentication
One bearer key, one machine identity
Every request carries an API key as a bearer token. A key begins with pv_live_ and is shown once, in the Developer area, when it is created. Veil stores only a hash of it, so a key that is lost is replaced rather than recovered.
A key is not a shared credential with a role attached. It has its own identity inside your organisation, so row level security applies to every read it makes, and the audit log records what the key did rather than who created it. Revoking a key stops it immediately and leaves its history intact.
A key holds a role, capped at the role of the person who created it and never owner, and a set of scopes. Scopes narrow the role; they never widen it. A key whose role is member and whose scopes include releasing a case still cannot release one, because the check that matters happens in the database.
A request outside the key's scopes is refused with a fixed code, before anything is read or written. Give a key the smallest set its integration needs: a key that can read cases and download bundles cannot open, advance or release one.
cases:read
cases:write
cases:release
documents:read
documents:write
entities:read
entities:write
bundle:read
webhooks:read
Errors
One shape for every failure
The code is drawn from a closed vocabulary, so a client branches on it rather than on prose. The message is a fixed English sentence and never quotes the input: a validation failure names the field that was wrong and never the value that was in it, because that value may be the personal data the whole product exists to keep in one place.
The request id is generated per request, returned in the body and in an X-Request-Id header, and appears in the one log line the request writes. It is what to quote in a support mail, and quoting it does not mean quoting anything else.
Every failure
{
"error": {
"code": "case_not_released",
"message": "The case has not been released yet.",
"request_id": "01J8ZQ9C4R7K2M6N0P3T5V8W1X"
}
}
Every error code the API answers with
Status
Code
Message
400
idempotency_key_required
This endpoint requires an Idempotency-Key header.
400
invalid_cursor
The cursor is not one this API issued.
400
invalid_request
The request does not match the contract.
401
invalid_key
The API key is missing, unknown, revoked or expired.
403
api_access_unavailable
This organisation's plan does not currently include API access.
403
decision_requires_a_person
This decision discloses personal data and is made by a person in the Veil dashboard.
403
insufficient_scope
The API key does not hold the scope this endpoint requires.
403
not_authorized
The API key's role does not allow this.
404
not_found
No such resource.
409
case_not_draft
The case has left draft, so its intake can no longer be changed.
409
case_not_ready
The case is not ready to be released.
409
case_not_released
The case has not been released yet.
409
entity_not_editable
The entity's current state does not allow this change.
409
idempotency_key_reused
This Idempotency-Key was already used with a different request body.
409
plan_limit_reached
A plan limit refused this request.
409
report_not_ready
The case has not been packaged, so it has no quality report yet.
409
review_not_open
The case is not in review, so entity decisions cannot be changed.
409
transition_refused
The case's current state does not allow this.
422
anchor_not_derivable
No searchable variant could be derived from that anchor.
429
rate_limited
Too many requests for this API key in the current window.
500
internal_error
The request could not be completed.
503
dependency_unavailable
A dependency this endpoint needs is unavailable.
Pagination
Cursors, never offsets
Every list is cursor paginated. A response carries the rows and a next cursor, which is opaque: it encodes an instant and an id, and the only correct thing to do with one is to send it back.
It is not an offset, and that is a correctness decision rather than a performance one. A case list mutates under a client that is paginating through it, and an offset silently skips rows when it does.
An Idempotency-Key header is accepted on every request that creates or advances something, and required on case creation. Veil stores the key with your organisation, the endpoint, a hash of the request body and the response, for 24 hours.
A replay with the same key and the same body returns the stored response and says so in an Idempotency-Replayed header. A replay with the same key and a different body is refused. That is what makes a retrying integration safe: a client that times out creating a case and tries again gets one case, not two, and does not spend a second slot against its plan.
A key may make 120 requests in a 60 second window. Two endpoints start work that costs model calls and take a smaller budget of 10 in the same window.
POST /cases
POST /cases/{caseId}/process
Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and, once a limit is reached, Retry-After. Honour that last one: a client that retries immediately spends its next window on refusals.
Endpoints
All 25 of them
Each one names the function behind it, because every mutating endpoint is a thin adapter over the same transition function the dashboard calls.
Register an endpoint in the Developer area, pick the events it should hear, and Veil posts to it. The signing secret is shown once, at registration and again on rotation, and is stored encrypted: nothing hands it back, which is why an endpoint offers rotation rather than a reveal.
Payloads carry ids, statuses, counts, timestamps and the case reference, and nothing else. No filename, no entity label, no anchor value, no document excerpt, no address, no name. The reference is included because it is Veil's own generated string and names nobody, and one human readable handle is what a customer needs to reconcile against their own ticket.
Payloads are signed and not encrypted. TLS protects them in transit, and there is nothing in one whose disclosure to a network observer would matter, which is what makes the rule above load bearing rather than decorative.
POSTcase.status_changed
Sent to every active endpoint of the organisation subscribed to case.status_changed. Verify Veil-Signature over the raw body before parsing it.
Every delivery carries Veil-Signature alongside Veil-Delivery and Veil-Event. The algorithm is HMAC-SHA256 over <t>.<raw request body>, keyed with the endpoint's signing secret.
Veil-Signature
t=<unix seconds>,v1=<hex>
01Read the raw request body as text before parsing it. A JSON round trip reorders keys and changes whitespace, and either would change the bytes the signature was taken over.
02Split the Veil-Signature header on commas into t and v1.
03Recompute HMAC-SHA256 over the exact string t + "." + body, keyed with your endpoint's signing secret, and compare the hex digest with v1 in constant time.
04Refuse a timestamp more than 300 seconds old, which is what makes a captured delivery useless as a replay.
05Deduplicate on Veil-Delivery, which is stable across retries. Delivery is at least once; it is not exactly once.
Signature verification is the part of a webhook integration that is easy to get subtly wrong, and getting it wrong fails open. Veil publishes a verifier alongside the contract so that the check is a call rather than a transcription of the recipe below. The same package carries the request and response types, the closed error vocabulary and the scope list, so a client branches on constants rather than on strings it typed. The verifier is verifyWebhookSignature, exported from @pritect-veil/api.
Delivery and retries
At least once, and never exactly once
Deliveries are dispatched by a scheduled job rather than at the instant of the transition, so a delivery waits up to one minute plus the dispatch itself. Do not build a synchronous flow on top of it.
A delivery is attempted up to 6 times. The first goes on the next dispatch tick, then Veil waits 1 minute, then 5 minutes, then 25 minutes, then 2 hours, then 6 hours. A 4xx other than 408 and 429 is terminal on the first response. 408, 429, every 5xx, a timeout at 10 seconds and a connection failure are retried.
After 20 consecutive failures an endpoint is disabled and somebody has to enable it again, because an endpoint nobody is listening to should stop generating traffic and start generating a message. Redirects are not followed. The delivery log holds 30 days.
Dispatch latency
up to 1 minute
Attempts
6
Replay window
5 minutes
Scope
What version one deliberately does not do
A boundary stated is a boundary somebody can build around. These are deliberate, and each one has a reason that is not a roadmap entry.
Two decisions a person makes
Marking an entity as the data subject and keeping one as non personal both disclose personal data into the released bundle. Both are made in the dashboard behind a gate that has to be typed out, and the API answers a request to make either of them with a refusal rather than a quiet success.
Anything that carries document text
The dashboard can show the sentence an entity was found in. The API cannot, because every field that carries document text is a field a customer's log aggregator may end up holding. The three decisions version one does allow need none of it.
Reporting a spot check miss
A miss carries a leaked value somebody typed. It stays in the dashboard, where that value is encrypted under the case key before it touches a row. The API can read a spot check and clear a page.
Connector pulls and the settings tree
Starting a pull from a connected workspace needs an interactive consent that already exists in the dashboard. There is no organisation, member, invitation, billing or subscription endpoint either: the API drives cases, not the tenancy.
A console on this page
There is no button here that sends a request. A console needs a live key in a browser, which is the one place a key should never be, and building one honestly needs a test mode that does not exist yet.
This page is generated from the contract itself. Every path, scope, error code, payload and interval on it is read from the document Veil serves, so it describes the API that is running rather than the one that was written about.