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

# Encrypted request bodies

> Encrypt the body of a request that carries Provider credentials with your Workspace's public key.

Some operations take Provider credentials, such as an API key or an OAuth
client secret. These operations accept only an encrypted body. Your code
encrypts the body with your Workspace's public key, and only Parable can
decrypt it. The private key stays in your Workspace's key ring, the
`credential-encryption` key that
[Data Encryption](https://docs.parable.work/protection/data-encryption)
describes.

The [SDKs](#encrypt-with-an-sdk) encrypt the body for you. Plain HTTP clients
build the [envelope](#build-the-envelope-yourself) themselves.

## Operations that need an encrypted body

| Operation | Purpose |
| - | - |
| `POST /api/connectors` | Connect a Provider. See [Authentication strategies](/connectors/auth.md). |
| `POST /api/connectors/configure` | Change the credentials or settings of a Provider connection. |

The [API reference](/api-reference/overview.md) shows the body of these
operations before encryption. Encrypt that JSON, then send the envelope in its
place. If you send the plain JSON, the request fails with
`400 Encrypted payload is required` and changes nothing.

## Get your Workspace's public key

Each Workspace has its own key. Send `GET /api/security/encryption-key` with
the `X-Tenant` header of the Workspace that you call. The operation needs no
token.

```bash theme={null}
curl "$PARABLE_API_URL/api/security/encryption-key" \
  -H "X-Tenant: $PARABLE_WORKSPACE"
```

```json Response (shortened) theme={null}
{
  "data": {
    "publicKey": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA...\n-----END PUBLIC KEY-----\n",
    "algorithm": "AES_256_GCM_RSA_OAEP_256",
    "keyId": "q3mXb2Z0kVw"
  },
  "meta": { "requestId": "..." }
}
```

| Field | Value |
| - | - |
| `publicKey` | A 4096-bit RSA public key, PEM-encoded. |
| `algorithm` | The envelope to build. It is `AES_256_GCM_RSA_OAEP_256`. |
| `keyId` | The ID of the key. Send it back in the envelope. |

You can keep the key and use it for many requests. A body that you encrypt
with the key of a different Workspace fails to decrypt.

> [!NOTE]
>
> `GET /api/security/content-encryption-key` returns a different key, which
> [Pulse](https://docs.parable.work/pulse/primer) clients use. A request body
> that you encrypt with it fails.

## Encrypt with an SDK

Get the key with the SDK, give it to the client, then call the operation as
usual. The client encrypts the body of each operation that needs it. It sends
other operations unchanged.

```typescript TypeScript theme={null}
const key = await sdk.security.encryptionKey();
sdk.setPublicEncryptionKey(key);

const connection = await sdk.connectors.addConnectorToTenant({
  connectorId: "<connectorId of the method>",
  groupingId: "<id of the method>",
  authenticationStrategyId: "<id of the strategy>",
  authenticationConfig: { token: "<GitHub personal access token>" },
  ingestionConfig: { orgId: "acme-corp" },
  name: "Acme GitHub",
  idempotencyKey: "acme-github-2026-10-20",
});
```

```python Python theme={null}
from parable_types_web_api import AddConnectorToTenantInput
from parable_web_api_sdk import PublicEncryptionKey

key = sdk.security.encryption_key()
sdk.set_public_encryption_key(
    PublicEncryptionKey(public_key=key.public_key, algorithm=key.algorithm, key_id=key.key_id)
)

connection = sdk.connectors.add_connector_to_tenant(
    AddConnectorToTenantInput(
        connector_id="<connectorId of the method>",
        grouping_id="<id of the method>",
        authentication_strategy_id="<id of the strategy>",
        authentication_config={"token": "<GitHub personal access token>"},
        ingestion_config={"orgId": "acme-corp"},
        name="Acme GitHub",
        idempotency_key="acme-github-2026-10-20",
    )
)
```

```go Go theme={null}
key, err := sdk.SecurityNamespace.EncryptionKey(ctx)
if err != nil {
	log.Fatal(err)
}
sdk.SetPublicEncryptionKey(&webapisdk.PublicEncryptionKey{
	PublicKey: string(key.PublicKey),
	Algorithm: string(key.Algorithm),
	KeyID:     key.KeyId,
})

// input is a webapitypes.AddConnectorToTenantInput. Pass nil options to use
// the key that the client holds.
connection, err := sdk.ConnectorsNamespace.AddConnectorToTenant(ctx, input, nil)
```

```rust Rust theme={null}
use parable_web_api_sdk::{EncryptedRequestOptions, PublicEncryptionKey};

// options names your Workspace, and input is a types::AddConnectorToTenantInput.
let key = sdk.security.encryption_key(Some(&options)).await?;
let encrypted = EncryptedRequestOptions {
    public_encryption_key: Some(PublicEncryptionKey {
        public_key: key.public_key,
        algorithm: key.algorithm.to_string(),
        key_id: key.key_id,
    }),
    request_options: Some(options.clone()),
};

let connection = sdk
    .connectors
    .add_connector_to_tenant(input, Some(&encrypted))
    .await?;
```

The TypeScript, Python, and Go clients also take the key when you create
them, as `encryption.publicEncryptionKey`, `public_encryption_key`, or
`Encryption.PublicEncryptionKey`. Each call can also take its own key. The
Rust client takes the key on each call only. Each SDK page shows how to
create the client.

## Build the envelope yourself

The `AES_256_GCM_RSA_OAEP_256` envelope encrypts the body with a new AES key,
then encrypts that AES key with your Workspace's RSA key. Do these steps for
each request:

1. Serialize the request body as UTF-8 JSON.
2. Make a random 32-byte AES key and a random 12-byte IV.
3. Encrypt the JSON with AES-256-GCM, with no additional data. Keep the
   16-byte authentication tag at the end of the ciphertext.
4. Encrypt the AES key with RSA-OAEP. Use SHA-256 for the hash and for MGF1,
   with no label.
5. Encode each binary value in standard Base64, with padding.
6. Send this JSON object as the request body, with the usual `Authorization`,
   `X-Tenant`, and `Content-Type: application/json` headers.

| Envelope field | Value |
| - | - |
| `algorithm` | `AES_256_GCM_RSA_OAEP_256`, the `algorithm` of the key |
| `keyId` | The `keyId` of the key |
| `payload` | The AES-GCM ciphertext followed by its tag, in Base64 |
| `encryptedKey` | The RSA-OAEP ciphertext of the AES key, in Base64 |
| `iv` | The 12-byte IV, in Base64 |

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

import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

base_url = os.environ["PARABLE_API_URL"]
headers = {
    "Authorization": f"Bearer {os.environ['PARABLE_API_TOKEN']}",
    "X-Tenant": os.environ["PARABLE_WORKSPACE"],
}

# 1. Get your Workspace's public key.
key = requests.get(
    f"{base_url}/api/security/encryption-key",
    headers={"X-Tenant": headers["X-Tenant"]},
    timeout=30,
).json()["data"]

# 2. Encrypt the body with a new AES-256-GCM key, then wrap that key with RSA-OAEP.
body = {
    "connectorId": "<connectorId of the method>",
    "groupingId": "<id of the method>",
    "authenticationStrategyId": "<id of the strategy>",
    "authenticationConfig": {"token": "<GitHub personal access token>"},
    "ingestionConfig": {"orgId": "acme-corp"},
    "name": "Acme GitHub",
    "idempotencyKey": "acme-github-2026-10-20",
}
plaintext = json.dumps(body).encode("utf-8")
aes_key = AESGCM.generate_key(bit_length=256)
iv = os.urandom(12)
payload = AESGCM(aes_key).encrypt(iv, plaintext, None)  # ciphertext + 16-byte tag

public_key = serialization.load_pem_public_key(key["publicKey"].encode("ascii"))
encrypted_key = public_key.encrypt(
    aes_key,
    padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
)


def b64(data: bytes) -> str:
    return base64.b64encode(data).decode("ascii")


envelope = {
    "algorithm": key["algorithm"],
    "keyId": key["keyId"],
    "payload": b64(payload),
    "encryptedKey": b64(encrypted_key),
    "iv": b64(iv),
}

# 3. Send the envelope in place of the body.
response = requests.post(f"{base_url}/api/connectors", headers=headers, json=envelope, timeout=30)
print(response.status_code, response.json())
```

```javascript Node.js theme={null}
import { constants, createCipheriv, publicEncrypt, randomBytes } from "node:crypto";

const baseUrl = process.env.PARABLE_API_URL;
const headers = {
  Authorization: `Bearer ${process.env.PARABLE_API_TOKEN}`,
  "X-Tenant": process.env.PARABLE_WORKSPACE,
};

// 1. Get your Workspace's public key.
const keyResponse = await fetch(`${baseUrl}/api/security/encryption-key`, {
  headers: { "X-Tenant": headers["X-Tenant"] },
});
const { data: key } = await keyResponse.json();

// 2. Encrypt the body with a new AES-256-GCM key, then wrap that key with RSA-OAEP.
const body = {
  connectorId: "<connectorId of the method>",
  groupingId: "<id of the method>",
  authenticationStrategyId: "<id of the strategy>",
  authenticationConfig: { token: "<GitHub personal access token>" },
  ingestionConfig: { orgId: "acme-corp" },
  name: "Acme GitHub",
  idempotencyKey: "acme-github-2026-10-20",
};
const aesKey = randomBytes(32);
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", aesKey, iv);
const payload = Buffer.concat([
  cipher.update(JSON.stringify(body), "utf8"),
  cipher.final(),
  cipher.getAuthTag(), // the 16-byte tag goes after the ciphertext
]);
const encryptedKey = publicEncrypt(
  { key: key.publicKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
  aesKey,
);

const envelope = {
  algorithm: key.algorithm,
  keyId: key.keyId,
  payload: payload.toString("base64"),
  encryptedKey: encryptedKey.toString("base64"),
  iv: iv.toString("base64"),
};

// 3. Send the envelope in place of the body.
const response = await fetch(`${baseUrl}/api/connectors`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify(envelope),
});
console.log(response.status, await response.json());
```

The Python example needs the `requests` and `cryptography` packages. The
Node.js example needs Node.js 18 or later.

### The RSA-only envelope

The `RSA_OAEP_256` envelope encrypts the JSON body directly with RSA-OAEP and
SHA-256. It has only `algorithm`, `keyId`, and `payload`. A 4096-bit key
encrypts at most 446 bytes this way, so most connection bodies do not fit.
Use `AES_256_GCM_RSA_OAEP_256`.

## Errors

Parable checks the token and its permissions first, then decrypts the body.
A decryption error is a `400` problem response with no `code`. Its `detail`
says what failed.

| `detail` | Cause and fix |
| - | - |
| `Encrypted payload is required` | The body is plain JSON, not an envelope. Send the envelope. |
| `Invalid encrypted payload envelope` | The body is not a JSON object, or an envelope field is not a string. |
| `Unsupported encryption algorithm` | `algorithm` is not `AES_256_GCM_RSA_OAEP_256` or `RSA_OAEP_256`. |
| `Failed to decrypt request payload` | A field is missing, a value is not valid Base64, or the key belongs to a different Workspace. Get the key again with the `X-Tenant` of the Workspace that you call, then build a new envelope. |
| `Decrypted payload must be valid JSON` | The body that you encrypted is not JSON. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.