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

# Encrypting and signing requests

> The exact wire formats: the ENSC-ENC-V1 request envelope, the ENSC-V1 request signature and the ENSC-RESP-V1 sealed response.

`@ensc/sdk` does everything on this page for you. Read on if you integrate from a language without an SDK or want to audit what the SDK does. The formats below are exact; the API refuses anything that deviates.

Order of operations on a write: serialise the JSON, **encrypt** it into the envelope, then **sign** the envelope bytes. On every response: **verify** ENSC's signature, then **open** the sealed body.

## Request encryption (ENSC-ENC-V1)

Applies to every `POST`, `PUT`, `PATCH` and `DELETE` made with a secret or restricted key. `GET` requests have no body and are not encrypted.

### Algorithm

* **Cipher**: AES-256-GCM.
* **Key**: your 32-byte encryption key (base64url-decode `ENSC_ENCRYPTION_KEY`).
* **IV**: 12 random bytes, fresh for every request.
* **Tag**: 16 bytes, transmitted separately from the ciphertext.
* **Additional authenticated data (AAD)**: the UTF-8 bytes of

```
ENSC-ENC-V1\n{METHOD}\n{PATH}\n{merchantId}\n{encKeyId}
```

where `METHOD` is upper-case, `PATH` is the request path without query string (for example `/v1/conversions`), and `\n` is a single line feed.

### Envelope

Serialize your request as JSON (this is the plaintext), encrypt it, and send this object as the HTTP body with `Content-Type: application/json`:

```json theme={null}
{
  "v": 1,
  "encKeyId": "enc_01J…",
  "iv": "<base64url, 16 chars>",
  "ciphertext": "<base64url>",
  "tag": "<base64url, 22 chars>"
}
```

Rules: exactly these five keys, base64url without padding, `v` must be `1`, `encKeyId` must match `^enc_[0-9A-Z]{26}$`, ciphertext must be non-empty and at most 1 MiB. A request without a body still sends an encrypted `{}`.

### Errors

| Code                          | Status | Meaning                                                                                       |
| ----------------------------- | ------ | --------------------------------------------------------------------------------------------- |
| `ENSC_ENCRYPTION_REQUIRED`    | 400    | Body is not a well-formed envelope (plaintext, extra fields, wrong version, empty ciphertext) |
| `ENSC_UNKNOWN_ENCRYPTION_KEY` | 401    | `encKeyId` does not belong to your account and environment                                    |
| `ENSC_ENCRYPTION_KEY_REVOKED` | 401    | The key was revoked, or rotated more than 24 hours ago                                        |
| `ENSC_DECRYPTION_FAILED`      | 400    | Wrong key, wrong AAD (method/path/merchant/key id mismatch), or tampered ciphertext/tag       |
| `ENSC_VALIDATION_FAILED`      | 400    | Decrypted successfully, but the plaintext is not JSON or fails the endpoint's schema          |

## Request signature (ENSC-V1)

The Ed25519 signature is computed over the envelope bytes exactly as sent, not over the plaintext. Encrypt first, then sign.

Every `POST`, `PUT`, `PATCH` and `DELETE` must carry:

```
X-ENSC-Timestamp: <unix seconds>
X-ENSC-Nonce: <unique per request; the SDK uses 18 random bytes, base64url>
X-ENSC-Key-Id: sig_…
X-ENSC-Signature: ed25519=<base64url of the 64-byte signature>
X-ENSC-Idempotency-Key: <8 to 64 URL-safe characters>
```

The signature is Ed25519, with your signing private key, over the UTF-8 bytes of this string (lines joined with a single `\n`):

```
ENSC-V1
{METHOD}                                  upper-case
{PATH}                                    e.g. /v1/conversions, no query string
{sha256_hex(canonical query)}             query pairs sorted by key, URL-encoded, joined with &; the SHA-256 of the empty string when there is none
{sha256_hex(body bytes)}                  the encrypted envelope exactly as sent; the SHA-256 of the empty string when there is none
{timestamp}
{nonce}
{merchantId}
{idempotencyKey}                          empty string if none
```

Rules ENSC enforces:

