API Reference
Native reference for the Tricryption REST Gateway (v8.1.0) — Agentless REST access to a Tricryption key server, for third-party integrations.
For applications that link the native library instead of calling HTTP, see the SDK Reference.
These pages are generated from the committed OpenAPI 3.1 spec — 51 operations over 42 paths:
- Download
openapi.yaml— feed it to your own tooling. - New here? Start with the Integrating via the API guide, which walks a complete worked arc.
Operations by family
| Family | Operations | What it covers |
|---|---|---|
| Sessions | 8 | Obtaining, inspecting and ending a credential. |
| Data cryptography | 4 | Encrypting, decrypting and MACing data through a key server session. |
| Key lifecycle | 4 | Creating, reading, exporting and expiring keys. |
| Key ACLs | 4 | Who may reach a key, and with which rights. |
| Principals | 8 | Identities -- listing, creating, amending and removing. |
| Groups | 3 | Group membership. A group IS a principal, so it is created and listed under Principals. |
| Roles | 3 | Roles and the operations they carry. |
| Certificates | 4 | Looking up certificate objects, and parsing submitted ones. |
| Certificate trust | 4 | Which CAs the key server trusts, and enrolling leaf certificates. |
| Server administration | 4 | Key server version, configuration and parameters. |
| Policy | 2 | The four XAuth/X.509 auto-provisioning policy parameters. |
| Audit | 2 | Reading the audit log. The one family that reaches a database rather than the key server. |
| Health | 1 | Liveness. |
The REST Gateway is the Tricryption product's published HTTP contract. It fronts a Tricryption key server so that an integration can use key management, data cryptography, principal and role administration, certificate trust and audit reading without embedding a native agent.
Scope of this document
This describes the surface as it is built today: 51 operations over 42 paths. The Gateway's target is the key server's administrative surface for agentless integration, and that target is larger than what is built. The surface is additive and versioned: paths and response fields are added over time, and an operation you do not find here is not necessarily one that will never exist. Do not read the absence of a capability as a statement that the product lacks it.
Response bodies carry a human-readable detail string alongside their structured fields. It
is written for the person debugging the call, and its wording is not part of the contract --
match on code, never on detail.
Authenticating
Every operation except five requires a session, presented as
Authorization: Bearer <session id>.
This surface sets no cookie on any response and reads no cookie on any request
(ADR-0033). If you are holding a cookie from this host, it belongs to something else. Do not
set credentials: "include" on a browser fetch: the Gateway deliberately never sends
Access-Control-Allow-Credentials, so that option turns a working request into a failing
one.
There are three ways to obtain a session, and they issue credentials with different properties:
| route | credential | who holds it |
|---|---|---|
POST /api/session | password (SRP) login. As wide as everything the principal can reach. | an operator, or a backend you trust with the password |
POST /api/session/certificate/challenge then POST /api/session/certificate | X.509 certificate login (AM08), in two legs. | a registered service |
POST /api/session/scoped | narrowed to an explicit object list, minutes-long, bound to a browser-held key. | an end user's page |
POST /api/session/delegated also mints a session, acting as an externally-authenticated
(XAuth) principal -- but it is not a login and it is guarded: only an already
authenticated, registered service may call it (ADR-0031).
Log out with DELETE /api/session. It is idempotent and answers 204 for a credential that
is already gone, for a dead one, and for no credential at all.
Proving you hold your key (DPoP)
Requests on this surface are sender-constrained using DPoP, RFC 9449 (ADR-0035). A stolen bearer credential on its own is not enough to make a request.
One verifier serves two kinds of caller, which is why the standard construction was chosen
over a bespoke signed envelope. A service's enrolled certificate key is RSA, so it signs
RS256; a browser's non-extractable WebCrypto keypair is P-256, so it signs ES256.
DPoP's alg header is what lets both use one mechanism.
The constraint belongs to the SESSION, not to the route
This is the most important sentence in this section. If your session is bound to a key, every call it makes must carry a proof -- not only the call that minted it. No route on this surface is exempt for a bound session.
Which sessions are bound:
- a certificate login whose principal has a
senderKeyregistered with this Gateway -- bound to that registered key; - a scoped browser credential -- bound at mint time to the RFC 7638 thumbprint (
jkt) of the key you sent assenderKey, and it cannot be minted unbound (ADR-0036); - a delegated session -- bound to the sender key of the service that minted it, because its holder is that service holding a second credential.
One class is not bound: a password login, because nobody proved possession of
anything at POST /api/session. A proof sent on an unbound session is ignored rather than
refused, so always sending one is safe, and that is what the security block on each
operation describes.
Minting a proof
A proof is a compact JWS -- base64url(header).base64url(payload).base64url(signature) --
sent in the DPoP request header. Mint a fresh one per request.
Header:
{ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "...": "your PUBLIC key, as a JWK" } }
Payload:
| claim | value |
|---|---|
htm | the HTTP method, uppercase |
htu | scheme://host/path of the request. Query and fragment are excluded, per RFC 9449 section 4.2. |
ath | base64url(SHA-256(<the bearer credential this request carries>)) |
bth | base64url(SHA-256(<the exact request body bytes>)). Hash the bytes you send, not a re-serialisation of them. Send the hash of the empty string when there is no body. |
iat | seconds since the epoch. Accepted within +/- 30 seconds of the Gateway's clock. |
jti | unique per proof. A repeat inside the acceptance window is refused. |
bth is a local extension. RFC 9449 binds the method, the URL and the token; it defines
no body claim. bth exists because binding method and URL alone leaves the body free --
an attacker holding a stolen credential and one captured proof could keep both and swap the
payload, which on this surface means swapping which principal a grant names. If you look
bth up in the RFC and cannot find it, this paragraph is why. Should a future revision of
RFC 9449 define a body claim, this one is superseded by it.
A worked browser example, about thirty lines, is in docs/GATEWAY_BROWSER_INTEGRATION.md.
When a proof is refused
Each failure has its own code, so that you are never sent to look at the wrong thing. All
are 401 except PROOF_MALFORMED (400) and PROOF_REPLAY_CACHE_FULL (503).
code | meaning |
|---|---|
PROOF_MISSING | Bearer sent, no DPoP header, on a bound session. The proof is in addition to the bearer, not instead of it. |
PROOF_MALFORMED | Not three base64url segments; or a segment is not a JSON object; or typ is not dpop+jwt; or iat/jti are the wrong type. |
PROOF_ALG_NOT_ALLOWED | alg absent, none, or outside RS256/ES256. Refused before any key material is touched. |
PROOF_KEY_MISMATCH | The jwk in the proof is not the key this session is bound to. Commonly: the page reloaded and regenerated a keypair while the old credential was still in hand. |
PROOF_SIGNATURE_REJECTED | Right key, bad signature. The signing input is the ASCII string <b64u header>.<b64u payload>, the segments exactly as sent. This is not a clock problem; clock problems have their own code. |
PROOF_BINDING_MISMATCH | htm, htu, ath or bth did not match the request. detail names which one. |
PROOF_CLOCK_SKEW | iat is more than 30 s from the Gateway's clock. Your signing is fine; synchronise the calling host's clock. |
PROOF_REPLAYED | That jti has already been used. One proof, one request. |
PROOF_REPLAY_CACHE_FULL | A deployment fact, not a caller mistake: the Gateway refuses rather than evicting, because eviction would make replay possible again. Retryable. |
The one that will actually bite you is htu behind a proxy. If anything terminates TLS
in front of the Gateway, your page signs https://gw.example/api/... while the Gateway
process only ever sees http://..., and every honest request mismatches. The refusal names
the claim but cannot name the cause, because from inside the process nothing looks wrong. The
fix is on the operator's side: they set TE_GATEWAY_PUBLIC_ORIGIN to the origin your page
actually addresses.
Calling from a browser (CORS)
Two deployment shapes are supported, and the Gateway prefers neither.
Shape 1 -- direct cross-origin. Your page is served from your origin and calls the Gateway at the Gateway's own origin. The operator puts your origin on an allowlist:
TE_GATEWAY_CORS_ORIGINS=https://app.example.com,https://staging.app.example.com
Comma-separated; whitespace around entries is ignored; duplicates collapse. Each entry is a
bare origin -- scheme://host with an optional :port -- with no trailing slash, no path and
no query. Matching is exact on scheme, host and port: there is no pattern matching and no
subdomain wildcard, so list every origin you use, including staging and preview hostnames.
Omit the port when it is the default for the scheme, because a browser sends
https://app.example.com and never https://app.example.com:443. Every one of these rules
is enforced at Gateway boot rather than discovered later: a malformed entry stops the
process with a diagnostic naming it.
* is not supported -- not as a default, not as a value, not behind a flag. This surface
hands out per-object keys belonging to named end users; "any origin may read the responses"
is not a configuration of that control but its removal. It is refused at boot.
Once your origin is allowed, the Gateway permits methods GET, POST, PUT, DELETE and request
headers Authorization, Content-Type, DPoP, and lets a browser cache the preflight for 10
minutes. The allowed-methods set is surface-wide and answered identically for every path;
whether a particular path takes a particular verb is still decided by the route table, and a
wrong verb comes back 405 METHOD_NOT_ALLOWED from the Gateway proper.
Omitting DPoP from a proxy's forwarded headers, or reading an older copy of the allowed
list that lacks it, makes the browser block your request before it is sent -- the same
failure mode as omitting Authorization, and harder to diagnose, because the credential is
perfectly valid and the Gateway never sees the call.
Shape 2 -- you reverse-proxy the Gateway under your own origin. Your page then calls a
relative URL, no origin is crossed, and CORS is not involved at all. This is the default:
with no allowlist configured the Gateway sends no CORS header of any kind, which is the
correct posture for this shape rather than an unfinished one. Make sure your proxy forwards
Authorization and DPoP.
A CORS refusal is close to invisible from the page -- fetch rejects with a bare TypeError
while the request often reached the Gateway and succeeded there. The Gateway logs every
refused origin server-side, by name, so ask the operator rather than guessing.
Error model
Every failure answers JSON carrying at least code and detail. code is the contract.
Some failures additionally carry retryable, reason, scope and reauth. A 503 whose
scope is session or process carries a Retry-After header.
Two conventions on this surface are worth knowing before reading the operations:
- A write is verified by re-reading, not by the call returning. Most key-server write
operations resolve without a result, so a resolved call proves only that the key server did
not refuse. Routes that write therefore re-read and compare, and answer
409with a code such asNOT_SAVEDorNOT_VERIFIEDwhen the read-back disagrees. A409on this surface generally means "nothing errored, and the state is not what you asked for" -- a different fact from a500. - An empty result and a failed read are different answers. Routes that could return an empty list distinguish "there is nothing" from "nothing was read", and never report the second as the first.