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, 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.

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

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.
  • 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, 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:

"@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).

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. 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: they give the exact base for every kind of signed call.