Skip to main content

API Reference

Native reference for the Tricryption REST Gateway (v1.7.1) — 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 — 110 operations over 84 paths:

Operations by family​

FamilyOperationsWhat it covers
Sessions15Obtaining, inspecting and ending a credential.
Data cryptography5Encrypting, decrypting and MACing data through a key server session.
Key lifecycle6Creating, reading, exporting and expiring keys.
Key ACLs6Who may reach a key, and with which rights.
Principals9Identities -- listing, creating, amending and removing.
Groups3Group membership. A group IS a principal, so it is created and listed under Principals.
ACL templates13ACL templates and the templated directories bound to them (ACL_MANAGEMENT template operations).
Roles5Roles and the operations they carry.
Certificates9Looking up certificate objects, and parsing submitted ones.
Certificate trust4Which CAs the key server trusts, and enrolling leaf certificates.
Trust administration11Peer key servers this key server trusts, its own trust identity, and the principal trusts that map principals to peers.
Code signing3The approved publishers and releases that client build admission is decided against.
Revocation5The key server's revocation store -- importing CRLs, reading the evidence, looking up a certificate, and the per-issuer revocation policy.
Server administration6Key server version, configuration and parameters.
Policy6The four XAuth/X.509 auto-provisioning policy parameters.
Audit2Reading the audit log. The one family that reaches a database rather than the key server.
Health2Liveness (GET /api/v1/health) and readiness (GET /api/v1/ready).

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: 110 operations over 84 paths. The Gateway's target is every operation the key server's TLS listener admits: each is routed here or excluded with a published reason, and the x-tricryption-coverage extension at the end of this document lists the exclusions and counts the routed ones. 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.

Versioning and compatibility​

