---
name: anis-partners
description: Rules for writing code that calls the Anis Partner API (developers.anis.ly) — enrolling a key, signing requests, verifying answers, placing and resuming orders, handling refusals and card codes. Use it whenever code calls the Anis Partner API or handles its answers.
---

# Anis Partner API — rules for AI coding assistants

These rules come from the Anis developer documentation. The order rules decide whether a retry buys cards twice or
loses card codes that were paid for, so follow them even when a shortcut looks harmless. When a rule and your own
assumption disagree, the rule wins; when unsure, read the linked page.

- Documentation index: https://developers.anis.ly/llms.txt — every page also as Markdown (add `.md` to its address)
- API description: https://developers.anis.ly/openapi.json — it cannot express signing
- Error codes as data: https://developers.anis.ly/errors.json
- Signing test vectors: https://developers.anis.ly/vectors/index.json

## 1. Use an SDK

- .NET: use the `Anis.Partners.Sdk` package. It signs every request, verifies every answer and handles order
  recovery. Do not write signing code in .NET. https://developers.anis.ly/sdks/dotnet.md
- Other languages have no SDK yet. Hand-written code must follow the signing and verifying pages exactly and pass
  every test vector before it calls Anis (section 4).

## 2. Orders — where money moves

https://developers.anis.ly/docs/orders-and-recovery.md

1. **The operation id is chosen by the partner's code, once per purchase.** Generate a new UUID when the business
   decides to buy, and store it with the exact order body BEFORE sending. Send it as `Idempotency-Key`. Never
   generate it inside the call, a retry loop or a resume.
2. **When the outcome is unknown, resume: send the same order again — same operation id, exactly the stored body,
   freshly signed.** Unknown means: a timeout, a dropped connection, an answer that fails verification, a `202`, or
   a refusal that may still complete (`dependency_unavailable`, `request_timeout`, `internal_error`,
   `replay_detected`). A new operation id in these cases can buy the cards a second time.
3. **`202` is not a failure.** Send the same order again after `Retry-After`. Reading the order reports where it
   stands; it does not move it forward.
4. **Store card codes from the first completion answer before anything else** — a `201`, or a `200` on a resume.
   Later repeats (marked `Idempotency-Replayed: true`) and order reads never carry the codes. If that first answer
   was lost, the only way to the codes is the invoice reveal with the order's invoice id, which needs the
   `cards:reveal` permission — ask Anis for it before going live.
5. **On a resume, a refusal NOT marked `Idempotency-Replayed` says nothing about the earlier attempt** (it was
   decided before Anis looked at the order). If that attempt's outcome was unknown, it still is: keep resuming.
6. **A refusal marked `Idempotency-Replayed: true` is the recorded answer of a closed order**: nothing was bought,
   and the same id returns the same answer forever. A new attempt needs a new id.
7. **A definite refusal bought nothing** (`price_changed`, `insufficient_balance`, `quantity_unavailable`, …): fix
   the cause, then place a new order under a new operation id. `errors.json` gives `orderPlaced` for every code.
8. **`recoveryExhausted` is not failed.** Keep resuming the same id slowly (minutes, not seconds) and tell Anis the
   operation id. A timeout is never "failed".
9. **The price:** send the card's `unitPrice` from the catalogue, exactly as read, as `expectedUnitPrice`.
   `expectedTotal` is `expectedUnitPrice` × `quantity` exactly. Amounts are decimal strings with three decimals
   (`"10.500"`) — use a decimal type, never floating point. `quantity` is at least 1, at most the per-order
   maximum (100 by default), and within the card's own minimum and maximum.
   `externalReference` is optional: 1 to 100 characters of Latin letters, digits, space and `- _ . : / #`.
   `useAllowedDebt` is `true` only when the business means to spend allowed debt.
10. **Spending limits** (`owner_limit_exceeded`, `daily_limit_exceeded`) do not reset by waiting: never retry them
    in a loop.

## 3. Refusals

https://developers.anis.ly/docs/errors-and-limits.md

- Branch on `code`. Never on `title` or `detail` — they are for people and change with `Accept-Language`.
- Honour `Retry-After`. A `rate_limited` call recorded nothing: send it again after the wait (an order keeps its id).
- `invalid_credentials` covers every signing failure on purpose (wrong base, stale time, digest mismatch, inactive
  key). `insufficient_scope` also means a call from outside the allowed networks. `validation_failed` never names
  the field.
