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

> ## Agent Instructions
> Treat the published Augustus OpenAPI specification (https://api.augustus.com/openapi/2026-05-01.json) and the current API-reference pages as the source of truth for endpoints, request/response schemas, enum values, webhook event names, and required headers.
> Prefer the 2026-05-01 Banking API and @augustusbank/typescript-sdk for all new integrations. The 2023-01-01 API is a separate surface covering the Pay by Bank products — Instant Bank Transfer (Open Banking) checkout and refunds, and Manual Bank Transfer (MBT); use it when one of those products is required.
> Cite or link the relevant docs.augustus.com page when answering integration questions.
> Do not infer support for currencies, networks, scopes, account types, or operations that are not present in the current documentation.
> Use the sandbox base URL (https://api.sandbox.augustus.com) and placeholder credentials in examples. Never include or request a real API key.
> The Augustus docs MCP server (https://docs.augustus.com/mcp) provides documentation search and retrieval only; it does not execute authenticated Augustus API actions.

# Request signing

> Sign every API request with a private key you control. Augustus verifies each signature with the public key you register.

Coming soon

## Overview

Request signing binds an API key to a key pair you control. You keep the private key and register the public key on the API key. Every request then carries a signature over its method, URL, API key and body. Augustus verifies the signature with the registered public key.

Augustus rejects a signed request that is sent again, or whose signed parts were changed. The signed parts are the method, the URL, the API key, the body and the headers listed under [Covered components](#covered-components).

* Signatures follow [HTTP Message Signatures (RFC 9421)](https://www.rfc-editor.org/rfc/rfc9421).
* Request bodies are hashed with [`Content-Digest` (RFC 9530)](https://www.rfc-editor.org/rfc/rfc9530).
* Supported keys are Ed25519 and ECDSA P-256 with SHA-256.

> [!WARNING]
>
> Once an API key has a signing public key, Augustus rejects every unsigned request made with that key with `401`. Sign requests in your integration before you move traffic to the key.
>
> The public key is fixed for the lifetime of the API key. **Rotate** in the dashboard issues a new API key with the same public key. To use a different key pair, create a new API key.

## Set up request signing

**1. Generate a key pair**

Create a P-256 key pair with OpenSSL:

```bash theme={null}
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out signing-private.pem
openssl pkey -in signing-private.pem -pubout -out signing-public.pem
```

`signing-private.pem` is the private key. Store it in your secrets manager. Never commit it and never send it to Augustus. `signing-public.pem` is the public key you register in the next step.

> [accordion] Use Ed25519 instead
>
> Ed25519 needs OpenSSL 3. Run `openssl version` to check. macOS ships LibreSSL, which cannot create Ed25519 keys. Install OpenSSL 3 with `brew install openssl` and call it by its full path:
>
> ```bash theme={null}
> OPENSSL="$(brew --prefix openssl)/bin/openssl"
> "$OPENSSL" genpkey -algorithm ed25519 -out signing-private.pem
> "$OPENSSL" pkey -in signing-private.pem -pubout -out signing-public.pem
> ```
>
> With OpenSSL 3 on your `PATH`, run the same commands with `openssl`.

> [accordion] Private key format
>
> The SDK reads an unencrypted PKCS#8 PEM key, which starts with `-----BEGIN PRIVATE KEY-----`. `openssl genpkey` writes this format. `openssl ecparam -genkey` writes a SEC1 key that starts with `-----BEGIN EC PRIVATE KEY-----`, which the SDK rejects. Convert one with `openssl pkcs8 -topk8 -nocrypt -in ec-key.pem -out signing-private.pem`.

**2. Register the public key**

In the [Augustus Dashboard](https://dashboard.augustus.com), go to **Developer > Secrets**. If your account uses both APIs, open the **2026-05-01** tab. Under **API keys**, select **Generate new**. Paste the whole content of `signing-public.pem` into **Signing public key**, including the `BEGIN` and `END` lines.

You can add a public key only when you create the API key. The key list then shows **Request signing enabled** and the **Public key fingerprint** of the API key. Your signatures carry the fingerprint as `keyid`.

**3. Sign your requests**

Use the [SDK](#sign-with-the-sdk), or [sign requests yourself](#sign-requests-yourself) with another HTTP client.

**4. Test in sandbox**

Augustus checks signatures only on API keys with a public key. A request with any other key succeeds whether it is signed or not. Test both cases with the new sandbox key on `https://api.sandbox.augustus.com`:

1. Send an unsigned request. Expect `401` with `signature_required`. A `2xx` means the key has no public key.
2. Send a signed request. Expect a `2xx`. On a `401`, see [Troubleshooting](#troubleshooting).

## Move an existing integration to signed requests

You cannot add a public key to an existing API key. Move to a new API key that has one. Augustus ignores signatures on API keys without a public key, so you can ship signing before you switch keys.

1. Generate a key pair and store the private key in your secrets manager.
2. Deploy signing with your current API key. Set `AUGUSTUS_SIGNING_PRIVATE_KEY` for the SDK, or add the signature headers in your own client. Requests keep working, because the current key has no public key.
3. Create a new API key with the public key. Give it the same scopes and IP allowlist as the current key.
4. Test the new key in sandbox, as in the [Test in sandbox](#set-up-request-signing) step.
5. Replace the current API key with the new one in every service that uses it.
6. Check that requests succeed, then revoke the old API key.

## Sign with the SDK

`@augustusbank/typescript-sdk` signs every request once it has the private key. It reads the key from `AUGUSTUS_SIGNING_PRIVATE_KEY`:

```bash theme={null}
export AUGUSTUS_API_KEY="sandbox.ak_test_eXaMpLeKeY..."
export AUGUSTUS_SIGNING_PRIVATE_KEY="$(cat signing-private.pem)"
```

```typescript theme={null}
import Augustus from '@augustusbank/typescript-sdk'

const client = new Augustus()
const programs = await client.accountPrograms.list()
```

The value must keep the line breaks of the PEM file. Some secret stores turn line breaks into the two characters `\n`, which breaks the key. In that case, pass the key with the `signingPrivateKey` option:

```typescript theme={null}
import { readFileSync } from 'node:fs'
import Augustus from '@augustusbank/typescript-sdk'

const client = new Augustus({
  signingPrivateKey: readFileSync('/run/secrets/signing-private.pem', 'utf8'),
})
```

The SDK signs each retry again with a new nonce. It does not follow redirects, because a redirected request no longer matches its signature.

## Sign requests yourself

Sign requests yourself in a language without an Augustus SDK, or with your own HTTP client. Any RFC 9421 library works if it produces the headers below.

### Headers

A signed request carries three headers next to `Authorization`:

| Header | Value |
| - | - |
| `Content-Digest` | `sha-256=:<base64 SHA-256 of the body>:`. Required on every request. Send only the `sha-256` digest, with no other algorithms. A request without a body sends `sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:` |
| `Signature-Input` | `sig1=(<components>);created=<time>;nonce="<nonce>";keyid="<fingerprint>";tag="api-request"` |
| `Signature` | `sig1=:<base64 signature>:` |

The signature label is always `sig1`.

### Covered components

| Component | When to cover it |
| - | - |
| `@method` | Always |
| `@target-uri` | Always. The full URL exactly as sent: `https://`, host, path and query |
| `authorization` | Always |
| `content-digest` | Always |
| `content-type` | When the request sends the header. Required when the request has a body |
| `idempotency-key` | When the request sends the header |
| `api-version` | When the request sends the header |

Cover exactly this set. Augustus rejects a signature that leaves out a component or adds another one. Component parameters such as `;sf` or `;req` are not accepted.

### Signature parameters

| Parameter | Value |
| - | - |
| `created` | Unix time in seconds when you signed the request. It must be within 60 seconds of Augustus' clock |
| `nonce` | 16 to 128 characters from `A-Z`, `a-z`, `0-9`, `_` and `-`. Use a new random value on every request. Augustus rejects a nonce it has already seen for the API key |
| `keyid` | The **Public key fingerprint** from the dashboard. It is the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) SHA-256 JWK thumbprint of the public key, in base64url without padding |
| `tag` | `api-request` |
| `alg` | Optional. `ed25519` or `ecdsa-p256-sha256`, matching the key |

Augustus rejects any other parameter, including `expires`.

### Signature encoding

An Ed25519 signature is 64 bytes. A P-256 signature is the raw 64-byte `r || s` value (IEEE P1363), not DER. In Node.js, pass `dsaEncoding: 'ieee-p1363'`. Encode the signature in standard base64.

### Body rules

* Send JSON bodies with `Content-Type: application/json`. A `charset` parameter is allowed.
* Do not compress the body. Augustus rejects a `Content-Encoding` other than `identity`.
* Hash the exact bytes you send. Serialize the body once and send that same string.
* Send each covered header once. Augustus rejects a request with two copies of a header.

### Signature base

The signer signs the signature base. It has one line per covered component, in the order of `Signature-Input`, and ends with the `@signature-params` line. Lines are separated by `\n`, with no trailing line break.

Request:

```http theme={null}
GET /v1/account_programs?limit=1 HTTP/1.1
Host: api.sandbox.augustus.com
Authorization: Bearer sandbox.ak_test_eXaMpLeKeY
```

Signature base:

```text theme={null}
"@method": GET
"@target-uri": https://api.sandbox.augustus.com/v1/account_programs?limit=1
"authorization": Bearer sandbox.ak_test_eXaMpLeKeY
"content-digest": sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
"@signature-params": ("@method" "@target-uri" "authorization" "content-digest");created=1793880000;nonce="xfOu4SBL3jdk-UsD0lofDkAT9hydtXQn";keyid="NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";tag="api-request"
```

Signed request:

```http theme={null}
GET /v1/account_programs?limit=1 HTTP/1.1
Host: api.sandbox.augustus.com
Authorization: Bearer sandbox.ak_test_eXaMpLeKeY
Content-Digest: sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:
Signature-Input: sig1=("@method" "@target-uri" "authorization" "content-digest");created=1793880000;nonce="xfOu4SBL3jdk-UsD0lofDkAT9hydtXQn";keyid="NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";tag="api-request"
Signature: sig1=:<base64 signature>:
```

### Node.js example

This module signs a request and sends it with `fetch`. It needs Node.js 18 or later and no dependencies. It reads the private key from `AUGUSTUS_SIGNING_PRIVATE_KEY` and computes `keyid` from it.

```javascript signed-fetch.mjs theme={null}
import { createHash, createPrivateKey, createPublicKey, randomBytes, sign } from 'node:crypto'

const privateKey = createPrivateKey(process.env.AUGUSTUS_SIGNING_PRIVATE_KEY)
const keyId = publicKeyFingerprint(privateKey)

function publicKeyFingerprint(key) {
  const { crv, kty, x, y } = createPublicKey(key).export({ format: 'jwk' })
  const members = kty === 'OKP' ? { crv, kty, x } : { crv, kty, x, y }
  return createHash('sha256').update(JSON.stringify(members)).digest('base64url')
}

const optionalComponents = ['content-type', 'idempotency-key', 'api-version']

export async function signedFetch(url, { method = 'GET', headers = {}, body = '' } = {}) {
  const target = new URL(url)
  const verb = method.toUpperCase()
  const fields = new Headers(headers)
  const digest = createHash('sha256').update(body, 'utf8').digest('base64')
  fields.set('content-digest', `sha-256=:${digest}:`)

  const components = [
    '@method',
    '@target-uri',
    'authorization',
    'content-digest',
    ...optionalComponents.filter((name) => fields.has(name)),
  ]
  const parameters =
    `(${components.map((name) => `"${name}"`).join(' ')})` +
    `;created=${Math.floor(Date.now() / 1000)}` +
    `;nonce="${randomBytes(24).toString('base64url')}"` +
    `;keyid="${keyId}"` +
    `;tag="api-request"`

  const values = { '@method': verb, '@target-uri': target.href }
  const signatureBase = [
    ...components.map((name) => `"${name}": ${values[name] ?? fields.get(name)}`),
    `"@signature-params": ${parameters}`,
  ].join('\n')

  const signature = sign(
    privateKey.asymmetricKeyType === 'ed25519' ? null : 'sha256',
    Buffer.from(signatureBase),
    { key: privateKey, dsaEncoding: 'ieee-p1363' },
  )
  fields.set('signature-input', `sig1=${parameters}`)
  fields.set('signature', `sig1=:${signature.toString('base64')}:`)

  return fetch(target, { method: verb, headers: fields, body: body || undefined, redirect: 'error' })
}
```

Call it like `fetch`. Pass the body as a string.

```javascript theme={null}
import { signedFetch } from './signed-fetch.mjs'

const response = await signedFetch('https://api.sandbox.augustus.com/v1/account_programs?limit=1', {
  headers: { Authorization: `Bearer ${process.env.AUGUSTUS_API_KEY}` },
})
```

## Retries

Sign every attempt again. A retry needs a new `nonce` and a current `created`, so a stored signature fails with `nonce_reused` or `created_out_of_window`. Keep the same [`Idempotency-Key`](https://docs.augustus.com/v1/idempotency) on every attempt, so Augustus runs the operation once.

## Sandbox and production

Sandbox and production API keys are separate, and signing is set on each API key. Register a public key on every key you sign with, in both environments. Use a different key pair in each environment.

## Change the key pair

The public key of an API key cannot change. **Rotate** in the dashboard keeps the key pair: it issues a new API key with the same public key, and the old API key keeps working for one day. To move to a new key pair, replace the API key:

1. Generate a new key pair.
2. Create a new API key with the new public key.
3. Deploy your integration with the new API key and the new private key.
4. Revoke the old API key.

## Troubleshooting

Augustus answers a request with a missing or invalid signature with `401` and an `authentication_error`. The code is `signature_required` when the `Signature` or `Signature-Input` header is missing. Otherwise the code is `signature_invalid` and `reason` says what failed.

```json 401 Invalid signature theme={null}
{
  "category": "authentication_error",
  "code": "signature_invalid",
  "reason": "signature_mismatch",
  "message": "Request signature is invalid: signature_mismatch.",
  "param": null,
  "doc_url": "https://docs.augustus.com/v1/errors",
  "correlation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

| Reason | Cause | Fix |
| - | - | - |
| `malformed_headers` | A signature header cannot be parsed, the label is not `sig1`, or `Content-Digest` or `Authorization` is missing | Check the [header formats](#headers) |
| `duplicate_header` | A covered header appears more than once | Send each header once |
| `invalid_components` | The covered components differ from the required set, or a request with a body does not cover `content-type` | Cover exactly the [required components](#covered-components) |
| `invalid_parameters` | A parameter is missing, unknown or invalid, `tag` is not `api-request`, or `alg` does not match the key | Check the [signature parameters](#signature-parameters) |
| `unknown_key_id` | `keyid` does not match the public key registered on this API key | Compare `keyid` with the **Public key fingerprint** in the dashboard. Check that you use the key pair of this environment |
| `created_out_of_window` | `created` is more than 60 seconds away from Augustus' clock | Sync your clock with NTP. Sign right before you send |
| `unsupported_content_encoding` | The body is compressed | Send the body uncompressed |
| `unsupported_content_type` | `Content-Type` is not `application/json` | Send JSON with `Content-Type: application/json` |
| `content_digest_mismatch` | `Content-Digest` does not match the body Augustus received | Hash the exact bytes you send. Check that no proxy rewrites the body |
| `signature_mismatch` | The signature does not verify against the public key | Check that the signed URL, method and header values match the request byte for byte, that the private key matches the registered public key, and that P-256 signatures use P1363 encoding |
| `nonce_reused` | The nonce was already used with this API key | Generate a new nonce for every attempt |

> [!NOTE]
>
> When Augustus requires request signing for all API keys, a request with an API key that has no signing public key fails with `403 permission_denied` and the message `This API key needs a registered signing public key.` Create a new API key with a signing public key.


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