* The timestamp must be within 300 seconds of ENSC's clock (`ENSC_TIMESTAMP_OUT_OF_WINDOW`).
* A nonce is accepted once (`ENSC_NONCE_REUSED`). Retries must re-sign with a fresh timestamp and nonce.
* `X-ENSC-Key-Id` is required on every write; a write without it is `ENSC_MISSING_SIGNATURE`.
* The key id must be one of your signing keys (`ENSC_MISSING_PUBLIC_KEY` if it is not, on reads and writes alike), and that key must be active or rotated less than 24 hours ago: a revoked key, or one rotated more than 24 hours ago, is `ENSC_INVALID_SIGNATURE`.
* Reads (`GET`) are not signed.

The idempotency key itself is covered in [Idempotency](/guides/idempotency).

## Response decryption (ENSC-RESP-V1)

Every successful (2xx) JSON response to a secret or restricted key is sealed to your signing key and signed by ENSC. Error responses are plain JSON.

### Headers

```
X-ENSC-Signature: ed25519=<base64url, 86 chars>
X-ENSC-Key-Id: <ENSC key id>
X-ENSC-Timestamp: <unix seconds>
X-ENSC-Request-Id: <request id>
Cache-Control: no-store
```

### Body

```json theme={null}
{ "v": 1, "enc": "<base64url, 43 chars>", "ciphertext": "<base64url>" }
```

### Verify, then open

1. Fetch ENSC's public keys from `GET /v1/.well-known/ensc-public-keys.json` (cache them; refetch once if you meet an unknown `X-ENSC-Key-Id`). Pick the entry whose `kid` equals the header and whose `use` includes `"responses"`.
2. Reject the response if `X-ENSC-Timestamp` is more than 300 seconds from your clock.
3. Verify the Ed25519 signature over the UTF-8 bytes of

```
ENSC-RESP-V1\n{requestId}\n{timestamp}\n{sha256_hex(body)}
```

where `body` is the raw response body exactly as received. Do not parse the body before the signature verifies.

4. Open the envelope with HPKE, RFC 9180, **base mode**:
   * KEM `DHKEM(X25519, HKDF-SHA256)` (0x0020), KDF `HKDF-SHA256` (0x0001), AEAD `ChaCha20-Poly1305` (0x0003)
   * Recipient private key: your Ed25519 signing seed converted to X25519 (SHA-512 of the seed, clamp the first 32 bytes; this is the standard Ed25519-to-X25519 conversion)
   * `enc`: the sender's ephemeral public key from the body
   * `info`: UTF-8 bytes of `ENSC-RESP-V1\n{requestId}`
   * `aad`: empty
   * Plaintext: the JSON response documented for the endpoint

### Errors the SDK raises

| Code                     | Meaning                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `ENSC_INVALID_SIGNATURE` | Missing headers or an empty body (an unsealed 2xx), unknown ENSC key id, malformed or failed signature, or timestamp outside the window |
| `ENSC_DECRYPTION_FAILED` | The envelope is malformed or does not open with your signing key (usually a mismatched `signingKeyId` / `signingPrivateKey` pair)       |
| `ENSC_UPSTREAM_FAILED`   | Network error or timeout, an unreadable body, or a non-ENSC answer from a gateway; `status` carries the HTTP status when there was one  |

Every `EnscError` carries `status` (the HTTP status received) and `requestId`. The SDK retries only network errors, timeouts and the statuses 500, 502, 503 and 504, never a 4xx or `429`. The full list is in [Errors](/guides/errors#codes-the-sdk-raises-itself).

## Which key does ENSC seal to?

The signing key named by the `X-ENSC-Key-Id` header on your request. Writes always carry it; send it on reads too. If you omit it on a read and have exactly one usable signing key, ENSC uses that one; with several, it answers `ENSC_MISSING_PUBLIC_KEY` and asks you to state the key. A key id that is not one of your keys is `ENSC_MISSING_PUBLIC_KEY` on reads and writes alike.

## Reference implementation

`@ensc/sdk` is the reference: `encryptRequestBody` and `openSealedResponse` in its source do exactly the steps above using Web Crypto and the `@noble` libraries. If your implementation disagrees with the SDK against the same inputs, the SDK is right.
