#
Decryption
Omni Partner API protects sensitive data using AES-256-GCM with a per-partner key.
Any field documented as “encrypted” is returned as a string that always starts with:
ENCRYPTED:
This page defines the encryption envelope format and how partners MUST decrypt these values.
#
1. Envelope format
All partner-facing encrypted values use a single, unified envelope format.
After the ENCRYPTED: prefix, the value is:
base64url( JSON(EncryptionEnvelope) )
where the JSON structure is:
{
"v": 1,
"alg": "A256GCM",
"kid": "<key-id>",
"iv": "<base64url-12-byte-iv>",
"tag": "<base64url-16-byte-tag>",
"data": "<base64url-ciphertext>"
}
Details:
- Algorithm: AES-256-GCM (
alg = "A256GCM") - Key: 32-byte AES key (see section 3)
- IV (nonce): 12 bytes (Base64URL)
- Auth tag: 16 bytes (Base64URL)
- Ciphertext: UTF-8 plaintext encrypted with AES-256-GCM
- KID: Key ID (used for rotation and debugging; not required to decrypt)
Base64URL uses
-and_instead of+and/and may omit padding.
#
2. Decryption procedure
Verify prefix
- The value MUST start with
ENCRYPTED:. - If an API is documented to has response which contain encrypted fields but does not have thisprefix, treat the response as invalid, log the error, and fail fast.
- The value MUST start with
Decode envelope
Strip the prefix:
encoded = value.substring("ENCRYPTED:".length)- Base64URL-decode
encodedto a JSON string. - Parse JSON into
{ v, alg, kid, iv, tag, data }. - Validate:
v === 1alg === "A256GCM"
Prepare AES parameters
- Base64URL-decode
iv,tag, anddatato bytes. - Base64-decode your partner encryption key to a 32-byte buffer.
- Base64URL-decode
AES-256-GCM decrypt
- Create an AES-256-GCM decipher with:
- key: 32-byte key from step 3
- IV: 12-byte
iv
- Set the auth tag to
tag. - Decrypt
datato a UTF-8 string.
- Create an AES-256-GCM decipher with:
Interpret plaintext
For scalar fields (email, phone, etc.) the plaintext is the final value.
For structured payloads (e.g.
storeAccess), the plaintext is a JSON string which SHOULD be parsed into an object.
If decryption fails (bad key, tampered data, parse error), Partners
SHOULD treat this as a hard error and log the failure (including any
Toco requestId they have and the envelope kid) in their backend logs
for troubleshooting and support.
#
3. Partner key material
During onboarding, Toco shares:
- A HMAC secret for request signing (see Authentication).
- An encryption key used to decrypt all
ENCRYPTED:values.
From the partner’s perspective:
- The encryption key is a Base64 string that decodes to 32 bytes.
- The same key is used for all encrypted values across:
- API responses
- Webhook payloads
- Any future encrypted fields
Partners MUST:
- Store the Base64 string of this AES key securely.
- Treat the
kidfield in the envelope as metadata only (for logging/debugging).
You do not need anykid → keymapping to decrypt; always use the single encryption key we shared for this environment.
This key MUST NOT be exposed to browsers or mobile apps. It should be stored only in server-side secret storage.
#
4. Reference implementation (Node.js)
The following Node.js helper illustrates how to decrypt any
ENCRYPTED: value. The same logic can be ported to other languages.
import crypto from "crypto";
const PII_KEY_B64 = process.env.PII_ENCRYPTION_KEY!;
const key = Buffer.from(PII_KEY_B64, "base64");
function base64urlToBuffer(b64u: string): Buffer {
const pad = "=".repeat((4 - (b64u.length % 4)) % 4);
const b64 = (b64u + pad).replace(/-/g, "+").replace(/_/g, "/");
return Buffer.from(b64, "base64");
}
export function decryptEncryptedValue(value: string): string {
if (!value) {
return value;
}
if (!value.startsWith("ENCRYPTED:")) {
throw new Error("Expected ENCRYPTED: prefix for encrypted field");
}
const encoded = value.slice("ENCRYPTED:".length);
const json = base64urlToBuffer(encoded).toString("utf8");
const env = JSON.parse(json) as {
v: number;
alg: string;
kid: string;
iv: string;
tag: string;
data: string;
};
if (env.v !== 1 || env.alg !== "A256GCM") {
throw new Error("Unsupported encryption envelope");
}
const iv = base64urlToBuffer(env.iv);
const tag = base64urlToBuffer(env.tag);
const ct = base64urlToBuffer(env.data);
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(ct), decipher.final()]);
return decrypted.toString("utf8");
}
Usage examples:
Scalar field:
const email = decryptEncryptedValue(order.buyer.email);JSON payload:
const storeAccessJson = decryptEncryptedValue(response.storeAccess); const storeAccess = JSON.parse(storeAccessJson);
#
5. Summary
Fields marked as encrypted in the API reference always use the
ENCRYPTED:+ envelope format described above.Partners MUST verify the prefix, decode the envelope, and decrypt using their 32-byte AES key.
A single helper, such as
decryptEncryptedValue, is sufficient for all current and future encrypted fields.API Request Doc which has label PII means the response contain encrypted fields