> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slash.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Message Level Encryption (MLE)

> Encrypt API request and response payloads end-to-end.

Message Level Encryption (MLE) encrypts the JSON body of an API request and
response with a key that only you and Slash hold, adding a second layer of
protection on top of TLS. Even if a request or response is logged or
intercepted anywhere in between, the payload stays opaque.

MLE is:

* **Opt-in per request.** Pass your key ID in the `MLE-KEY-ID` header to opt a
  request in. Requests without the header keep working as plain JSON, even if
  your entity has active keys — so you can roll MLE out endpoint by endpoint.
* **Symmetric.** Payloads are sealed with **AES-256-GCM** using a shared
  256-bit secret created in the dashboard. There are no key pairs, JWKs, or
  JWE envelopes to manage.
* **Two-way.** When a request opts in, Slash decrypts your request body with
  the key and encrypts the response body back to you with the same key.

## The envelope

An MLE payload is a single-field JSON object:

```json theme={null}
{ "encryptedData": "<base64url(iv || ciphertext || tag)>" }
```

| Component    | Detail                                                                          |
| ------------ | ------------------------------------------------------------------------------- |
| Cipher       | AES-256-GCM                                                                     |
| `iv`         | 12 random bytes, generated fresh for every message                              |
| `ciphertext` | The UTF-8 JSON payload, encrypted                                               |
| `tag`        | 16-byte GCM authentication tag                                                  |
| Encoding     | `base64url` over the concatenation of `iv`, `ciphertext`, `tag` (in that order) |

Most AES-GCM implementations (WebCrypto, Node, Python, Java, Go) already emit
`ciphertext || tag` as one buffer, so building the envelope is just prepending
your IV and base64url-encoding.

## Setup

### 1. Create an encryption key

