> ## 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 **2023-01-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.sand.getivy.de`:

1. Send an unsigned request. Expect `401` with the message `A request signature is 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 `IVY_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 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

`@getivy/node-sdk` signs every request once it has the private key. It reads the key from `IVY_SIGNING_PRIVATE_KEY`:

```bash theme={null}
export IVY_API_KEY="<api-key>"
export IVY_SIGNING_PRIVATE_KEY="$(cat signing-private.pem)"
```

```typescript theme={null}
import Ivy from '@getivy/node-sdk'

const client = new Ivy({ environment: 'sandbox' })
const balance = await client.balance.retrieve({ currency: 'EUR' })
```

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 Ivy from '@getivy/node-sdk'

const client = new Ivy({
  environment: 'sandbox',
  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 `X-Ivy-Api-Key`:

| 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 |
| `x-ivy-api-key` | 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}
POST /api/service/balance/retrieve HTTP/1.1
Host: api.sand.getivy.de
X-Ivy-Api-Key: <api-key>
Content-Type: application/json

{"currency":"EUR"}
```

Signature base:

```text theme={null}
"@method": POST
"@target-uri": https://api.sand.getivy.de/api/service/balance/retrieve
"x-ivy-api-key": <api-key>
"content-digest": sha-256=:8v0KgjsuNL2JNY85D6HEZhyNYIFQhMKzpj8hDnr/3ig=:
"content-type": application/json
"@signature-params": ("@method" "@target-uri" "x-ivy-api-key" "content-digest" "content-type");created=1793880000;nonce="xfOu4SBL3jdk-UsD0lofDkAT9hydtXQn";keyid="NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";tag="api-request"
```

Signed request:

```http theme={null}
POST /api/service/balance/retrieve HTTP/1.1
Host: api.sand.getivy.de
X-Ivy-Api-Key: <api-key>
Content-Type: application/json
Content-Digest: sha-256=:8v0KgjsuNL2JNY85D6HEZhyNYIFQhMKzpj8hDnr/3ig=:
Signature-Input: sig1=("@method" "@target-uri" "x-ivy-api-key" "content-digest" "content-type");created=1793880000;nonce="xfOu4SBL3jdk-UsD0lofDkAT9hydtXQn";keyid="NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";tag="api-request"
Signature: sig1=:<base64 signature>:

{"currency":"EUR"}
```

### 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 `IVY_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.IVY_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',
    'x-ivy-api-key',
    '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.sand.getivy.de/api/service/balance/retrieve', {
  method: 'POST',
  headers: {
    'X-Ivy-Api-Key': process.env.IVY_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ currency: 'EUR' }),
})
```

## 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/reference/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`. The `message` is `A request signature is required.` when the `Signature` or `Signature-Input` header is missing. Otherwise it is `Request signature is invalid: <reason>.`

```json 401 Invalid signature theme={null}
{
  "message": "Request signature is invalid: signature_mismatch.",
  "category": "invalid_request_error",
  "docUrl": "https://docs.augustus.com/reference/authentication"
}
```

| Reason | Cause | Fix |
| - | - | - |
| `malformed_headers` | A signature header cannot be parsed, the label is not `sig1`, or `Content-Digest` or `X-Ivy-Api-Key` 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` 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.