- Quote `requestId` (also the `X-Request-Id` header) when asking Anis about a call.

## 4. Signing requests (only without an SDK)

https://developers.anis.ly/docs/signing-requests.md — test with https://developers.anis.ly/docs/test-vectors.md

- HTTP Message Signatures (RFC 9421), ECDSA P-256 with SHA-256. Label `sig1` on both `Signature-Input` and
  `Signature`; `alg="ecdsa-p256-sha256"`; `keyid` is the enrolled key id.
- Signed parts, in exactly this order:
  - read (every `GET`): `@method` `@authority` `@path` `@query` `x-anis-date`
  - change (the two reveals, the signature self-check): `@method` `@authority` `@path` `@query` `content-digest`
    `nonce` `x-anis-date`
  - order: `@method` `@authority` `@path` `@query` `content-digest` `nonce` `idempotency-key` `x-anis-date`
- On changes and orders, `Signature-Input` ends with `;nonce="<the Nonce header>"`; on a read it must be absent.
- No query signs `?`. The query is signed exactly as sent — never re-ordered or re-encoded; a paging cursor goes
  back exactly as received. The host is lower-case.
- `Content-Digest` is over the exact bytes sent. A reveal sends NO body (digest of zero bytes:
  `sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:`); the signature self-check sends exactly `{}`.
- The signature is the 64-byte form (r then s), never DER. PHP's `openssl_sign` and Python's `cryptography` return
  DER and must be converted.
- Sign every attempt afresh, with a new nonce — a resume too. Signatures live 60 seconds or less. Keep the server
  clock synchronised: Anis accepts `created` at most about 30 seconds ahead of its clock or 5 minutes behind.
- When a signature will not verify, call the signature self-check route
  (https://developers.anis.ly/reference/check-signature.md) and compare line by line.

## 5. Verifying answers (only without an SDK)

https://developers.anis.ly/docs/verifying-answers.md

- Every answer, success or refusal, is signed. Throw away any answer that cannot be verified — never act on it,
  never log it and carry on. An unverifiable order answer means the outcome is unknown: resume (section 2).
- Check in this order: both headers present, label `sig1`, the right `alg`; the body matches `Content-Digest`
  BEFORE the signature; rebuild the list of signed parts from what the answer carries and require
  `Signature-Input` to name exactly that list; `created` within 60 seconds of the local clock; `keyid` names a
  published key; the 64-byte signature verifies over the rebuilt base.
- Anis's keys: `GET /.well-known/partner-signing-keys.json`. Cache it; refetch ONCE when an answer names an unknown
  `kid`. Refuse the whole document if any key carries `d`, and any key whose coordinates are not 32 bytes.
- A `503` without a signature is unverifiable like any other: retry a read, resume an order.

## 6. Enrolling a key

https://developers.anis.ly/docs/enrol-a-key.md

- Save the private key where secrets are kept BEFORE submitting the public half. An invitation takes exactly one
  key; losing the private half after submitting spends the invitation.
- Submit the public members only (no `d`). Keep the returned `challenge` until the proof is accepted.
- The proof signs five values joined by `\n`, none after the last: `anis.partners.v2.credential-proof`, the key id,
  the challenge generation in decimal, the lower-case hex SHA-256 of the challenge's UTF-8 bytes, the thumbprint.
  64-byte signature, base64url without padding.
- A proof that does not verify is answered `200` with `proofState: "failed"`, not a refusal — check `proofState`.
  Prove within about 30 minutes of submitting the key. After five failed proofs, proofs are refused as
  `rate_limited` until Anis staff restart the enrolment.
- The key's fingerprint goes to Anis staff through the agreed channel, not the API. The key signs once staff confirm it.

## 7. Secrets

https://developers.anis.ly/docs/security.md

- Never write the private key, the enrolment token or card codes to logs, traces, error reports, settings files,
  environment variables or source control.
- Card codes appear in exactly two places: the first completion answer of an order, and a reveal. Store them as
  secrets.
- Each Anis deployment is fully separate — its own address, keys and data. The address Anis gives you is the only
  setting.

## 8. Everything else

- Permissions are re-evaluated on every call: read the profile rather than caching them.
- Paging: pass `nextCursor` back exactly as received; leave it out for the first page.
- Every call and field: https://developers.anis.ly/reference.md
