Request Body Encryption (JWE)
Request body encryption lets you encrypt the JSON body of an API request before it leaves your systems, using a public key that Atomic publishes for your account. Atomic decrypts the body with the matching private key, which is held in Atomic's key management service and never exported. This protects sensitive fields such as identity data and account numbers with application-layer encryption in addition to TLS.
Encryption uses JSON Web Encryption (JWE) in compact serialization with RSA-OAEP-256 for key encryption and A256GCM for content encryption. Any standards-compliant JOSE library can produce a compatible payload.
How it works
- Atomic enables the feature for your account and generates a key pair per environment.
- You fetch the public key and its id from the JWE public keys endpoint and store both.
- For each request you want to protect, you encrypt the full JSON body as a JWE and send it as the
encryptedBodyfield, with the key id in thex-enc-key-idheader. - Atomic decrypts the body and processes the request exactly as if it were plaintext.
x-enc-key-id header. A request without that header is processed as plaintext, even for accounts with encryption enabled. Make sure the header is set on every request you intend to encrypt, and consider a test that asserts it. Retrieving the public key
Call GET /secrets/jwe-public-keys with your API key and secret. Keys are scoped to the environment the credentials belong to, so fetch separately for sandbox and production.
{
"data": [
{
"_id": "66bd1c0f3a2e4f5a8c9d0e12",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkq...\n-----END PUBLIC KEY-----",
"createdAt": "2026-08-14T18:02:11.541Z"
}
]
} Store the _id and publicKey of the key you will use. The _id is the value you send in the x-enc-key-id header. The publicKey is a PEM-encoded SubjectPublicKeyInfo.
Encrypting a request
Serialize the request body you would normally send as JSON, encrypt it with the public key using alg: RSA-OAEP-256 and enc: A256GCM, and send the resulting compact JWE string as the only field in the body. Include your usual authentication headers plus x-enc-key-id.
| Header | Description |
|---|---|
x-api-key | API Key for your Atomic account |
x-api-secret | API Secret for your Atomic account |
x-enc-key-id | The _id of the public key used to encrypt the body. Note the spelling: enc, short for encryption. |
curl https://sandbox-api.atomicfi.com/access-token \
-H "x-api-key: $ATOMIC_API_KEY" \
-H "x-api-secret: $ATOMIC_API_SECRET" \
-H "x-enc-key-id: 66bd1c0f3a2e4f5a8c9d0e12" \
-H "Content-Type: application/json" \
-d '{ "encryptedBody": "eyJhbGciOiJSU0EtT0FFUC0yNTYi..." }'import { importSPKI, CompactEncrypt } from 'jose'
const apiUrl = 'https://sandbox-api.atomicfi.com'
const authHeaders = {
'x-api-key': process.env.ATOMIC_API_KEY,
'x-api-secret': process.env.ATOMIC_API_SECRET
}
// 1. Fetch and cache the public key (once, not per request)
const keysUrl = `${apiUrl}/secrets/jwe-public-keys`
const { data } = await fetch(keysUrl, { headers: authHeaders })
.then((res) => res.json())
const { _id: keyId, publicKey } = data[0]
const key = await importSPKI(publicKey, 'RSA-OAEP-256')
// 2. Encrypt the request body you would otherwise send as JSON
const body = { identifier: 'YOUR_USER_IDENTIFIER' }
const encryptedBody = await new CompactEncrypt(
new TextEncoder().encode(JSON.stringify(body))
)
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
.encrypt(key)
// 3. Send it with the key id header
const response = await fetch(`${apiUrl}/access-token`, {
method: 'POST',
headers: {
...authHeaders,
'x-enc-key-id': keyId,
'Content-Type': 'application/json'
},
body: JSON.stringify({ encryptedBody })
})Encryption works on any endpoint that accepts a JSON body and is authenticated with your API key and secret. It is most commonly used on Create Access Token and, in the Just-In-Time flow, on Update User. Endpoints authenticated with a public token do not support it.
Errors
When the x-enc-key-id header is present, Atomic validates the encrypted payload before anything else and returns a 400 with one of the following messages if it cannot proceed.
| Message | Cause |
|---|---|
'x-enc-key-id' header detected. must have required property 'encryptedBody' | The header was sent but the body has no encryptedBody field. |
'x-enc-key-id' not found in customer public keys | The key id does not belong to your account in this environment. Check that you are using the sandbox key against sandbox and the production key against production. |
'x-enc-key-id' not able to decrypt body | The JWE could not be decrypted with that key. Confirm the body was encrypted with the matching public key using RSA-OAEP-256 and A256GCM. |
must have required property 'identifier' on a request you believe is encrypted, the x-enc-key-id header was not received. Atomic treated the body as plaintext and validated { "encryptedBody": "..." } against the endpoint's schema. Check the header name for typos. Key rotation
Up to two keys can be active for an environment at once, which allows rotation without a coordinated cutover:
- Ask your Atomic representative to generate a new key for the environment.
- Fetch the public keys again. The response now contains both keys; identify the new one by
createdAt. - Deploy the new key and id to your services at your own pace.
- Once no traffic uses the old key, tell Atomic and the old key is removed. Requests that still reference it will then fail with
'x-enc-key-id' not found in customer public keys.
Key generation is a manual step on Atomic's side, so allow a few business days when planning a rotation.