---
title: "Signing requests — Anis Developers"
description: "The exact rules for signing an Anis Partner request with HTTP Message Signatures — profiles, components, the signature base and the traps."
url: https://developers.anis.ly/docs/signing-requests/
language: en
---

> Every page of this documentation: https://developers.anis.ly/llms.txt

# Signing requests

> **Using an SDK?**
>
> The SDKs do all of this for you. This page is for writing your own client, or for understanding a refusal.

Every call except enrolment and Anis’s public keys is signed with **HTTP Message Signatures (RFC 9421)**, using **ECDSA P-256 with SHA-256**. You sign a precise text — the *signature base* — built from parts of the request. Anis rebuilds the same text from what it received and checks your signature against it. One different byte and the call is refused as [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md), with no hint which byte, on purpose.

## The three kinds of signed call

Which parts you sign is decided by the route — see each route’s page in the [API reference](https://developers.anis.ly/reference.md).

| Kind | Used by | Signed parts, in this exact order |
| --- | --- | --- |
| **Read** | every `GET` | `@method` `@authority` `@path` `@query` `x-anis-date` |
| **Change** | the two reveals and the signature self-check | `@method` `@authority` `@path` `@query` `content-digest` `nonce` `x-anis-date` |
| **Order** | placing an order | `@method` `@authority` `@path` `@query` `content-digest` `nonce` `idempotency-key` `x-anis-date` |

`idempotency-key` comes **before** `x-anis-date`, not after it.

## The headers you send

| Header | On | Value |
| --- | --- | --- |
| `X-Anis-Date` | every signed call | The time, ISO-8601 UTC: `2026-09-19T08:00:00Z` |
| `Content-Digest` | changes and orders | `sha-256=:<base64 of SHA-256 of the exact body bytes>:` |
| `Nonce` | changes and orders | A fresh random value for every request (the SDKs use 128 random bits, base64url) |
| `Idempotency-Key` | orders | Your operation id: a UUID, lower-case with hyphens |
| `Signature-Input` | every signed call | The parameters below |
| `Signature` | every signed call | `sig1=:<base64 of the 64-byte signature>:` |

## The parameters

```text
sig1=("@method" "@authority" "@path" "@query" "x-anis-date");created=1789804800;expires=1789804860;keyid="3f2a9c14-8d6e-4b21-9f07-5c8ab2d61e43";alg="ecdsa-p256-sha256"
```

- The label is always `sig1`, on both `Signature-Input` and `Signature`.
- `created` and `expires` are Unix seconds. Anis allows at most 300 seconds between them; **use 60 or less** (the SDKs do) — see [Clock](#clock).
- `keyid` is your key id from enrolment: a UUID, lower-case with hyphens.
- `alg` is always `ecdsa-p256-sha256`. There is nothing to negotiate.
- **On changes and orders, add `;nonce="<the Nonce header>"` at the end**, and it must equal the `Nonce` header. On a read it must be absent. A mismatch is refused as `invalid_credentials` before the signature is even checked.

Every signing failure — a wrong base, a stale time, a digest that does not match the body — is answered [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md), on purpose.

## The signature base

One line per signed part, `"name": value`, each ending with a newline — then a last line for `"@signature-params"`, **with no newline after it**. An order:

```text
"@method": POST
"@authority": partners.anis.ly
"@path": /v1/wallets/2f1c8a94-6d37-4e52-b8a1-0c9e5d3f7b26/orders
"@query": ?
"content-digest": sha-256=:3AuIAnFFepkxwk6edx9OofFKHz5L8cSdZNVmztycemo=:
"nonce": b2F1dGgtbm9uY2UtMDAx
"idempotency-key": 9b2e4f17-3c6a-4d58-b0e1-7a5c8d2f6b34
"x-anis-date": 2026-09-19T08:00:00Z
"@signature-params": ("@method" "@authority" "@path" "@query" "content-digest" "nonce" "idempotency-key" "x-anis-date");created=1789804800;expires=1789804860;keyid="3f2a9c14-8d6e-4b21-9f07-5c8ab2d61e43";alg="ecdsa-p256-sha256";nonce="b2F1dGgtbm9uY2UtMDAx"
```

| Part | Value |
| --- | --- |
| `@method` | The method, upper-case |
| `@authority` | The host — plus the port if it is not the scheme’s default — **lower-case** |
| `@path` | The path, exactly as sent. Every path is fixed words and UUIDs, so it never needs percent-encoding |
| `@query` | `?` followed by the query string exactly as sent. **No query signs `?`** |
| `content-digest`, `nonce`, `idempotency-key`, `x-anis-date` | The header’s value, exactly as sent |

Sign the base’s UTF-8 bytes with ECDSA P-256 and SHA-256. Send the signature in its **64-byte** form — `r` then `s`, 32 bytes each — never DER.

## The traps

- **No query signs `?`, not an empty string.**
- **The query is signed exactly as sent** — not re-ordered, re-encoded or rebuilt from parsed values. A paging cursor goes back exactly as you received it.
- **The host is lower-case.** A mixed-case host signs text Anis never rebuilds.
- **Each change digests exactly what it sends.** A **reveal sends no body at all** — not even `{}` — so its digest is that of zero bytes: `sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:`. The **signature self-check** sends exactly `{}`: `sha-256=:RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=:`. An order digests its JSON body — compute the digest after the body is final, and send exactly those bytes.
- **The signature is 64 bytes, never DER.** .NET and WebCrypto produce the 64-byte form. PHP’s `openssl_sign` and Python’s `cryptography` produce DER and need converting; a 70-to-72-byte signature is DER.

## Clock

Anis accepts a signature whose `created` is at most about 30 seconds ahead of its clock or 5 minutes behind it, and whose `expires` has not passed. Anis’s answers carry their own time, and a client accepts an answer only within 60 seconds of its own clock (see [Verifying answers](https://developers.anis.ly/docs/verifying-answers.md)).

That is why **signatures should live 60 seconds or less**: with a longer window, a server clock a few minutes slow could get an order accepted and completed, and then throw away the answer that carries the card codes as too old. Keep your server clock synchronised.

## When a signature will not verify

Call [Check your signature](https://developers.anis.ly/reference/check-signature.md). It reports the method, address, path and query Anis signed over and the key it found. Compare them with your base, line by line.

Before your client calls Anis at all, run it against the [signing test vectors](https://developers.anis.ly/docs/test-vectors.md): they give the exact base for every kind of signed call.
