Skip to main content
Available on Enterprise plans.
Task run inputs and outputs can carry sensitive values, such as account numbers or government IDs. Payload encryption encrypts the entire input and output of every task run in the API responses Deck returns to you, so those values never travel over the wire in plaintext.

How it works

When encryption is on, the input and output fields on every task run come back as encrypted strings instead of JSON objects. You hold the key and decrypt them client-side.
  • Organization-wide and all-or-nothing. The setting applies to your whole organization and encrypts the complete input and output, not selected fields. For per-field control over inputs, use tokenization instead.
  • Applies to every task run. Encryption happens at read time, so the setting governs all responses uniformly, including runs that completed before you turned it on. Turning it off returns every response to plaintext.
  • The Console always shows plaintext. Only requests authenticated with an API key receive encrypted payloads. Task run details in the Console remain readable.
  • API responses only. Encryption applies to task run input and output in API responses. Inputs and outputs aren’t included in webhook payloads, so webhooks are unaffected.

Enabling encryption

You manage encryption from the Console.
1

Open API Keys & Encryption

Go to Developer settings → API Keys & Encryption.
2

Generate an encryption key

Create a named key. Deck shows the full key secret once, at creation. Copy or download it then and store it securely. A lost key cannot be retrieved, only replaced with a new one.
3

Turn on encryption

Enable the setting. From that point on, every task run response to an API-key request is encrypted with your oldest active key. You must have at least one active key before you can enable encryption.

Reading an encrypted task run

With encryption on, input and output are returned as enc_-prefixed strings. Every other field is unchanged.
GET /v2/task-runs/{run_id}?include=input
{
  "id": "trun_a1b2c3d4e5f6g7h8",
  "object": "task_run",
  "status": "completed",
  "result": "success",
  "task_id": "task_p9o8i7u6y5t4r3e2",
  "credential_id": "cred_5f8a2c91b7e34d60",
  "session_id": "sess_2d4f6a8c0e1b3d5f",
  "agent_id": "agt_7c1e9b3a5d2f4068",
  "source_id": "src_9b2d4f6a8c0e1357",
  "runtime_ms": 45200,
  "input": "enc_AQEs...kJ8x2mP9",
  "output": "enc_AQEs...Qw3rT7uV",
  "errors": null,
  "interaction": null,
  "created_at": "2026-06-11T14:30:00Z",
  "updated_at": "2026-06-11T14:35:45Z",
  "request_id": "req_2Wd6sE9fR1gT4yUh"
}

Envelope format

Each encrypted value is a single string: the enc_ prefix followed by a base64url-encoded, AES-256-GCM payload. The ID of the key used to encrypt the value is embedded in the payload, so you can hold several keys at once and always know which one decrypts a given value.

Decrypting a value

After base64url-decoding the part of the string that follows the enc_ prefix, the bytes are laid out as:
BytesField
1Format version
1Key ID length (n)
nKey ID, ASCII (for example, enck_Bv7cX1zL5kM9nP3r)
12Nonce
16Authentication tag
variableCiphertext
The payload is base64url-encoded without padding. To recover the plaintext JSON object:
  1. Strip the enc_ prefix and base64url-decode the remainder.
  2. Read the embedded key ID and look up the matching key secret from your store.
  3. Derive the 32-byte AES key by hex-decoding the part of the ek_live_ secret after the prefix.
  4. Decrypt the ciphertext with AES-256-GCM using the nonce and authentication tag.
  5. Parse the result as JSON.
import crypto from "node:crypto";

// Your keyring: encryption key ID -> the ek_live_ secret you saved at creation.
const keyring = {
  enck_Bv7cX1zL5kM9nP3r:
    "ek_live_9d4e7a02c61b835f0a2d94e7c13b68f5d40a91e2c7b3f8061d5a29c4e8b07f31",
};

function decrypt(value) {
  // 1. Strip the prefix and base64url-decode.
  const payload = Buffer.from(value.slice("enc_".length), "base64url");

  // 2. Parse the envelope: version | keyIdLen | keyId | nonce | tag | ciphertext
  let offset = 0;
  const version = payload[offset++];
  if (version !== 1) throw new Error(`Unsupported envelope version ${version}`);

  const keyIdLen = payload[offset++];
  const keyId = payload.subarray(offset, offset + keyIdLen).toString("ascii");
  offset += keyIdLen;

  const nonce = payload.subarray(offset, offset + 12);
  offset += 12;
  const tag = payload.subarray(offset, offset + 16);
  offset += 16;
  const ciphertext = payload.subarray(offset);

  // 3. Look up the key and derive the 32-byte AES key from the hex secret.
  const secret = keyring[keyId];
  if (!secret) throw new Error(`No key for ${keyId}`);
  const key = Buffer.from(secret.slice("ek_live_".length), "hex");

  // 4. Decrypt with AES-256-GCM, then 5. parse JSON.
  const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
  decipher.setAuthTag(tag);
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
  return JSON.parse(plaintext.toString("utf8"));
}

const input = decrypt(taskRun.input);
const output = decrypt(taskRun.output);

Rotating keys

Your oldest active key encrypts responses, so creating a newer key changes nothing until you revoke the older one. Rotate with no gap in decryption:
  1. Create the new key and add it to your application’s secrets. Responses still come back on the old key, so nothing breaks yet.
  2. Revoke the old key to cut over. The new key becomes your oldest active key and starts encrypting — and your application already has it.
Revoked keys stay listed as an audit trail, and because decryption happens in your application from the key ID embedded in each value, revoking a key never affects values you still hold the key for. You can’t revoke your last active key while encryption is enabled — disable encryption or create a replacement first.