@ensc/sdk does everything on this page for you. Read on if you integrate from a language without an SDK or want to audit what the SDK does. The formats below are exact; the API refuses anything that deviates.
Order of operations on a write: serialise the JSON, encrypt it into the envelope, then sign the envelope bytes. On every response: verify ENSC’s signature, then open the sealed body.
Request encryption (ENSC-ENC-V1)
Applies to everyPOST, PUT, PATCH and DELETE made with a secret or restricted key. GET requests have no body and are not encrypted.
Algorithm
- Cipher: AES-256-GCM.
- Key: your 32-byte encryption key (base64url-decode
ENSC_ENCRYPTION_KEY). - IV: 12 random bytes, fresh for every request.
- Tag: 16 bytes, transmitted separately from the ciphertext.
- Additional authenticated data (AAD): the UTF-8 bytes of
METHOD is upper-case, PATH is the request path without query string (for example /v1/conversions), and \n is a single line feed.
Envelope
Serialize your request as JSON (this is the plaintext), encrypt it, and send this object as the HTTP body withContent-Type: application/json:
v must be 1, encKeyId must match ^enc_[0-9A-Z]{26}$, ciphertext must be non-empty and at most 1 MiB. A request without a body still sends an encrypted {}.
Errors
Request signature (ENSC-V1)
The Ed25519 signature is computed over the envelope bytes exactly as sent, not over the plaintext. Encrypt first, then sign. EveryPOST, PUT, PATCH and DELETE must carry:
\n):
- The timestamp must be within 300 seconds of ENSC’s clock (
ENSC_TIMESTAMP_OUT_OF_WINDOW). - A nonce is accepted once (
ENSC_NONCE_REUSED). Retries must re-sign with a fresh timestamp and nonce. X-ENSC-Key-Idis required on every write; a write without it isENSC_MISSING_SIGNATURE.- The key id must be one of your signing keys (
ENSC_MISSING_PUBLIC_KEYif it is not, on reads and writes alike), and that key must be active or rotated less than 24 hours ago: a revoked key, or one rotated more than 24 hours ago, isENSC_INVALID_SIGNATURE. - Reads (
GET) are not signed.
Response decryption (ENSC-RESP-V1)
Every successful (2xx) JSON response to a secret or restricted key is sealed to your signing key and signed by ENSC. Error responses are plain JSON.Headers
Body
Verify, then open
- Fetch ENSC’s public keys from
GET /v1/.well-known/ensc-public-keys.json(cache them; refetch once if you meet an unknownX-ENSC-Key-Id). Pick the entry whosekidequals the header and whoseuseincludes"responses". - Reject the response if
X-ENSC-Timestampis more than 300 seconds from your clock. - Verify the Ed25519 signature over the UTF-8 bytes of
body is the raw response body exactly as received. Do not parse the body before the signature verifies.
- Open the envelope with HPKE, RFC 9180, base mode:
- KEM
DHKEM(X25519, HKDF-SHA256)(0x0020), KDFHKDF-SHA256(0x0001), AEADChaCha20-Poly1305(0x0003) - Recipient private key: your Ed25519 signing seed converted to X25519 (SHA-512 of the seed, clamp the first 32 bytes; this is the standard Ed25519-to-X25519 conversion)
enc: the sender’s ephemeral public key from the bodyinfo: UTF-8 bytes ofENSC-RESP-V1\n{requestId}aad: empty- Plaintext: the JSON response documented for the endpoint
- KEM
Errors the SDK raises
Every
EnscError carries status (the HTTP status received) and requestId. The SDK retries only network errors, timeouts and the statuses 500, 502, 503 and 504, never a 4xx or 429. The full list is in Errors.
Which key does ENSC seal to?
The signing key named by theX-ENSC-Key-Id header on your request. Writes always carry it; send it on reads too. If you omit it on a read and have exactly one usable signing key, ENSC uses that one; with several, it answers ENSC_MISSING_PUBLIC_KEY and asks you to state the key. A key id that is not one of your keys is ENSC_MISSING_PUBLIC_KEY on reads and writes alike.
Reference implementation
@ensc/sdk is the reference: encryptRequestBody and openSealedResponse in its source do exactly the steps above using Web Crypto and the @noble libraries. If your implementation disagrees with the SDK against the same inputs, the SDK is right.