/v1, and the venue’s own systems are built on it. A read answers {offset, now, value}: the ledger offset it was read at, the engine’s clock and the value. A write becomes an operation you follow to its end.
Examples use $ENGINE for the engine’s URL and $TOKEN for a bearer. Every route’s fields, roles and refusals are in the API reference.
Sign-in
People sign in through your identity provider (OIDC) and call with its bearer token. The engine checks the token’s signature against the provider’s keys (RS256 or ES256), its issuer, audience and expiry, and reads the caller’s roles from theroles claim. An investor’s token also carries its holder id in the holder claim, and with it the investor reads only that holder’s records. The token’s name and email claims, where it carries them, are recorded beside its subject on what the caller does (Identity provider).
curl
GET /v1/health and GET /v1/openapi.json need no bearer. See Roles and permissions and Identity provider.
Request signing
A service client (your backend, a NAV feed, a monitor) carries a bearer under its own subject, and signs each request with an HMAC key the engine holds for that subject. A captured request cannot be replayed, and one altered in transit is refused.
The five lines are joined by
\n, with none after the last. The path and query are as sent, the path alone when there is no query; the hex is lowercase, and a request with no body hashes no bytes. The secret is trimmed and read as UTF-8.
curl
required must sign every request of its subject. A signature that does not hold is 401 signature-invalid, its check naming the part: key, timestamp, nonce or signature. Sign the path as you send it: a proxy that rewrites the path or the query breaks the signature.
Errors
Every refusal is an RFC 9457 problem,application/problem+json. The type ends in a stable slug, check names the rule that refused, detail says what was wrong with this request, and recovery what would make it succeed.
check, never on the text. retryable and retryAfter say when the same request can succeed later. A write refused after it was accepted fails its operation, and the operation’s problem has the same shape.
A 400 lists every invalid field at once in errors, each with a pointer (a JSON Pointer into the body, or the parameter’s name) and a detail. Here a subscription’s body sent "units": "many" and no holderId:
Policy refusals
A policy refusal is 409policy-refused. Its check names the rule, code and reason are stable, and policyVersion is the version that refused. A client can show them as they come.
A dry run answers the codes an act would meet now, without acting:
GET /v1/instruments/{instrumentId}/precheck, with act=subscription or act=transfer, the holderId, the units, and a transfer’s toHolderId. An investor runs it for its own holder, and sees a refusal that comes from the receiver of a transfer as receiver, with nothing about that holder. What each rule does is on Limits and settlement safety.
Request ids and tracing
Every answer carriesX-Request-Id: yours, when you send one of 1 to 128 printable characters, or one the engine makes. Every problem repeats it as requestId, so log it beside your own record of the call, and quote it when you report a refusal. Send a W3C traceparent as well, and the engine joins your trace: it takes a span of its own, and passes the trace on to your Canton node in every ledger call it makes for the request, its operation’s included.
curl
X-RateLimit-* headers say where the caller stands against its rate limit; see Idempotency and retries.
Idempotency and retries
Every write is an operation. The first call answers 202 withLocation: /v1/operations/{operationId}. The same Idempotency-Key and body again answers 200 with the same operation, so a retry never writes twice; the same key with another body is 422 idempotency-key-reused.
curl
- Retry a timeout, a 5xx or a lost answer with the same key and body.
- A failed operation is final for its key: put the cause right, then send again under a new key.
- When the ledger is out of reach, the operation fails with
ledger-unavailableand aretryAfter; a call that waits on the ledger answers 503 withRetry-After. - Each signed-in caller has a rate limit, counted per token subject: 6000 calls a minute unless the deployment sets another (
venue.api.rate-limit). Every answer to it says where it stands (X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset, the seconds until the window ends). Over it, a call is 429rate-limitedwithRetry-Afterand the problem’sretryAfter: wait that many seconds, then send it again. - Always send a key, and build it from your own id for the action (
register-inv-001), so a restart of your service sends the same one.
Pagination
A list answers a page:{offset, now, value, next}. Send next back as after for the page after it; next is null on the last page. limit is the most rows a page holds, 1 to 1000. A page holds the rows after the cursor in the list’s own order, so a row added or removed between two pages moves nothing else.
curl
next is never null: a page that comes back empty keeps the cursor, so you poll with it for what comes next. See Events and webhooks.
Decimals and time
Amounts, units and prices are exact decimals. Send them as strings ("units": "30.0") or as plain JSON numbers; the engine reads either without a float. Answers carry them as plain JSON numbers, digit for digit and never in exponent notation, so read them into an exact decimal type, never a float, and keep every digit (1.50).
Times are ISO-8601 instants in UTC. A day the API counts, such as a policy’s daily limit, is a UTC one. A statement’s month, a tax year, the calendar’s today and a series’ day are counted in the zone you name, an IANA time zone such as Asia/Seoul, and in UTC when you name none (Time zones). A business date in a statement you send is in its source’s own time zone.
?at=<offset> reads at an earlier offset and ?asOf=<instant> as the ledger stood then, so the same question asked later gets the same answer. Show the figures the engine serves; never compute a ledger figure from them.
Four eyes
A change that needs a second approver comes back pending: the maker’s call is recorded, answers 202, itsstate awaiting, with an actionKey and Location to the approval, and runs only when an approver under another sign-in approves it. The approver is never the maker, and never holds the auditor role.
curl
GET /v1/approvals?state=awaitinglists what waits, each with its request, maker and state, andGET /v1/approvals/{actionKey}reads one.- The actions that loosen a control or move an investor’s units take two approvers: lifting a restriction, releasing a hold, loosening a policy, a standing approval, a forced transfer or a recovery. The first approval answers
awaitingagain, withsignedBy; the second runs it. - Reject with
POST /v1/approvals/{actionKey}:rejectand areason; a maker can withdraw its own. A rejected action never runs, and the same request again is 409approval-rejected.