Every path is served under the contract's major version, /api/v1/.... Nothing is served at a bare /api/...: such a request is 404. info.version above is the contract's own semantic version (1.0.0 was the first release; 1.1.0 added the rate and capacity codes, Server-Timing and x-idempotent; 1.2.0 adds one exclusion to x-tricryption-coverage, the key server's new ROTATE_SYSTEM_KEYS, and no route; 1.3.0 adds the five OpenID Connect sign-in operations and the one cookie this surface sets; 1.3.1 changes wording only; 1.3.2 changes wording only, naming the largest plaintext one encrypt request carries; 1.4.0 adds subjectToken to POST /api/v1/session/scoped, so a registered service can present its end user's access token instead of naming the user, and the refusals that come with it; 1.5.0 adds POST /api/v1/keys/remove, one result per key on expire, remove and import, 410 KEY_EXPIRED on every operation that uses a key, and deprecates the remove field of POST /api/v1/keys/expire; 1.6.0 adds the RAW and WRAPPED formats to POST /api/v1/keys/import -- a customer's own keys imported under hidden links the key server mints, answered 201 -- and certificatePem, the import key, to GET /api/v1/trust/local-certificate; 1.7.0 adds 422 DECRYPT_FAILED to POST /api/v1/crypto/decrypt -- a ciphertext that does not decrypt under the key it names, which was answered 403 ACCESS_DENIED -- the audit reason decryptFailed, and the 403 codes TRANSPORT_PRINCIPAL_REQUIRED and PRINCIPAL_MISMATCH; 1.7.1 changes wording only, naming who may read GET /api/v1/trust/local-certificate); x-tricryption-product-version names the product release that serves it. The policy is the rest of this section.

Within a major, a release only ever ADDS (a minor version): a new operation, a new optional response field, a new optional request field. An added error status or code on an existing operation is also a minor change -- so a client must handle a code it does not recognise by its status class (4xx: the request; 5xx: the service) rather than failing on it. An added enum value on a field the Gateway returns is compatible only on a schema marked x-extensible-enum: true; every other returned enum is closed. A field a client sends may accept a new value in a minor release: a client that never sends it is unaffected.

Anything else is a new major at a new path prefix (/api/v2/...): removing a path, an operation or a field; changing a type or a meaning; a new required request field; a changed default; a changed authentication or sender-constraint requirement; removing or renaming an error code.

Behaviour is part of the contract, not only the schemas: error codes and their statuses, pagination, credential scope and lifetime, DPoP proofs (with the local bth claim), and retry semantics (retryable, Retry-After). The wording of detail is not.

A replaced major is retired no sooner than 12 months after its successor is generally available, except for an urgent security fix. Throughout that period every response of a deprecated operation carries Deprecation (RFC 9745), Sunset (RFC 8594) once a date is set, and Link: <...>; rel="deprecation" naming the migration note, and the operation is marked deprecated: true here.

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.

Rate and capacity limits​

The Gateway limits traffic in two stages. Every refusal below is answered by the Gateway itself, before anything is sent to the key server, and every one carries Retry-After (whole seconds).

Before authentication -- a request the Gateway cannot take at all:

  • 413 PAYLOAD_TOO_LARGE -- the request body is larger than this deployment accepts. It is refused before it is parsed; the connection is closed. Not retryable as sent.
  • 503 CAPACITY_UNAVAILABLE (scope: process) -- the Gateway is serving its maximum number of requests at once. A small reserve above that ceiling is kept for DELETE /api/v1/session, POST /api/v1/session/scoped/revoke, GET /api/v1/health and GET /api/v1/ready, so a saturated Gateway can still end sessions and answer its probes.
  • 429 RATE_LIMITED -- too many logins (POST /api/v1/session and both certificate legs) from one network source. The source is the connecting address, or -- only behind a proxy the operator has listed -- the address that proxy reports.

After authentication and DPoP verification -- the request is charged to the verified identity, never to the session that carried it:

  • a registered service is charged for its own certificate sessions and for every delegated session and scoped credential it minted; a password session is charged to its principal. Logging in again, or minting another credential, never buys another allowance.
  • each identity has an allowance per cost class -- session (logins and minting), ordinary, enumeration (list and search operations) and audit -- a ceiling on its requests in progress, and a service a ceiling on its live sessions. A scoped credential also has an allowance of its own beneath its service's.
  • any of them spent: 429 RATE_LIMITED with Retry-After. A 429 carries no scope: it is your own allowance, not a deployment ceiling. A request whose DPoP proof fails is refused for the proof and spends nothing.

The numbers are deployment configuration, not contract: they are not published here, they differ between deployments, and several Gateway instances in front of one key server each enforce their own. Honour Retry-After; do not tune a client to a number.

A response whose handling called the key server carries Server-Timing: ks;dur=<ms> -- the time spent in key-server conversations for that request, in milliseconds.

Retries and idempotency​

  • Every attempt carries a fresh DPoP proof. A proof is accepted once (jti); re-sending the proof of an earlier attempt is refused PROOF_REPLAYED, never served.
  • A timeout does not establish that a mutation failed. The key server may have done it.
  • A mutating operation is NOT idempotent unless it says x-idempotent: true. Every POST, PUT and DELETE here carries the extension. Where it is true, repeating the request after an unknown outcome leaves the same state -- the repeat may answer differently (a 404 for a removal already made) but changes nothing further. Where it is false, a repeat can act twice (mint a second key, issue a second certificate, add a second entry): read the state back before retrying.
  • Retry on 429 and 503 after Retry-After; on any other 5xx only an x-idempotent operation, and only after reading retryable.

Authenticating​

Every operation except six requires a session, presented as Authorization: Bearer <session id>.

No cookie carries a session or any API authority on this surface. It sets one cookie, the OpenID Connect login transaction cookie, on GET /api/v1/auth/oidc/launch, and reads and clears it on the provider's redirect to GET /api/v1/auth/oidc/callback/{providerId}; no other operation sets or reads a cookie. If you are holding any other 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:

routecredentialwho holds it
POST /api/v1/sessionpassword (SRP) login. As wide as everything the principal can reach.an operator, or a backend you trust with the password
POST /api/v1/session/certificate/challenge then POST /api/v1/session/certificateX.509 certificate login (AM08), in two legs.a registered service
POST /api/v1/session/scopednarrowed to an explicit object list, minutes-long, bound to a browser-held key.an end user's page

POST /api/v1/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.

Log out with DELETE /api/v1/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. 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 senderKey registered 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 as senderKey, and it cannot be minted unbound;
  • 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/v1/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:

claimvalue
htmthe HTTP method, uppercase
htuscheme://host/path of the request. Query and fragment are excluded, per RFC 9449 section 4.2.
athbase64url(SHA-256(<the bearer credential this request carries>))
bthbase64url(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.
iatseconds since the epoch. Accepted within +/- 30 seconds of the Gateway's clock.
jtiunique 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).

codemeaning
PROOF_MISSINGBearer sent, no DPoP header, on a bound session. The proof is in addition to the bearer, not instead of it.
PROOF_MALFORMEDNot 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_ALLOWEDalg absent, none, or outside RS256/ES256. Refused before any key material is touched.
PROOF_KEY_MISMATCHThe 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_REJECTEDRight 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_MISMATCHhtm, htu, ath or bth did not match the request. detail names which one.
PROOF_CLOCK_SKEWiat is more than 30 s from the Gateway's clock. Your signing is fine; synchronise the calling host's clock.
PROOF_REPLAYEDThat jti has already been used. One proof, one request.
PROOF_REPLAY_CACHE_FULLA deployment fact, not a caller mistake: the Gateway refuses rather than evicting, because eviction would make replay possible again. Retryable.
SENDER_KEY_RETIREDYour service rotated its sender key and the key this session was bound to at login is no longer honoured: its overlap (previousSenderKey.notAfter) has passed, or it was replaced with no overlap. The session is closed with this answer; log in again, which binds the current key. Not retryable.
SERVICE_REGISTRATION_ENDEDThe service registration this credential was issued under was removed or narrowed on this Gateway. The credential has ended; nothing was sent to the key server.

The registered service's credential lifecycle​

A registered service keeps its registration across a certificate renewal: the registration is keyed on the key server's login id, and both renewal paths keep it -- POST /api/v1/certificates/renew for a certificate the key server issued, and POST /api/v1/transport-principals/reset followed by POST /api/v1/certificates/enroll of a replacement with the same subject for an imported one. The renewed-away certificate is then refused at login with 401 CERTIFICATE_RETIRED.

Sender-key rotation. The operator rotates a sender key by editing the registry file: the new key as senderKey, the old one as previousSenderKey: {key, notAfter} -- at most one previous key, and notAfter no more than TE_GATEWAY_SENDER_KEY_OVERLAP_MAX_HOURS (default 24) after the file is loaded. The Gateway re-reads the file while it runs (no restart); a file that fails validation is refused whole, the registry in force stays, and the refusal is shown on GET /api/v1/ready under lifecycle.serviceRegistry. New logins bind the new key; a session bound to the old one -- and a delegated session it minted -- keeps working until notAfter and is then refused 401 SENDER_KEY_RETIRED.

Revocation. Everything a service holds or minted at this Gateway -- its sessions, the delegated sessions and the scoped credentials it minted -- ends, its key-server sessions closed, when its registration is removed or narrowed, when a call on its own session comes back CERTIFICATE_REVOKED, and otherwise within TE_GATEWAY_REVOCATION_SWEEP_SECONDS (default 30) of the key server holding a CRL that names its certificate: the Gateway asks the key server for each live service certificate's revocation status on that interval, through its own identity. That interval is the revocation bound. A credential that has ended answers 401 (SESSION_NOT_FOUND, or one of the two codes above).

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/v1/... 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 409 with a code such as NOT_SAVED or NOT_VERIFIED when the read-back disagrees. A 409 on this surface generally means "nothing errored, and the state is not what you asked for" -- a different fact from a 500.
  • 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.