In the [dashboard](https://app.slash.com), go to **Settings → API →
Encryption Keys** and click **Create Encryption Key**.

You get back two values:

* **Key ID** (`tok_...`) — the identifier you pass in the `MLE-KEY-ID`
  header. Visible in the dashboard at any time.
* **Secret** — a base64url-encoded 256-bit AES key.

<Warning>
  The secret is shown **exactly once**, at creation time. Store it in your
  secret manager immediately — if you lose it, revoke the key and create a new
  one.
</Warning>

### 2. Encrypt your request body

Decode the secret, then seal the JSON body into the envelope:

<CodeGroup>
  ```ts Node.js theme={null}
  import { createCipheriv, randomBytes } from "node:crypto";

  const key = Buffer.from(process.env.SLASH_MLE_SECRET, "base64url"); // 32 bytes

  function encrypt(payload) {
    const iv = randomBytes(12);
    const cipher = createCipheriv("aes-256-gcm", key, iv);
    const ciphertext = Buffer.concat([
      cipher.update(JSON.stringify(payload), "utf8"),
      cipher.final(),
    ]);
    return {
      encryptedData: Buffer.concat([iv, ciphertext, cipher.getAuthTag()]).toString(
        "base64url"
      ),
    };
  }
  ```

  ```python Python theme={null}
  import base64
  import json
  import os

  from cryptography.hazmat.primitives.ciphers.aead import AESGCM

  secret = os.environ["SLASH_MLE_SECRET"]
  key = base64.urlsafe_b64decode(secret + "=" * (-len(secret) % 4))  # 32 bytes

  def encrypt(payload: dict) -> dict:
      iv = os.urandom(12)
      sealed = AESGCM(key).encrypt(iv, json.dumps(payload).encode(), None)  # ciphertext || tag
      encoded = base64.urlsafe_b64encode(iv + sealed).rstrip(b"=").decode()
      return {"encryptedData": encoded}
  ```
</CodeGroup>

### 3. Send the request with `MLE-KEY-ID`

Send the envelope as the request body and pass your key ID in the
`MLE-KEY-ID` header. Everything else about the request — URL, method, query
parameters, authentication — stays exactly the same.

```bash theme={null}
curl https://api.slash.com/contact \
  -X POST \
  -H "X-API-Key: $SLASH_API_KEY" \
  -H "MLE-KEY-ID: tok_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "encryptedData": "9Zw1..." }'
```

GET requests have no body to encrypt — just pass the header and the response
comes back encrypted.

### 4. Decrypt the response

Responses to opted-in requests are served as plain `application/json`
containing the same single-field envelope. Decrypt it with the same key:

<CodeGroup>
  ```ts Node.js theme={null}
  import { createDecipheriv } from "node:crypto";

  function decrypt({ encryptedData }) {
    const data = Buffer.from(encryptedData, "base64url");
    const decipher = createDecipheriv("aes-256-gcm", key, data.subarray(0, 12));
    decipher.setAuthTag(data.subarray(data.length - 16));
    const plaintext = Buffer.concat([
      decipher.update(data.subarray(12, data.length - 16)),
      decipher.final(),
    ]);
    return JSON.parse(plaintext.toString("utf8"));
  }
  ```

  ```python Python theme={null}
  def decrypt(envelope: dict) -> dict:
      encoded = envelope["encryptedData"]
      data = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4))
      plaintext = AESGCM(key).decrypt(data[:12], data[12:], None)
      return json.loads(plaintext)
  ```
</CodeGroup>

## Complete example

```ts Node.js theme={null}
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";

const key = Buffer.from(process.env.SLASH_MLE_SECRET, "base64url");
const MLE_KEY_ID = "tok_live_...";

function encrypt(payload) {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([
    cipher.update(JSON.stringify(payload), "utf8"),
    cipher.final(),
  ]);
  return {
    encryptedData: Buffer.concat([iv, ciphertext, cipher.getAuthTag()]).toString(
      "base64url"
    ),
  };
}

function decrypt({ encryptedData }) {
  const data = Buffer.from(encryptedData, "base64url");
  const decipher = createDecipheriv("aes-256-gcm", key, data.subarray(0, 12));
  decipher.setAuthTag(data.subarray(data.length - 16));
  const plaintext = Buffer.concat([
    decipher.update(data.subarray(12, data.length - 16)),
    decipher.final(),
  ]);
  return JSON.parse(plaintext.toString("utf8"));
}

const res = await fetch("https://api.slash.com/contact", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.SLASH_API_KEY,
    "MLE-KEY-ID": MLE_KEY_ID,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(
    encrypt({
      name: "Slash Financial",
      recipientType: "contact",
      recipientLegalName: "Slash Financial Inc",
      recipientEmail: "ap@slash.com",
    })
  ),
});

const { contact } = decrypt(await res.json());
console.log("Created contact:", contact.id);
```

## Card reveals through vault.slash.com

MLE works on [card reveals](/api-reference/card-get-by-id) through
`vault.slash.com` exactly like on `api.slash.com`: pass `MLE-KEY-ID`, get the
same encrypted envelope back, decrypt with the same key.

## Key management

* **Multiple active keys** can exist at once, so rotation is zero-downtime:
  create a new key, move your integration over, then revoke the old one.
* **Revoke keys** from the same dashboard page. Requests referencing a revoked
  key are rejected with `400`.
* Key IDs are not secret — only the secret is. Treat the secret like an API
  key: keep it server-side, never ship it to a browser or mobile client.

## Errors

MLE failures use the [standard error envelope](/introduction#errors) and fail
closed — an opted-in request is never silently processed or answered in
plaintext.

| Status | Message                                                                                            | What it means                                                                                   |
| ------ | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `400`  | `MLE-KEY-ID is invalid, revoked, or does not belong to this legal entity`                          | The key ID is unknown, revoked, or owned by a different legal entity.                           |
| `400`  | `MLE-KEY-ID must be provided once`                                                                 | The header was sent more than once.                                                             |
| `400`  | `Unsupported MLE envelope`                                                                         | The request body is not a `{ "encryptedData": "..." }` object.                                  |
| `400`  | `Invalid MLE payload`                                                                              | `encryptedData` is too short to contain an IV and tag.                                          |
| `400`  | `Unable to decrypt MLE payload`                                                                    | Decryption failed — wrong key, corrupted ciphertext, or a failed GCM authentication check.      |
| `400`  | `Decrypted MLE payload is not valid JSON`                                                          | Decryption succeeded but the plaintext isn't JSON.                                              |
| `400`  | `MLE is not supported for endpoints with non-JSON responses. Retry without the MLE-KEY-ID header.` | The endpoint returns a non-JSON body (e.g. a PDF), which can't be wrapped in the JSON envelope. |
