# Anis Partner API > Integrate with Anis: guides, API reference and error pages for the Anis Partner API. Before writing code against this API, read the rules for AI assistants: https://developers.anis.ly/agents/anis-partners/SKILL.md — they hold what costs money when broken, above all how to retry an order without buying twice. Use an Anis SDK where one exists (.NET today). Signing by hand must follow https://developers.anis.ly/docs/signing-requests.md byte for byte and pass the published test vectors. Every page here also exists as Markdown: add `.md` to its address (`/docs/overview/` → `/docs/overview.md`). All of it in one file: https://developers.anis.ly/llms-full.txt. The site also has Arabic pages for people (under /ar); every file for AI models, this one included, is in English. --- --- title: "Overview — Anis Developers" description: "How the Anis Partner API works — your application, its keys, signed calls, verified answers and safe orders." url: https://developers.anis.ly/docs/overview/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Overview The Anis Partner API lets your own system buy Anis cards and hand them to your customers: read your wallets and the catalogue, place orders, and read the card codes you bought. This page explains how it fits together before the guides go into detail. ## Who is who - **Your application** — what Anis staff create for you. It has **permissions** (which calls it may make), the **networks** it may call from, optional **limits**, and the **wallets** it may use. - **Your Anis business account** — the account the wallets belong to. Purchases are paid from its wallets. - **Your key** — a key pair you create yourself. The private half never leaves your side; Anis only ever sees the public half. An application can have its key replaced without changing anything else. ## Every call is signed, every answer is verified There is no API key to send. Instead: 1. **You sign every request** with your private key. Anis checks the signature, and refuses anything it cannot verify, before it looks at what you asked for. 2. **Anis signs every answer** with its own key. Your client checks that signature and throws the answer away if it does not verify — so a forged or altered answer is never acted on. The signing is exact to the byte, which is why we provide [SDKs](https://developers.anis.ly/sdks.md) that do it for you. If you write your own client, the [signing](https://developers.anis.ly/docs/signing-requests.md) and [verifying](https://developers.anis.ly/docs/verifying-answers.md) guides give every rule. ## What you can do | Area | Calls | | --- | --- | | Your application | Read your profile and current permissions | | Wallets | List your wallets and their balances | | Catalogue | Browse categories, subcategories and the cards a wallet can buy, priced for that wallet | | Orders | Place an order, resume one whose outcome you did not receive, read where an order stands | | Owned cards | List bought cards, and reveal the codes of one card or a whole invoice | | Diagnostics | Check your signature: Anis reports exactly what it saw | The [API reference](https://developers.anis.ly/reference.md) lists every call with its permission, fields and refusals. ## Orders cannot be bought twice by accident You choose an **operation id** for each order and keep it. Sending the same id again never buys twice: it resumes the same order and returns its real outcome. That is what makes it safe to retry after a timeout. See [Orders and recovery](https://developers.anis.ly/docs/orders-and-recovery.md). ## One address, nothing else to configure Anis gives you the address to call. Each Anis deployment is a fully separate copy — its own address, keys and data — so there is no “environment” setting anywhere: which one you talk to is decided by the address alone, and a key enrolled with one means nothing to another. > **Getting access** > > Anis staff create your application, set its permissions, networks and limits, and send you an invitation to enrol your first key. Start with [Enrol a key](https://developers.anis.ly/docs/enrol-a-key.md). --- --- title: "Enrol a key — Anis Developers" description: "Turn an Anis invitation into an active signing key — the steps, the proof, the time limits and what to do when something goes wrong." url: https://developers.anis.ly/docs/enrol-a-key/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Enrol a key You cannot call the API until a key of yours is active. Enrolment turns an invitation from Anis into that key. ## The steps 1. **Anis staff send you an invitation** — an invitation id and a single-use enrolment token. 2. **You create a P-256 key pair.** Save the private half first, where you keep secrets. 3. **You submit the public half** and receive a challenge. 4. **You prove you hold the private half** by signing a message built from that challenge. 5. **You give Anis staff the key’s fingerprint** through the channel you agreed with them — not through the API. They record it and confirm the key. From that moment it signs requests. The four enrolment calls are authorised with the token — `Authorization: Enrollment ` — not with a signature. Their answers are still signed by Anis, and your client should verify them like any other. > **Save the private key before you submit it** > > An invitation takes exactly one key. If you submitted the public half and then lost the private half, the invitation is spent on a key nobody holds, and Anis staff must restart the enrolment. ## With the .NET SDK ```csharp using var key = ECDsa.Create(ECCurve.NamedCurves.nistP256); await File.WriteAllTextAsync("/secure/partner-key.pem", key.ExportPkcs8PrivateKeyPem(), ct); // first using var enrollment = AnisEnrollmentClient.Create(authority, invitationId, enrollmentToken); var submitted = await enrollment.SubmitKeyAsync(new EnrollmentKeyRequest { PublicJwk = AnisEnrollmentClient.PublicJwkOf(key), NotBefore = DateTimeOffset.UtcNow, ExpiresAt = DateTimeOffset.UtcNow.AddYears(1), Cidrs = ["203.0.113.0/24"], // the networks you will call from — a proposal }, ct); var status = await enrollment.ProveAsync(submitted, key, ct); if (status.ProofState != "accepted") throw new InvalidOperationException("The proof failed — check the key and prove again."); // state: pendingApproval Console.WriteLine($"key id {submitted.KeyId}, fingerprint {submitted.Thumbprint}"); ``` `submitted.KeyId` is the key id every signature will carry. `submitted.Thumbprint` is the fingerprint Anis staff record. ## Without an SDK **Submit the key** — [`POST /v1/enrollments/{invitationId}/keys`](https://developers.anis.ly/reference/submit-key.md): ```json { "publicJwk": { "kty": "EC", "crv": "P-256", "x": "<32 bytes, base64url>", "y": "<32 bytes, base64url>" }, "notBefore": "2026-09-24T08:00:00Z", "expiresAt": "2027-09-24T08:00:00Z", "cidrs": ["203.0.113.0/24"] } ``` Send the public members only — a `d` is refused. Only the **length** of the window is used — kept between 1 day and 2 years by default — and it starts when Anis staff activate the key. The answer carries `keyId`, `thumbprint`, `challenge` and `challengeGeneration`. **Keep the challenge** until the proof is accepted: it is returned only here. **Build the proof message.** Not the bare challenge — five values, joined by a single `\n`, with none after the last: ```text anis.partners.v2.credential-proof ``` Sign those bytes with ECDSA P-256 and SHA-256. The signature must be the **64-byte** form (`r` then `s`, 32 bytes each), not DER, encoded as base64url without padding. **Submit the proof** — [`POST /v1/enrollments/{invitationId}/proof`](https://developers.anis.ly/reference/submit-proof.md): ```json { "keyId": "", "challengeGeneration": 1, "signature": "<86 base64url characters>" } ``` **Check the answer’s `proofState`.** A proof that does not verify is not refused: the answer says `"failed"`, and you may try again. Only `"accepted"` moves the key on. After five failed proofs, further proofs are refused as [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) until Anis staff restart the enrolment. ## Where the key stands [Read the enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) with the same token: | `state` | Meaning | | --- | --- | | `pendingProof` | Submitted; possession not proved yet | | `pendingApproval` | Proved; waiting for Anis staff to record the fingerprint and confirm the key | | `active` | Confirmed — the key signs requests | | `unavailable` | The key left enrolment (revoked, expired or replaced); ask for a new invitation | ## Time limits - **The invitation** is valid for about a day by default. [Read the invitation](https://developers.anis.ly/reference/read-invitation.md) for its exact `expiresAt`. - **The proof** must follow the key submission within about 30 minutes by default; a later proof comes back `"failed"`. If your key lives in a vault or hardware module whose signing needs an approval, arrange that approval **before** you submit the key. ## When something goes wrong | What happened | What to do | | --- | --- | | The invitation is refused as [`invitation_invalid`](https://developers.anis.ly/errors/invitation-invalid.md) | It is unknown, used or expired: ask Anis staff for a new one | | The key is refused as [`key_proof_invalid`](https://developers.anis.ly/errors/key-proof-invalid.md) | The public key is not a usable P-256 key: check `kty`, `crv`, and 32-byte `x` and `y` | | The proof comes back `proofState: "failed"` | Check the proof message, the 64-byte signature form and the challenge generation, and prove again — within about 30 minutes of the key submission | | Proofs are refused as [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) | Five proofs failed: ask Anis staff to restart the enrolment | | The proof is refused as [`challenge_expired`](https://developers.anis.ly/errors/challenge-expired.md) | Anis staff restarted the enrolment: enrol again with the new invitation | | The key submission got no answer | Submit the **same** key once more. If that is refused as [`key_duplicate`](https://developers.anis.ly/errors/key-duplicate.md), the first one was taken — ask Anis staff to restart the enrolment | ## Replacing and revoking keys - **Replacing a key.** Anis staff start the replacement and send a new invitation. Enrol the new key exactly as above. Once it is confirmed, both keys work for an overlap period Anis sets — switch your signer to the new key id within it. - **Revoking a key.** From the moment Anis staff revoke a key, every call signed with it is refused with [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md). Revoking a key you no longer use does not affect your current one. --- --- title: "Your first call — Anis Developers" description: "Register the client, read your profile, and check your signature when something does not verify." url: https://developers.anis.ly/docs/first-call/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Your first call Once your key is active, read your profile. It is the simplest signed call, and it tells you what your application may do. ## With the .NET SDK ```csharp builder.Services .AddAnisPartners(options => options.Authority = new Uri("https://")) .WithSigner(EcdsaP256Signer.FromPemFile("/secure/partner-key.pem", keyId)); // later, from the container: var profile = await anis.Profile.GetAsync(ct); Console.WriteLine(string.Join(", ", profile.Application!.Scopes)); ``` See the [.NET SDK](https://developers.anis.ly/sdks/dotnet.md) page for settings files, key custody and the rest of the client. ## What comes back [Read your profile](https://developers.anis.ly/reference/read-profile.md) returns your partner and application ids, the Anis business account you act for, and **the permissions your application has right now**. Permissions come from your application’s live policy and are checked on every call — a change by Anis staff applies to your next request, not your next restart — so read them rather than caching them. ## When a call is refused Every refusal carries a `code` and a link to its page in [Error codes](https://developers.anis.ly/errors.md). The first ones you may meet: | Code | Usually means | | --- | --- | | [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) | The key id, the key, the signature or the clock is wrong — or the key is not active yet | | [`insufficient_scope`](https://developers.anis.ly/errors/insufficient-scope.md) | A missing permission, or a call from outside your allowed networks | ## Check your signature If a signature will not verify, ask Anis what it saw: [Check your signature](https://developers.anis.ly/reference/check-signature.md) runs your call through all of Anis’s checks, changes nothing, and reports the method, address, path and query it signed over, the key it found and the permissions in force. It needs the `diagnostics:use` permission. ```csharp var seen = await anis.Diagnostics.CheckSignatureAsync(ct); Console.WriteLine($"{seen.Method} {seen.Authority}{seen.Path} ?{seen.CanonicalQuery} key {seen.KeyId}"); ``` Compare each value with what you signed — it is almost always the address or the query. > **If the self-check is refused too** > > Then the key itself is the problem: a key id that does not match the key file, a key not yet confirmed or already revoked, or a server clock that is not synchronised. --- --- title: "Build with an AI assistant — Anis Developers" description: "Give your AI coding assistant the files it needs, so the code it writes follows the rules that protect your money." url: https://developers.anis.ly/docs/ai-assistants/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Build with an AI assistant AI coding assistants write much of today’s integration code. They are confident — and on this API a confident mistake can buy cards twice, or lose card codes that were paid for. Everything on this site is also published in forms an assistant reads directly. Give it these before it writes code. Every file for AI models is in English, whatever the language of the page you are reading — models work best in it, and one version cannot drift from another. Your assistant still answers in the language you ask in. ## Give your assistant the rules [The rules for AI assistants](https://developers.anis.ly/agents/anis-partners/SKILL.md) hold, on one page, what an assistant must never get wrong: how to retry an order, where card codes appear, how to sign and verify, how to enrol. Each rule links to the page that explains it. Add the file to your assistant: | Assistant | Where the file goes | | --- | --- | | Claude Code | `.claude/skills/anis-partners/SKILL.md` in your project | | Cursor | a rule in `.cursor/rules/` | | GitHub Copilot | `.github/copilot-instructions.md` | | Others | `AGENTS.md` at the root of your project, or paste it at the start of the conversation | ```bash mkdir -p .claude/skills/anis-partners curl -o .claude/skills/anis-partners/SKILL.md https://developers.anis.ly/agents/anis-partners/SKILL.md ``` ## Point it at the documentation - [`/llms.txt`](https://developers.anis.ly/llms.txt) — an index of every page, with each page’s Markdown address. Paste this address into your assistant and it can find the rest. - **Every page as Markdown:** add `.md` to its address — `/docs/orders-and-recovery.md`. - [`/llms-full.txt`](https://developers.anis.ly/llms-full.txt) — the whole site in one file. - On every page, **Copy for AI** copies the page as Markdown, and **Open in Claude** or **Open in ChatGPT** starts a conversation about it. ## Give tools the data | File | What it holds | | --- | --- | | [`/openapi.json`](https://developers.anis.ly/openapi.json) | Every call, field and answer, for generating request and response types. It cannot describe signing — generate the types from it, never the signing. | | [`/errors.json`](https://developers.anis.ly/errors.json) | Every error code: its status, whether to send it again unchanged, whether an order was placed, and what to do. | | [`/vectors/index.json`](https://developers.anis.ly/vectors/index.json) | The [signing test vectors](https://developers.anis.ly/docs/test-vectors.md): exact requests, answers and signatures. | ## Check what it wrote > **Review the order code yourself** > > Before anything goes live, check three things by hand. The operation id is created once per purchase and stored before the call — never inside a retry. A timeout, an answer that fails verification, or a refusal that may still complete resumes the order with the **same** id. Card codes are stored before anything else, and never logged. If the assistant wrote the signing code, it is not finished until every [test vector](https://developers.anis.ly/docs/test-vectors.md) passes and [Check your signature](https://developers.anis.ly/reference/check-signature.md) accepts a call made with your real key. --- --- 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=::` | | `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=::` | ## 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=""` 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. --- --- title: "Verifying answers — Anis Developers" description: "How a client checks that an answer really came from Anis — and why it must throw away any answer it cannot check." url: https://developers.anis.ly/docs/verifying-answers/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Verifying answers > **Using an SDK?** > > The SDKs verify every answer before you see it, and throw away any that fail. This page is for writing your own client. Every answer Anis sends — success or refusal — is signed. **A client throws away any answer it cannot verify**: it does not log it and carry on, it does not act on it. An order answer that cannot be verified is treated like one that never arrived — the outcome is unknown and the order is resumed with the same operation id. ## What Anis signs Answers are signed with **ECDSA P-256 and SHA-256**, label `sig1`, with parameters `created`, `keyid` and `alg` — and no `expires`. The signed parts, in this order: | # | Part | Present when | | --- | --- | --- | | 1 | `"@status"` | always | | 2 | `"content-digest"` | always | | 3 | `"x-request-id"` | always | | 4 | `"signature-input";req` | you signed the request (absent on enrolment answers) — its value is **your own** `Signature-Input` | | 5 | `"location"` | the answer carries `Location` | | 6 | `"retry-after"` | the answer carries `Retry-After` | | 7 | `"idempotency-replayed"` | the answer carries `Idempotency-Replayed: true` | | 8 | `"cache-control"` | the answer carries `Cache-Control: no-store` | The base is built like a request’s: one line per part, then `"@signature-params"` with no newline after it. ```text "@status": 200 "content-digest": sha-256=:epGQsVay09pY/GXv6MpAjMbsZ7VvobF1nAdylVo/mMk=: "x-request-id": 01J9R2K8T4V6XQ0M3B7C5D9E1F "signature-input";req: sig1=("@method" "@authority" "@path" "@query" "x-anis-date");created=1789804800;expires=1789804860;keyid="…";alg="ecdsa-p256-sha256" "@signature-params": ("@status" "content-digest" "x-request-id" "signature-input";req);created=1789804800;keyid="partner-response-signing/v1-active";alg="ecdsa-p256-sha256" ``` `;req` sits **outside** the quotes, identically in its line and in `@signature-params`. ## The checks, in order 1. Both `Signature` and `Signature-Input` are present; the label is `sig1`; `alg` is `ecdsa-p256-sha256`. 2. **The body matches its `Content-Digest`** — checked before the signature. The signature covers the digest header, not the body, so a client that checks the signature first accepts a swapped body whenever the digest was rewritten too. 3. **Rebuild the list of signed parts from what the answer carries** (the table above) and require the answer’s `Signature-Input` to name exactly that list. Never trust the list it advertises: an answer validly signed over a shorter list leaves its body unprotected. 4. `created` is within **60 seconds** of your clock — an answer has no `expires`, so without this bound a captured answer could be replayed to you forever. 5. The `keyid` names a key in Anis’s published keys (below). 6. The signature is 64 bytes and verifies over the rebuilt base. Anything else: throw the answer away. The [answer test vectors](https://developers.anis.ly/docs/test-vectors.md#testing-answer-verification) cover every one of these checks. ## Anis’s public keys [`GET /.well-known/partner-signing-keys.json`](https://developers.anis.ly/reference/signing-keys.md) — the one route that needs no signature — publishes every key that may sign an answer, including the next one before a rotation and the previous one after it. Each has a `kid`, which is what an answer’s `keyid` names. Treat `kid` as an opaque name. - **Cache the document.** When an answer names a `kid` you do not have, fetch it again **once** — that is the ordinary sign of a rotation. Do not refetch without limit on unknown keys. - **Refuse the whole document if any key carries a private member (`d`)** — not just that key. A document that leaks a private key is not to be trusted at all. - **Each coordinate is exactly 32 bytes.** Refuse a key of any other shape. ## The one unsigned answer If Anis’s own signing is unavailable, it answers `503` without a signature. Treat it like any answer you cannot verify: retry a read, resume an order with the same operation id. --- --- title: "Signing test vectors — Anis Developers" description: "Exact requests, answers and signatures to test a client you write yourself — before it ever calls Anis." url: https://developers.anis.ly/docs/test-vectors/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Signing test vectors A signature is right to the byte or it is refused, and a refusal never says which byte. These vectors let you find the byte yourself: each one is a real request, answer or enrolment proof with the exact text that was signed and the exact result expected. > **Using the .NET SDK?** > > It already passes every vector. This page is for a client written by hand — or by an AI assistant. > **Test keys only** > > The keys in these files are test keys — never Anis’s or any partner’s real keys — and `partners.anis.ly` is an example address. Use them to test, never to enrol. ## What is here | Set | What it tests | Vectors | | --- | --- | --- | | Requests | Your signature base, byte for byte, for every kind of signed call | 9 | | Answers | Your answer checks: which answers to accept and which to throw away | 39 | | Enrolment | Your proof-of-possession message | 2 | [`/vectors/index.json`](https://developers.anis.ly/vectors/index.json) lists every vector with its title, its file and — for answers — whether it must be accepted or rejected. The same index names the signed parts of each kind of call. ## Testing request signing Each request vector carries the request (`request`), the signature’s times (`signature.created`, `signature.expires`), the test key (`key`) and what must come out (`expected`). 1. Build the signature base from `request.method`, `request.authority`, `request.path`, `request.canonicalQuery` (the query without its `?`), `request.anisDate`, `request.nonce`, `request.idempotencyKey` and the body in `request.bodyBase64`, with `key.keyId` as the key id. 2. Compare your `Content-Digest` with `expected.contentDigest`, your `Signature-Input` with `expected.signatureInput`, and your base with `expected.signatureBaseUtf8` — **byte for byte**. 3. Sign your base with the test private key (`key.privateKeyPkcs8Base64`) and check that `key.publicJwk` verifies it. An ECDSA signature is different every time, so compare the base, never the signature bytes. `RQ-03` gives the host as a caller might type it (`request.authorityAsGiven`) and as it must be signed (`request.authority`, lower-case). `RQ-06` is a reveal: no body at all, so the digest of zero bytes. ## Testing answer verification Each answer vector carries the answer (`response.status`, `response.headers`, `response.bodyBase64`), the `Signature-Input` of the request it answers (`request.signatureInput`), Anis’s published keys at that moment (`signingKeys`) and the time to use as your clock (`verifyAt`, Unix seconds — use it, not the real clock). Your verifier must reach `expected.outcome`: `accept` or `reject`. For a rejection, `expected.reason` says why; your own error names may differ, so match the outcome and use the reason to understand a mismatch. `signatureBaseForDebugging` is the base Anis signed. Most answers come in families: one that must be accepted (`RS-001-…`) and changed copies of it (`RS-001-…-M01` onward) that must each be rejected — one body byte changed, a rewritten digest, a DER signature, a signed part missing, a time out of the window. Others test the published keys: an answer signed by the next or the retiring key must be accepted, and a key document carrying a private member must be refused whole. ## Testing the enrolment proof Each enrolment vector gives what the key submission returned (`keySubmissionResult`) and the test key (`key`). 1. Build the proof message from `keySubmissionResult` and compare it with `expected.proofMessageUtf8` — and the challenge’s hash with `expected.challengeHashHex`. 2. Sign it with the test key; `exampleSignature` is one valid signature, and `key.publicJwk` verifies both. 3. `expected.proofRequest` is the body to send. ## After the vectors pass Call [Check your signature](https://developers.anis.ly/reference/check-signature.md) with your real key. It runs every check Anis runs on a signed call, reports what Anis saw, and changes nothing. --- --- title: "Orders and recovery — Anis Developers" description: "Placing an order safely, the answers it can get, and how to recover when you do not know whether it went through." url: https://developers.anis.ly/docs/orders-and-recovery/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Orders and recovery The part where money moves. Read it once, properly. ## The operation id is yours Every order carries an **operation id** that **you** choose — a new UUID for each purchase — sent as the `Idempotency-Key` header. Store it, with the exact order, **before** you send it. The same id sent again never buys twice: it resumes the same order and returns its real outcome. That is what makes a retry safe — even after your process restarts — and it is why the SDKs never invent the id for you: an id made up on each attempt would turn a lost answer plus a retry into a second purchase. ```csharp var operationId = Guid.NewGuid(); await db.RecordOrderIntentAsync(operationId, walletId, request, ct); // BEFORE the call var outcome = await anis.Orders.CreateAsync(walletId, operationId, request, ct); ``` An operation id is unique across all of Anis. Sending one id with a different order body is refused as [`idempotency_conflict`](https://developers.anis.ly/errors/idempotency-conflict.md) — which is also what a resume gets if its body drifted from the stored one. Resume with exactly the body you stored. ## The price to send Send the card’s **`unitPrice`** from the catalogue, exactly as read, as `expectedUnitPrice` — it is the price this wallet pays and the one the order is checked against. The other prices (`businessPrice`, `personalPrice`, `specialOfferPrice`) are for display; an order at one of them can be refused as [`price_changed`](https://developers.anis.ly/errors/price-changed.md). `expectedTotal` must be `expectedUnitPrice` times `quantity` **exactly**. Amounts are decimal strings with three decimals (`"10.500"`) — compute them with a decimal type, never floating point. ```json { "cardId": "8d4b1e73-9a25-4c60-8f37-6b2e9d5a1c48", "quantity": 2, "expectedUnitPrice": { "amount": "10.500", "currency": "LYD" }, "expectedTotal": { "amount": "21.000", "currency": "LYD" }, "externalReference": "your-order-123" } ``` `externalReference` is optional — your own reference, 1 to 100 characters of Latin letters and digits, space and `- _ . : / #`. `quantity` is at least 1 and at most the per-order maximum (100 by default). `useAllowedDebt` defaults to `false`; set it to `true` only if you mean to spend the account’s allowed debt. ## The answers an order can get | Answer | What it means | What to do | | --- | --- | --- | | `201` with card codes | The order completed | **Store the codes before anything else** | | `200` with card codes | A resume got the completion whose first answer was lost | Store the codes — this is the first time you see them | | `201` or `200` with `Idempotency-Replayed: true` | This order’s outcome was already sent | If you stored the codes from that first answer, nothing to do. If you never received it — a timeout — the codes were in it: reveal them with the order’s invoice id | | `202` | Accepted, no outcome yet | Send the **same** order again after `Retry-After` | | A refusal | See below | Depends on whether it was placed | **Only the answer that first reports a completion carries the card codes.** Any later repeat carries the order without them, and [reading the order](https://developers.anis.ly/reference/read-order.md) never carries them. So if that first answer is lost — a timeout, a dropped connection, an answer that could not be verified — the order is paid for and its codes are only available by a reveal with the order’s invoice id, which needs the `cards:reveal` permission. > **Ask for cards:reveal before you go live** > > Without it, an order whose first answer was lost cannot give you its codes again. ## Recovery is sending the same order again A `202` carries a `Location` pointing at the order, so the instinct is to poll it. **Reading an order reports where it stands; it does not move it forward.** The way to recover an order is to send **exactly the same order again — same operation id, same body**, freshly signed. Do the same whenever you do not know what happened: - your call **timed out**, or the connection dropped; - the answer **could not be verified**; - the refusal says the order **may still complete** (below). A resume is always safe. A new operation id in these cases is not — it can buy the cards a second time. ```csharp // resuming: true when this call repeats an attempt whose outcome you did not learn try { var outcome = resuming ? await anis.Orders.ResumeAsync(walletId, operationId, request, ct) : await anis.Orders.CreateAsync(walletId, operationId, request, ct); } catch (AnisApiException failure) when (failure.OrderOutcome == OrderRefusalOutcome.Unknown || (resuming && !failure.IsReplayed)) { await ResumeLaterAsync(operationId); // no decision reached — or a resume refused at the door } catch (AnisApiException failure) { await CloseAsNotPlacedAsync(operationId, failure); // nothing bought: fix the cause, new order, new id } catch (Exception e) when (e is TaskCanceledException or HttpRequestException or UnverifiableResponseException) { await ResumeLaterAsync(operationId); // timed out, connection lost, or an untrusted answer } ``` > **Recovery exhausted** > > An order whose status is `recoveryExhausted` is **not** failed — Anis could not learn its outcome yet, and it may have completed. Do not place it again under a new id. Keep resuming the same id, slowly (minutes, not seconds) — a later resume can still return the completion with its codes — and tell Anis the operation id. ## When an order is refused: was it placed? Every [error page](https://developers.anis.ly/errors.md) answers that first. - **This request bought nothing.** Fix the cause, then place a new order under a **new** operation id. (A refusal at the door — a rate limit, a missing permission — records nothing against the id, so reusing it also works.) - **But on a resume**, a refusal that is not marked `Idempotency-Replayed` was decided before Anis looked at the order, so it says nothing about the earlier attempt: if that attempt’s outcome was unknown, it still is — keep resuming with the same id. - **May still complete.** No decision was reached: [`dependency_unavailable`](https://developers.anis.ly/errors/dependency-unavailable.md), [`request_timeout`](https://developers.anis.ly/errors/request-timeout.md), [`internal_error`](https://developers.anis.ly/errors/internal-error.md), or [`replay_detected`](https://developers.anis.ly/errors/replay-detected.md) (an identical copy of your request got there first). Resume with the **same** operation id. A refusal marked **`Idempotency-Replayed: true`** is the recorded answer of an order that is already **closed** — nothing was bought, and sending the same id again returns the same answer forever. Close it on your side; a new attempt needs a new id. | Refusal | Placed by this request? | What to do | | --- | --- | --- | | [`price_changed`](https://developers.anis.ly/errors/price-changed.md) | no | Re-read the catalogue; new order at the new `unitPrice` | | [`insufficient_balance`](https://developers.anis.ly/errors/insufficient-balance.md) | no | Top up; new order | | [`quantity_unavailable`](https://developers.anis.ly/errors/quantity-unavailable.md), [`card_unavailable`](https://developers.anis.ly/errors/card-unavailable.md) | no | Re-read the catalogue; new order | | [`allowed_debt_consent_required`](https://developers.anis.ly/errors/allowed-debt-consent-required.md) | no | New order with `useAllowedDebt: true`, only if you mean it | | [`owner_limit_exceeded`](https://developers.anis.ly/errors/owner-limit-exceeded.md), [`daily_limit_exceeded`](https://developers.anis.ly/errors/daily-limit-exceeded.md) | no | Do not retry in a loop; new order once the limit changes | | [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) | no | Wait for `Retry-After`, then send again | | [`idempotency_conflict`](https://developers.anis.ly/errors/idempotency-conflict.md) | no | If you meant to resume, send the exact stored body; for a new purchase, use a new id | | [`dependency_unavailable`](https://developers.anis.ly/errors/dependency-unavailable.md), [`request_timeout`](https://developers.anis.ly/errors/request-timeout.md), [`internal_error`](https://developers.anis.ly/errors/internal-error.md), [`replay_detected`](https://developers.anis.ly/errors/replay-detected.md) | **may still complete** | Resume with the **same** id | A timeout is never `failed`. Only a definite business refusal is `failed`, and a completed order never becomes failed afterwards. --- --- title: "Errors and limits — Anis Developers" description: "What every refusal carries, how to branch on it, and the limits that can apply to your application." url: https://developers.anis.ly/docs/errors-and-limits/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Errors and limits ## What a refusal carries Every refusal is a signed JSON problem: ```json { "type": "https://developers.anis.ly/errors/price-changed", "title": "Price changed", "status": 409, "code": "price_changed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` - **Branch on `code`.** Never on `title` or `detail`: they are written for people, follow the language you ask for (`Accept-Language: ar` or `en`) and can change wording. - **`type`** is the address of the code’s page — every code has one in [Error codes](https://developers.anis.ly/errors.md), saying what it means, whether an order was bought and what to do. - **`requestId`** is what to quote when you ask Anis about a call. It is also in the `X-Request-Id` header of every answer. ## Two things a refusal may carry - **`Retry-After`** — when Anis knows how long to wait, in seconds. Honour it. - **`Idempotency-Replayed: true`** — on an order: the recorded answer of an order that is already closed. ## Refusals that hide which rule failed Some refusals give the same answer for different causes, on purpose, so that a refusal never tells anyone which thing to work around: | Code | Covers | | --- | --- | | [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) | an unknown key, a key that is not active, a revoked key, a wrong signature, an expired signature, a body that does not match its digest | | [`insufficient_scope`](https://developers.anis.ly/errors/insufficient-scope.md) | a missing permission, or a call from outside your allowed networks | | [`wallet_not_granted`](https://developers.anis.ly/errors/wallet-not-granted.md), [`resource_not_found`](https://developers.anis.ly/errors/resource-not-found.md) | something that does not exist, or that is not yours | | [`validation_failed`](https://developers.anis.ly/errors/validation-failed.md) | any broken rule — the field is never named | ## Limits Anis staff can set limits on your application, for the whole application or per wallet. Each counts one kind of call in a time window: | Limit | Counts | | --- | --- | | All requests | every signed call | | Orders | placing an order (and resuming one) | | Reveals | both reveals | When a limit is reached the call is refused as [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md), with `Retry-After` when the reset time is known. Anis also protects itself with limits that give no reset time — back off exponentially then. A refused call records nothing: once the wait is over, send it again (an order keeps its operation id). The [Routes and permissions](https://developers.anis.ly/docs/routes-and-permissions.md) page shows which limits each call counts toward. > **Spending limits are different** > > [`owner_limit_exceeded`](https://developers.anis.ly/errors/owner-limit-exceeded.md) and [`daily_limit_exceeded`](https://developers.anis.ly/errors/daily-limit-exceeded.md) are limits on what the Anis business account may spend, not on how often you call. Waiting does not reset them — do not retry in a loop. --- --- title: "Routes and permissions — Anis Developers" description: "Every call, the permission it needs, how it is authenticated and which limits it counts toward." url: https://developers.anis.ly/docs/routes-and-permissions/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Routes and permissions Anis staff grant your application its permissions. [Read your profile](https://developers.anis.ly/reference/read-profile.md) to see the ones it has right now. ## Every call | Call | Permission | Authentication | Limits it counts toward | | --- | --- | --- | --- | | GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) | `profile:read` | Signed read | All requests | | GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) | `wallets:read` | Signed read | All requests | | GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) | `wallets:read` | Signed read | All requests | | GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) | `catalogue:read` | Signed read | All requests | | GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) | `catalogue:read` | Signed read | All requests | | GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) | `catalogue:read` | Signed read | All requests | | GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) | `catalogue:read` | Signed read | All requests | | POST [Place an order](https://developers.anis.ly/reference/create-order.md) | `orders:create` | Signed order | All requests, Orders | | GET [Read an order](https://developers.anis.ly/reference/read-order.md) | `orders:read` or `orders:create` for your own orders | Signed read | All requests | | GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) | `cards:read` | Signed read | All requests | | GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) | `cards:read` | Signed read | All requests | | POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) | `cards:reveal` | Signed change | All requests, Reveals | | POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) | `cards:reveal` | Signed change | All requests, Reveals | | POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) | `diagnostics:use` | Signed change | All requests | | GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) | None | Enrolment token | None | | POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) | None | Enrolment token | None | | POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) | None | Enrolment token | None | | GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) | None | Enrolment token | None | | GET [Anis’s public signing keys](https://developers.anis.ly/reference/signing-keys.md) | None | Public | None | The permissions are `profile:read`, `wallets:read`, `catalogue:read`, `orders:create`, `orders:read`, `cards:read`, `cards:reveal` and `diagnostics:use`. `orders:create` also lets you read your **own** orders. ## Before a call is looked at Anis checks each of these before any business decision, so an order refused by one of them was never placed. The order in which they run is not part of the contract — do not rely on meeting one before another. | Check | Refusal | | --- | --- | | The request’s shape and body, including a body where none is allowed | [`validation_failed`](https://developers.anis.ly/errors/validation-failed.md) | | The body matches its digest | [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) | | The key exists, is active and signed this request; the signature is fresh | [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) | | The nonce was not seen before | [`replay_detected`](https://developers.anis.ly/errors/replay-detected.md) | | A staff-set limit, or Anis’s own protection | [`rate_limited`](https://developers.anis.ly/errors/rate-limited.md) | | The call comes from one of your allowed networks | [`insufficient_scope`](https://developers.anis.ly/errors/insufficient-scope.md) | | Your application has the route’s permission | [`insufficient_scope`](https://developers.anis.ly/errors/insufficient-scope.md) | | The wallet in the address is granted to your application | [`wallet_not_granted`](https://developers.anis.ly/errors/wallet-not-granted.md) | ## Paging The list calls return a page and a `nextCursor`. Pass it back as `cursor` — exactly as received — to get the next page; there is no `nextCursor` on the last one. A changed or malformed cursor is refused as [`validation_failed`](https://developers.anis.ly/errors/validation-failed.md). --- --- title: "Security — Anis Developers" description: "Keeping your key safe, why every answer is verified, key replacement and revocation, card codes and your clock." url: https://developers.anis.ly/docs/security/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Security ## Your private key stays yours Anis never sees your private key — only its public half, at enrolment. Keep the private half where you keep your most sensitive secrets: a key vault, a hardware security module, or a file only your service can read. - **Never** put it in a settings file, an environment variable, source control or anywhere a log or crash report can reach. - Signing only needs something that can sign bytes with ECDSA P-256 and return the 64-byte signature. A vault or hardware module that signs on your behalf can do it without the key ever entering your application — the SDKs accept any such signer. ## Every answer is verified Anis signs every answer, and your client checks it before acting on it — see [Verifying answers](https://developers.anis.ly/docs/verifying-answers.md). An answer that fails the check is thrown away: never act on it, never log it and carry on. Enrolment answers are signed too. ## One address per deployment Each Anis deployment is fully separate — its own address, keys and data. There is no setting that switches between them; the address you call decides. A key enrolled with one deployment means nothing to another. ## Replacing and revoking your key - **Replacing:** Anis staff start the replacement and send you a new invitation; enrol the new key. Both keys work during the overlap Anis sets — move to the new key id within it. - **Revoking:** from the moment a key is revoked, every call signed with it is refused as [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md). To sign again, enrol a new key from a new invitation. - **If you think your key leaked,** tell Anis at once so staff can revoke it. ## Allowed networks Your application may only be called from the networks agreed with Anis. A call from anywhere else is refused as [`insufficient_scope`](https://developers.anis.ly/errors/insufficient-scope.md) — the same answer as a missing permission, on purpose. ## Card codes are secrets Card codes appear in exactly two places: the answer that first reports a completed order, and a reveal. Store them where you keep secrets, and never write them to logs, traces or error reports. ## Clock Signatures carry their time, and so do Anis’s answers. Keep your server clock synchronised (NTP): a clock that drifts gets requests refused as [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) and answers discarded as too old. Sign for 60 seconds or less — see [Signing requests](https://developers.anis.ly/docs/signing-requests.md#clock). > **Reporting a security issue** > > Write to [info@anis.ly](mailto:info@anis.ly) and quote the request ids involved. --- --- title: "Observability — Anis Developers" description: "What to record, what to alert on, and how to trace one purchase across your system and Anis." url: https://developers.anis.ly/docs/observability/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Observability ## Two ids tie everything together - **The request id** — every answer carries `X-Request-Id`, and every refusal repeats it as `requestId`. Log it with every call; it is what Anis needs to find your call. - **The operation id** — the id you chose for an order. Log it at every step, from storing the intent to storing the codes. It traces one purchase across your system and Anis. ## What to alert on | Signal | Why | | --- | --- | | **Orders whose outcome is unknown** — timeouts, lost connections, unverifiable answers, and refusals that may still complete | Each one must be resumed with the same operation id | | **Answers that could not be verified** | Something between you and Anis changed an answer, a clock is badly out, or an Anis key rotation was missed. One short burst of unsigned `503`s is Anis’s own signing being briefly unavailable | | **`invalid_credentials` on every call** | The key was revoked or replaced, or the clock drifted | | **`rate_limited`** | Your traffic is near a limit Anis staff set | ## Never record Your private key, signatures, signature bases, nonces, the enrolment token, or card codes (serials and vouchers) — anywhere: logs, traces, metrics or error reports. ## With the .NET SDK The SDK reports everything under one name, `Anis.Partners.Sdk`, for traces and for metrics, and logs through your own logging: ```csharp builder.Services.AddOpenTelemetry() .WithTracing(tracing => tracing.AddSource("Anis.Partners.Sdk")) .WithMetrics(metrics => metrics.AddMeter("Anis.Partners.Sdk")); ``` | Metric | What it tells you | | --- | --- | | `anis.partners.order.outcomes` | Orders by outcome: `completed`, `processing`, `replayed` — and `unknown`, the one to alert on | | `anis.partners.request.duration` | Every call, by route and status or refusal code | | `anis.partners.signature.duration` | Time spent signing — a slow vault shows here | | `anis.partners.response.verification.failures` | Answers thrown away, by the rule that failed | | `anis.partners.signing_keys.fetches` | Fetches of Anis’s public keys; a rising `refresh` means a rotation | Each call’s trace carries the route, the request id and, on orders, the operation id. The SDK never records any of the secrets above — its tests check every log line, trace and metric for them. > **Note** > > More in the [.NET SDK](https://developers.anis.ly/sdks/dotnet.md) page. --- --- title: "API reference — Anis Developers" description: "Every route of the Anis Partner API: what it does, what it needs and what it returns." url: https://developers.anis.ly/reference/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # API reference Every route of the Anis Partner API, generated from the contract the gateway publishes. Each page says what the route is for, the permission it needs, how it is signed, the fields it takes and returns, and every refusal it can give. ## Your application | | | | | --- | --- | --- | | GET | [Read your profile](https://developers.anis.ly/reference/read-profile.md) — `/v1/profile` | `profile:read` | ## Wallets | | | | | --- | --- | --- | | GET | [List wallets](https://developers.anis.ly/reference/list-wallets.md) — `/v1/wallets` | `wallets:read` | | GET | [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) — `/v1/wallets/{walletId}` | `wallets:read` | ## Catalogue | | | | | --- | --- | --- | | GET | [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) — `/v1/wallets/{walletId}/catalog/categories` | `catalogue:read` | | GET | [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) — `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` | `catalogue:read` | | GET | [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) — `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` | `catalogue:read` | | GET | [List cards for sale](https://developers.anis.ly/reference/list-cards.md) — `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` | `catalogue:read` | ## Orders | | | | | --- | --- | --- | | POST | [Place an order](https://developers.anis.ly/reference/create-order.md) — `/v1/wallets/{walletId}/orders` | `orders:create` | | GET | [Read an order](https://developers.anis.ly/reference/read-order.md) — `/v1/orders/{operationId}` | `orders:read` | ## Owned cards | | | | | --- | --- | --- | | GET | [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) — `/v1/wallets/{walletId}/cards` | `cards:read` | | GET | [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) — `/v1/wallets/{walletId}/cards/{soldCardId}` | `cards:read` | | POST | [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) — `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` | `cards:reveal` | | POST | [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) — `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` | `cards:reveal` | ## Diagnostics | | | | | --- | --- | --- | | POST | [Check your signature](https://developers.anis.ly/reference/check-signature.md) — `/v1/diagnostics/signature` | `diagnostics:use` | ## Enrolment | | | | | --- | --- | --- | | GET | [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) — `/v1/enrollments/{invitationId}` | None | | POST | [Submit a public key](https://developers.anis.ly/reference/submit-key.md) — `/v1/enrollments/{invitationId}/keys` | None | | POST | [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) — `/v1/enrollments/{invitationId}/proof` | None | | GET | [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) — `/v1/enrollments/{invitationId}/status` | None | ## Anis signing keys | | | | | --- | --- | --- | | GET | [Anis’s public signing keys](https://developers.anis.ly/reference/signing-keys.md) — `/.well-known/partner-signing-keys.json` | None | --- --- title: "Error codes — Anis Developers" description: "Every refusal the Anis Partner API can return, what it means and what to do." url: https://developers.anis.ly/errors/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Error codes Every refusal is a signed problem with a machine `code`, an HTTP status and a `type` link to its page here. Branch on the `code` — never on the title or detail, which are worded for people and change with the language you ask for. The last column answers the first question to ask when an **order** is refused: did the purchase happen? *This request bought nothing*: fix the cause, then place a new order with a new operation id — but if you were **resuming** an order whose outcome was unknown, a refusal not marked `Idempotency-Replayed` says nothing about that order: keep resuming it. *May still complete*: send the same order again with the **same** operation id — never a new one, which could buy twice. *Never returned when buying*: the error only comes from other calls. ## Keys and signatures Anis could not accept how the call was signed. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | This request bought nothing | | [Signature expired](https://developers.anis.ly/errors/signature-expired.md) | `signature_expired` | 401 | This request bought nothing | | [Invalid content digest](https://developers.anis.ly/errors/invalid-content-digest.md) | `invalid_content_digest` | 400 | This request bought nothing | | [Replay detected](https://developers.anis.ly/errors/replay-detected.md) | `replay_detected` | 409 | May still complete — resume with the same id | ## Permissions and account The call was signed correctly but is not allowed right now. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | This request bought nothing | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | This request bought nothing | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | This request bought nothing | | [Wallet disabled](https://developers.anis.ly/errors/wallet-disabled.md) | `wallet_disabled` | 409 | This request bought nothing | | [Wallet expired](https://developers.anis.ly/errors/wallet-expired.md) | `wallet_expired` | 409 | This request bought nothing | | [Business subscription required](https://developers.anis.ly/errors/business-subscription-required.md) | `business_subscription_required` | 409 | This request bought nothing | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | This request bought nothing | ## The request itself Something in the request does not exist or breaks a rule. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | This request bought nothing | | [Card not found](https://developers.anis.ly/errors/card-not-found.md) | `card_not_found` | 404 | This request bought nothing | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | This request bought nothing | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | This request bought nothing | | [Currency not supported](https://developers.anis.ly/errors/currency-not-supported.md) | `currency_not_supported` | 422 | This request bought nothing | ## Orders Answers only a purchase can receive. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Operation id already used](https://developers.anis.ly/errors/idempotency-conflict.md) | `idempotency_conflict` | 409 | This request bought nothing | | [Order still processing](https://developers.anis.ly/errors/operation-processing.md) | `operation_processing` | 202 | May still complete — resume with the same id | | [Allowed-debt consent required](https://developers.anis.ly/errors/allowed-debt-consent-required.md) | `allowed_debt_consent_required` | 402 | This request bought nothing | | [Insufficient balance](https://developers.anis.ly/errors/insufficient-balance.md) | `insufficient_balance` | 409 | This request bought nothing | | [Purchase not allowed](https://developers.anis.ly/errors/purchase-not-allowed.md) | `purchase_not_allowed` | 403 | This request bought nothing | | [Order refused](https://developers.anis.ly/errors/order-refused.md) | `purchase_not_allowed` | 409 | This request bought nothing | | [Card unavailable](https://developers.anis.ly/errors/card-unavailable.md) | `card_unavailable` | 409 | This request bought nothing | | [Quantity unavailable](https://developers.anis.ly/errors/quantity-unavailable.md) | `quantity_unavailable` | 409 | This request bought nothing | | [Price changed](https://developers.anis.ly/errors/price-changed.md) | `price_changed` | 409 | This request bought nothing | ## Limits A limit on requests or on spending was reached. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Spending allowance used up](https://developers.anis.ly/errors/owner-limit-exceeded.md) | `owner_limit_exceeded` | 409 | This request bought nothing | | [Daily limit reached](https://developers.anis.ly/errors/daily-limit-exceeded.md) | `daily_limit_exceeded` | 429 | This request bought nothing | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | This request bought nothing | ## Revealing cards A card’s codes could not be shown. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Reveal not allowed](https://developers.anis.ly/errors/reveal-not-allowed.md) | `reveal_not_allowed` | 409 | Never returned when buying | | [Invoice too large to reveal](https://developers.anis.ly/errors/invoice-reveal-limit-exceeded.md) | `invoice_reveal_limit_exceeded` | 409 | Never returned when buying | ## Enrolling a key A step of turning an invitation into a working key was refused. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Invitation invalid](https://developers.anis.ly/errors/invitation-invalid.md) | `invitation_invalid` | 401 | Never returned when buying | | [Challenge replaced](https://developers.anis.ly/errors/challenge-expired.md) | `challenge_expired` | 409 | Never returned when buying | | [Public key invalid](https://developers.anis.ly/errors/key-proof-invalid.md) | `key_proof_invalid` | 422 | Never returned when buying | | [Invitation already used](https://developers.anis.ly/errors/key-duplicate.md) | `key_duplicate` | 409 | Never returned when buying | ## No decision reached Anis could not finish the call. On an order, the outcome is unknown. | Error | Code | Status | If this was an order | | --- | --- | --- | --- | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | May still complete — resume with the same id | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | May still complete — resume with the same id | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | May still complete — resume with the same id | --- --- title: "Changelog — Anis Developers" description: "What changed in the Anis Partner API, the SDKs and this website." url: https://developers.anis.ly/changelog/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # Changelog What changed in the Partner API, the SDKs and this website — newest first. ## Developer website 1.0 Upcoming First version of the developer website - Guides: getting started, enrolling a key, signing requests, verifying answers, signing test vectors, orders and recovery, errors and limits, routes and permissions, security, observability, and building with an AI assistant. - An API reference page for every call, and a permanent page for every error code. - English and Arabic. - Ready for AI assistants: every page as Markdown, `llms.txt`, rules for AI assistants, an OpenAPI description, the error codes as data, and signing test vectors. ## .NET SDK 1.0.0 Upcoming First release of the .NET SDK - For .NET 8 and .NET 10. - Signs every request and verifies every answer; every call of the API. - Safe orders: your own operation id, three result types, and for every refused order whether it was placed. - Enrolment of a new key, including the proof of possession. - Several Anis applications in one host. - Traces, metrics and logs under `Anis.Partners.Sdk`, never recording a secret. ## Partner API 1.0 Upcoming First version of the Partner API - Profile, wallets, catalogue, orders, owned cards, reveals and the signature self-check. - Enrolment of keys, replacement with an overlap, and revocation. - Every request signed with your key; every answer signed by Anis. --- --- title: ".NET SDK — Anis Developers" description: "The Anis Partner SDK for .NET 8 and .NET 10 — install, register, call, handle refusals and observe." url: https://developers.anis.ly/sdks/dotnet/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt # .NET SDK `Anis.Partners.Sdk` — for **.NET 8** and **.NET 10**. ```bash dotnet add package Anis.Partners.Sdk ``` ## Register it ```csharp builder.Services .AddAnisPartners(builder.Configuration.GetSection("AnisPartners")) .WithSigner(EcdsaP256Signer.FromPemFile("/secure/partner-key.pem", keyId)); ``` ```json { "AnisPartners": { "Authority": "https://", "SignatureLifetime": "00:01:00", "AcceptLanguage": "English", "SigningKeyCacheDuration": "00:10:00", "Timeout": "00:00:30" } } ``` Then take `IAnisPartnersClient` from the container. The settings are checked when your application starts: a missing address, or a signature lifetime outside 1 to 60 seconds, stops it there. **The signer is a separate step on purpose** — there is no safe default for where your private key lives. `EcdsaP256Signer` signs with a key file; for a vault or hardware module, implement `IRequestSigner` — your key id, and a method that signs bytes and returns the 64-byte signature — and the key never enters your process. ## Use it ```csharp var profile = await anis.Profile.GetAsync(ct); await foreach (var wallet in anis.Wallets.ListAsync(ct)) // pages followed for you Console.WriteLine($"{wallet.Name} {wallet.Balance}"); var cards = await anis.Catalogue.ListCardsAsync(walletId, subcategoryId, cancellationToken: ct); var credential = await anis.OwnedCards.RevealAsync(walletId, soldCardId, ct); ``` ## Orders ```csharp var operationId = Guid.NewGuid(); // yours: store it with the request BEFORE sending var outcome = await anis.Orders.CreateAsync(walletId, operationId, new CreateOrderRequest { CardId = card.Id, Quantity = 2, ExpectedUnitPrice = card.UnitPrice!.Value, ExpectedTotal = card.UnitPrice!.Value.Multiply(2), // exact decimal }, ct); switch (outcome) { case OrderCompleted completed: await vault.StoreAsync(completed.Credentials, ct); break; // store FIRST case OrderProcessing processing: await ResumeAfterAsync(operationId, processing.RetryAfter); break; case OrderReplayed replayed: await EnsureStoredAsync(replayed.Order, ct); break; // sent earlier: if you never stored them (the first answer was lost), reveal by replayed.Order.InvoiceId } ``` `ResumeAsync` sends the same order again under the same id — the way to recover. See [Orders and recovery](https://developers.anis.ly/docs/orders-and-recovery.md) for the full rules and the error handling. ## Refusals Every refusal is an `AnisApiException` (or a subclass for the cases you handle differently) carrying `Code`, `RequestId`, `RetryAfter`, `IsReplayed` and — on an order — `OrderOutcome`: `NotPlaced` or `Unknown`. Each [error page](https://developers.anis.ly/errors.md) names its exception. Three other exceptions mean something different: | Exception | Means | On an order | | --- | --- | --- | | `UnverifiableResponseException` | An answer could not be shown to come from Anis; it was thrown away | Resume with the same id | | `RequestSigningException` | Your signer failed; nothing was sent | Send it again once the signer works | | `TaskCanceledException`, `HttpRequestException` | Timed out, or the connection failed | Resume with the same id | ## Several Anis applications in one host ```csharp builder.Services.AddAnisPartners("brand-a", config.GetSection("AnisPartners:BrandA")).WithSigner(signerA); builder.Services.AddAnisPartners("brand-b", config.GetSection("AnisPartners:BrandB")).WithSigner(signerB); var brandA = factory.GetClient("brand-a"); // IAnisPartnersClientFactory, or [FromKeyedServices("brand-a")] ``` Each application has its own pipeline and key, and its traces and metrics carry `anis.client`. > **No automatic retries on this client** > > A retry policy added to every HTTP client (for example the .NET Aspire service defaults) gives up on each attempt after 10 seconds by default, sooner than the SDK’s 30. A slow order can then complete at Anis while its answer — with the card codes — is thrown away. Retry reads yourself if you like; recover orders with `ResumeAsync`. ## Observability Traces and metrics under `Anis.Partners.Sdk`; logs through your own logging. See [Observability](https://developers.anis.ly/docs/observability.md) for the metric names and what to alert on. ## If your host replaces .NET’s built-in container The SDK uses .NET 8 keyed services. A third-party container must support them — Autofac 9 or later, Lamar 12.1 or later, and SimpleInjector all do. Otherwise build `AnisPartnersClient` directly with an `HttpClient` that carries a `PartnerVerifyingHandler` around a `PartnerSigningHandler`. --- --- 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=""`; 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 --- --- title: "Read your profile — Anis Developers" description: "Your application’s identity, the Anis business account it acts for, and the permissions its current policy grants. Permissions are re-evaluated on every call, so read them rather than caching them." url: https://developers.anis.ly/reference/read-profile/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Your application # Read your profile `GET /v1/profile` Your application’s identity, the Anis business account it acts for, and the permissions its current policy grants. Permissions are re-evaluated on every call, so read them rather than caching them. - **Permission:** `profile:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Profile.GetAsync()` ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [Profile](#type-profile) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### Profile | Field | Type | Notes | | --- | --- | --- | | `partner` (required) | object | The partner your application belongs to. | | `partner.id` (required) | UUID | The partner’s id. | | `application` (required) | object | Your application. | | `application.id` (required) | UUID | Your application’s id. It stays the same when your keys are replaced. | | `application.scopes` (required) | list of string | The permissions your application has right now. Re-read rather than cache: a change applies to your next call. | | `ownerAccount` | object | The Anis business account your application acts for, when shown. | | `ownerAccount.id` | UUID | The business account’s id. | | `ownerAccount.displayName` | string | The business account’s name. | | `documentationVersion` | string | The version of this documentation the Anis deployment you called matches. | --- --- title: "List wallets — Anis Developers" description: "The wallets your application may use, with their balances. Pages with a cursor." url: https://developers.anis.ly/reference/list-wallets/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Wallets # List wallets `GET /v1/wallets` The wallets your application may use, with their balances. Pages with a cursor. - **Permission:** `wallets:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Wallets.ListAsync()` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `cursor` (optional) | `query` | string | The `nextCursor` of the previous page, passed back exactly as received. Leave it out for the first page. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [WalletCollection](#type-walletcollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### WalletCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [Wallet](#type-wallet) | The wallets on this page. | | `nextCursor` | string | Pass it as `cursor` to get the next page. Absent on the last page. | ### Wallet | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The wallet’s id. Every wallet-level call takes it in the address. | | `name` (required) | string | The wallet’s name. | | `currency` (required) | string | The currency the wallet holds and buys in. | | `balance` (required) | [Money](#type-money) | The balance you can spend now, with the moment it was read. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "Read a wallet — Anis Developers" description: "One wallet and its balance." url: https://developers.anis.ly/reference/read-wallet/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Wallets # Read a wallet `GET /v1/wallets/{walletId}` One wallet and its balance. - **Permission:** `wallets:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Wallets.GetAsync(walletId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [Wallet](#type-wallet) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### Wallet | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The wallet’s id. Every wallet-level call takes it in the address. | | `name` (required) | string | The wallet’s name. | | `currency` (required) | string | The currency the wallet holds and buys in. | | `balance` (required) | [Money](#type-money) | The balance you can spend now, with the moment it was read. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "List catalogue categories — Anis Developers" description: "The top level of the catalogue, as this wallet sees it. Pages with a cursor." url: https://developers.anis.ly/reference/list-categories/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Catalogue # List catalogue categories `GET /v1/wallets/{walletId}/catalog/categories` The top level of the catalogue, as this wallet sees it. Pages with a cursor. - **Permission:** `catalogue:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Catalogue.ListCategoriesAsync(walletId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `cursor` (optional) | `query` | string | The `nextCursor` of the previous page, passed back exactly as received. Leave it out for the first page. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [CatalogueCollection](#type-cataloguecollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### CatalogueCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [CatalogueObject](#type-catalogueobject) | The categories, subcategories or cards on this page — each list route returns one kind. | | `nextCursor` | string | Pass it as `cursor` to get the next page. Absent on the last page. | ### CatalogueObject One of: [CatalogueCategory](#type-cataloguecategory), [CatalogueSubcategory](#type-cataloguesubcategory), [CatalogueCard](#type-cataloguecard) — each route answers with exactly one of them. ### CatalogueCategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The category’s id. | | `name` | [LocalizedText](#type-localizedtext) | The category’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the category’s logo image. | | `type` | string | Whether the category is local or international. Values: `local`, `international` | | `inStock` (required) | boolean | Whether anything in it can be bought now. | | `displayOrder` (required) | integer | The order to show categories in — lower first. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | ### CatalogueSubcategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The subcategory’s id. | | `categoryId` (required) | UUID | The category it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The subcategory’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the subcategory’s logo image. | | `isBestSelling` (required) | boolean | Marked by Anis as a best seller. | | `displayOrder` (required) | integer | The order to show subcategories in — lower first. | | `available` (required) | boolean | Whether it can be bought from now. | ### CatalogueCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The card’s id — the `cardId` an order carries. | | `subcategoryId` (required) | UUID | The subcategory it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The card’s name, in Arabic and English. | | `faceValue` | string | The value printed on the card, when it has one. | | `unitPrice` | [Money](#type-money) | The price THIS wallet pays. Send it unchanged as the order’s `expectedUnitPrice`. A card without it cannot be sold to this wallet. | | `businessPrice` | [Money](#type-money) | For display. An order at this price can be refused as `price_changed`. | | `personalPrice` | [Money](#type-money) | The retail price, shown only while it is above the business price. For display. | | `hasSpecialOffer` (required) | boolean | Whether a special offer applies. | | `specialOfferPrice` | [Money](#type-money) | The offer price, shown only with a special offer. For display. | | `available` (required) | boolean | Whether the card can be bought now. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "List subcategories of a category — Anis Developers" description: "The subcategories under one category. Pages with a cursor." url: https://developers.anis.ly/reference/list-subcategories/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Catalogue # List subcategories of a category `GET /v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` The subcategories under one category. Pages with a cursor. - **Permission:** `catalogue:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Catalogue.ListSubcategoriesAsync(walletId, categoryId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `categoryId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `cursor` (optional) | `query` | string | The `nextCursor` of the previous page, passed back exactly as received. Leave it out for the first page. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [CatalogueCollection](#type-cataloguecollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### CatalogueCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [CatalogueObject](#type-catalogueobject) | The categories, subcategories or cards on this page — each list route returns one kind. | | `nextCursor` | string | Pass it as `cursor` to get the next page. Absent on the last page. | ### CatalogueObject One of: [CatalogueCategory](#type-cataloguecategory), [CatalogueSubcategory](#type-cataloguesubcategory), [CatalogueCard](#type-cataloguecard) — each route answers with exactly one of them. ### CatalogueCategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The category’s id. | | `name` | [LocalizedText](#type-localizedtext) | The category’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the category’s logo image. | | `type` | string | Whether the category is local or international. Values: `local`, `international` | | `inStock` (required) | boolean | Whether anything in it can be bought now. | | `displayOrder` (required) | integer | The order to show categories in — lower first. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | ### CatalogueSubcategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The subcategory’s id. | | `categoryId` (required) | UUID | The category it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The subcategory’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the subcategory’s logo image. | | `isBestSelling` (required) | boolean | Marked by Anis as a best seller. | | `displayOrder` (required) | integer | The order to show subcategories in — lower first. | | `available` (required) | boolean | Whether it can be bought from now. | ### CatalogueCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The card’s id — the `cardId` an order carries. | | `subcategoryId` (required) | UUID | The subcategory it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The card’s name, in Arabic and English. | | `faceValue` | string | The value printed on the card, when it has one. | | `unitPrice` | [Money](#type-money) | The price THIS wallet pays. Send it unchanged as the order’s `expectedUnitPrice`. A card without it cannot be sold to this wallet. | | `businessPrice` | [Money](#type-money) | For display. An order at this price can be refused as `price_changed`. | | `personalPrice` | [Money](#type-money) | The retail price, shown only while it is above the business price. For display. | | `hasSpecialOffer` (required) | boolean | Whether a special offer applies. | | `specialOfferPrice` | [Money](#type-money) | The offer price, shown only with a special offer. For display. | | `available` (required) | boolean | Whether the card can be bought now. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "Read a subcategory — Anis Developers" description: "One subcategory." url: https://developers.anis.ly/reference/read-subcategory/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Catalogue # Read a subcategory `GET /v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` One subcategory. - **Permission:** `catalogue:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Catalogue.GetSubcategoryAsync(walletId, subcategoryId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `subcategoryId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [CatalogueObject](#type-catalogueobject) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### CatalogueObject One of: [CatalogueCategory](#type-cataloguecategory), [CatalogueSubcategory](#type-cataloguesubcategory), [CatalogueCard](#type-cataloguecard) — each route answers with exactly one of them. ### CatalogueCategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The category’s id. | | `name` | [LocalizedText](#type-localizedtext) | The category’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the category’s logo image. | | `type` | string | Whether the category is local or international. Values: `local`, `international` | | `inStock` (required) | boolean | Whether anything in it can be bought now. | | `displayOrder` (required) | integer | The order to show categories in — lower first. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | ### CatalogueSubcategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The subcategory’s id. | | `categoryId` (required) | UUID | The category it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The subcategory’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the subcategory’s logo image. | | `isBestSelling` (required) | boolean | Marked by Anis as a best seller. | | `displayOrder` (required) | integer | The order to show subcategories in — lower first. | | `available` (required) | boolean | Whether it can be bought from now. | ### CatalogueCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The card’s id — the `cardId` an order carries. | | `subcategoryId` (required) | UUID | The subcategory it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The card’s name, in Arabic and English. | | `faceValue` | string | The value printed on the card, when it has one. | | `unitPrice` | [Money](#type-money) | The price THIS wallet pays. Send it unchanged as the order’s `expectedUnitPrice`. A card without it cannot be sold to this wallet. | | `businessPrice` | [Money](#type-money) | For display. An order at this price can be refused as `price_changed`. | | `personalPrice` | [Money](#type-money) | The retail price, shown only while it is above the business price. For display. | | `hasSpecialOffer` (required) | boolean | Whether a special offer applies. | | `specialOfferPrice` | [Money](#type-money) | The offer price, shown only with a special offer. For display. | | `available` (required) | boolean | Whether the card can be bought now. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "List cards for sale — Anis Developers" description: "The cards of a subcategory that this wallet can buy, with the price this wallet pays. Pages with a cursor." url: https://developers.anis.ly/reference/list-cards/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Catalogue # List cards for sale `GET /v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` The cards of a subcategory that this wallet can buy, with the price this wallet pays. Pages with a cursor. - **Permission:** `catalogue:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Catalogue.ListCardsAsync(walletId, subcategoryId)` - Send a card’s `unitPrice` as the order’s `expectedUnitPrice`. The other prices are for display. ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `subcategoryId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `cursor` (optional) | `query` | string | The `nextCursor` of the previous page, passed back exactly as received. Leave it out for the first page. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [CatalogueCollection](#type-cataloguecollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### CatalogueCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [CatalogueObject](#type-catalogueobject) | The categories, subcategories or cards on this page — each list route returns one kind. | | `nextCursor` | string | Pass it as `cursor` to get the next page. Absent on the last page. | ### CatalogueObject One of: [CatalogueCategory](#type-cataloguecategory), [CatalogueSubcategory](#type-cataloguesubcategory), [CatalogueCard](#type-cataloguecard) — each route answers with exactly one of them. ### CatalogueCategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The category’s id. | | `name` | [LocalizedText](#type-localizedtext) | The category’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the category’s logo image. | | `type` | string | Whether the category is local or international. Values: `local`, `international` | | `inStock` (required) | boolean | Whether anything in it can be bought now. | | `displayOrder` (required) | integer | The order to show categories in — lower first. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | ### CatalogueSubcategory | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The subcategory’s id. | | `categoryId` (required) | UUID | The category it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The subcategory’s name, in Arabic and English. | | `description` | [LocalizedText](#type-localizedtext) | A description, in Arabic and English. | | `logo` | string | The address of the subcategory’s logo image. | | `isBestSelling` (required) | boolean | Marked by Anis as a best seller. | | `displayOrder` (required) | integer | The order to show subcategories in — lower first. | | `available` (required) | boolean | Whether it can be bought from now. | ### CatalogueCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The card’s id — the `cardId` an order carries. | | `subcategoryId` (required) | UUID | The subcategory it belongs to. | | `name` | [LocalizedText](#type-localizedtext) | The card’s name, in Arabic and English. | | `faceValue` | string | The value printed on the card, when it has one. | | `unitPrice` | [Money](#type-money) | The price THIS wallet pays. Send it unchanged as the order’s `expectedUnitPrice`. A card without it cannot be sold to this wallet. | | `businessPrice` | [Money](#type-money) | For display. An order at this price can be refused as `price_changed`. | | `personalPrice` | [Money](#type-money) | The retail price, shown only while it is above the business price. For display. | | `hasSpecialOffer` (required) | boolean | Whether a special offer applies. | | `specialOfferPrice` | [Money](#type-money) | The offer price, shown only with a special offer. For display. | | `available` (required) | boolean | Whether the card can be bought now. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | --- --- title: "Place an order — Anis Developers" description: "Buys cards from a wallet, under an operation id you choose and keep. The same id sent again never buys twice: it resumes the same order." url: https://developers.anis.ly/reference/create-order/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Orders # Place an order `POST /v1/wallets/{walletId}/orders` Buys cards from a wallet, under an operation id you choose and keep. The same id sent again never buys twice: it resumes the same order. - **Permission:** `orders:create` - **Authentication:** Signed order - **Staff-set limits it counts toward:** All requests, Orders - **.NET SDK:** `anis.Orders.CreateAsync(walletId, operationId, order) / ResumeAsync(...)` - `201`: the order completed, and this answer carries the card codes. Store them first. - `200`: the answer a resume receives when the first answer was lost. If it carries card codes, it is the first time you see them — store them. - An answer marked `Idempotency-Replayed: true` repeats an order whose outcome was already sent: it carries the order, not the codes. If you never received that first answer — a timeout — the codes were in it: reveal them with the order’s invoice id, which needs `cards:reveal`. - `202`: accepted, no outcome yet. Send the same order again after `Retry-After`. ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key, with a one-time nonce, a digest of the body and your operation id as `Idempotency-Key`. Headers: `Signature-Input`, `Signature`, `Content-Digest`, `Nonce`, `Idempotency-Key`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** [CreateOrderRequest](#type-createorderrequest), as JSON. | Field | Type | Notes | | --- | --- | --- | | `externalReference` | string | Your own reference, 1 to 100 characters: Latin letters and digits, space and `- _ . : / #`. | | `cardId` (required) | UUID | The catalogue card to buy. | | `quantity` (required) | integer | At least 1 and at most the per-order maximum (100 by default), and within the card’s own minimum and maximum. | | `expectedUnitPrice` (required) | [Money](#type-money) | The card’s `unitPrice` exactly as the catalogue showed it for this wallet. | | `expectedTotal` (required) | [Money](#type-money) | `expectedUnitPrice` times `quantity`, exactly — computed without floating point. | | `useAllowedDebt` | boolean | Set to `true` only if you consent to use the account’s allowed debt. Defaults to `false` and is never switched on for you. | ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [Order](#type-order) | | 201 | The order completed. | [Order](#type-order) | | 202 | Accepted — no outcome yet. | [Order](#type-order) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 402 | Refused: needs consent to use allowed debt. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Replay detected](https://developers.anis.ly/errors/replay-detected.md) | `replay_detected` | 409 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Currency not supported](https://developers.anis.ly/errors/currency-not-supported.md) | `currency_not_supported` | 422 | | [Operation id already used](https://developers.anis.ly/errors/idempotency-conflict.md) | `idempotency_conflict` | 409 | | [Allowed-debt consent required](https://developers.anis.ly/errors/allowed-debt-consent-required.md) | `allowed_debt_consent_required` | 402 | | [Insufficient balance](https://developers.anis.ly/errors/insufficient-balance.md) | `insufficient_balance` | 409 | | [Purchase not allowed](https://developers.anis.ly/errors/purchase-not-allowed.md) | `purchase_not_allowed` | 403 | | [Order refused](https://developers.anis.ly/errors/order-refused.md) | `purchase_not_allowed` | 409 | | [Business subscription required](https://developers.anis.ly/errors/business-subscription-required.md) | `business_subscription_required` | 409 | | [Wallet disabled](https://developers.anis.ly/errors/wallet-disabled.md) | `wallet_disabled` | 409 | | [Wallet expired](https://developers.anis.ly/errors/wallet-expired.md) | `wallet_expired` | 409 | | [Card unavailable](https://developers.anis.ly/errors/card-unavailable.md) | `card_unavailable` | 409 | | [Quantity unavailable](https://developers.anis.ly/errors/quantity-unavailable.md) | `quantity_unavailable` | 409 | | [Price changed](https://developers.anis.ly/errors/price-changed.md) | `price_changed` | 409 | | [Spending allowance used up](https://developers.anis.ly/errors/owner-limit-exceeded.md) | `owner_limit_exceeded` | 409 | | [Daily limit reached](https://developers.anis.ly/errors/daily-limit-exceeded.md) | `daily_limit_exceeded` | 429 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### CreateOrderRequest | Field | Type | Notes | | --- | --- | --- | | `externalReference` | string | Your own reference, 1 to 100 characters: Latin letters and digits, space and `- _ . : / #`. | | `cardId` (required) | UUID | The catalogue card to buy. | | `quantity` (required) | integer | At least 1 and at most the per-order maximum (100 by default), and within the card’s own minimum and maximum. | | `expectedUnitPrice` (required) | [Money](#type-money) | The card’s `unitPrice` exactly as the catalogue showed it for this wallet. | | `expectedTotal` (required) | [Money](#type-money) | `expectedUnitPrice` times `quantity`, exactly — computed without floating point. | | `useAllowedDebt` | boolean | Set to `true` only if you consent to use the account’s allowed debt. Defaults to `false` and is never switched on for you. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | ### Order | Field | Type | Notes | | --- | --- | --- | | `operationId` (required) | UUID | The operation id you chose and sent as `Idempotency-Key`. | | `status` (required) | string | `processing`: accepted, no outcome yet. `completed`: bought. `failed`: refused by a business rule, nothing bought. `recoveryExhausted`: Anis could not learn the outcome yet — it may have completed; keep resuming slowly and tell Anis. Values: `processing`, `recoveryExhausted`, `completed`, `failed` | | `invoiceId` | UUID | The invoice the cards are on. Use it to reveal them again later. | | `walletId` | UUID | The wallet the order was paid from. | | `cardId` | UUID | The catalogue card bought. | | `quantity` | integer | How many cards. | | `total` | [Money](#type-money) | What the order cost. | | `soldCards` | list of [RevealedCredential](#type-revealedcredential) | The card codes. Present only on the answer that FIRST reports the order completed — store them before anything else. | | `completedAt` | string (date-time) | When the order completed. | ### RevealedCredential | Field | Type | Notes | | --- | --- | --- | | `soldCardId` (required) | UUID | The sold card these codes belong to. | | `serialNumber` | string | The card’s serial number. A secret: store it where you keep secrets and never log it. | | `voucher` | string | The card’s voucher code. A secret: store it where you keep secrets and never log it. | | `revealedAt` | string (date-time) | When it was revealed. | --- --- title: "Read an order — Anis Developers" description: "Where an order stands. It reports state only: it never carries card codes and never moves an order forward — to recover an order, send it again." url: https://developers.anis.ly/reference/read-order/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Orders # Read an order `GET /v1/orders/{operationId}` Where an order stands. It reports state only: it never carries card codes and never moves an order forward — to recover an order, send it again. - **Permission:** `orders:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Orders.GetAsync(operationId)` - Allowed with `orders:read`, or with `orders:create` for your own orders. ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `operationId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [Order](#type-order) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### Order | Field | Type | Notes | | --- | --- | --- | | `operationId` (required) | UUID | The operation id you chose and sent as `Idempotency-Key`. | | `status` (required) | string | `processing`: accepted, no outcome yet. `completed`: bought. `failed`: refused by a business rule, nothing bought. `recoveryExhausted`: Anis could not learn the outcome yet — it may have completed; keep resuming slowly and tell Anis. Values: `processing`, `recoveryExhausted`, `completed`, `failed` | | `invoiceId` | UUID | The invoice the cards are on. Use it to reveal them again later. | | `walletId` | UUID | The wallet the order was paid from. | | `cardId` | UUID | The catalogue card bought. | | `quantity` | integer | How many cards. | | `total` | [Money](#type-money) | What the order cost. | | `soldCards` | list of [RevealedCredential](#type-revealedcredential) | The card codes. Present only on the answer that FIRST reports the order completed — store them before anything else. | | `completedAt` | string (date-time) | When the order completed. | ### Money | Field | Type | Notes | | --- | --- | --- | | `amount` (required) | string | A decimal string with exactly three decimals, such as `"10.500"` — never a JSON number. | | `currency` (required) | string | The currency code, such as `LYD`: always the wallet’s currency. | | `asOf` | string (date-time) | When this price or balance was read. | ### RevealedCredential | Field | Type | Notes | | --- | --- | --- | | `soldCardId` (required) | UUID | The sold card these codes belong to. | | `serialNumber` | string | The card’s serial number. A secret: store it where you keep secrets and never log it. | | `voucher` | string | The card’s voucher code. A secret: store it where you keep secrets and never log it. | | `revealedAt` | string (date-time) | When it was revealed. | --- --- title: "List owned cards — Anis Developers" description: "The cards a wallet has bought, with their codes hidden. Pages with a cursor." url: https://developers.anis.ly/reference/list-owned-cards/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Owned cards # List owned cards `GET /v1/wallets/{walletId}/cards` The cards a wallet has bought, with their codes hidden. Pages with a cursor. - **Permission:** `cards:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.OwnedCards.ListAsync(walletId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `cursor` (optional) | `query` | string | The `nextCursor` of the previous page, passed back exactly as received. Leave it out for the first page. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [MaskedCardCollection](#type-maskedcardcollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### MaskedCardCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [MaskedCard](#type-maskedcard) | The owned cards on this page. | | `nextCursor` | string | Pass it as `cursor` to get the next page. Absent on the last page. | ### MaskedCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The sold card’s id — what revealing one card takes. | | `orderOperationId` | UUID | The operation id of the order that bought it. | | `invoiceId` | any | The invoice it is on — what revealing a whole invoice takes. | | `card` | object | The catalogue card it was sold from. | | `card.id` | UUID | The catalogue card’s id. | | `card.name` | [LocalizedText](#type-localizedtext) | The catalogue card’s name, in Arabic and English. | | `serialNumberMasked` | string | The serial number, partly hidden. The full codes come only from a reveal. | | `credentialAvailable` (required) | boolean | Whether this card’s codes can be revealed. | | `purchasedAt` | string (date-time) | When it was bought. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | --- --- title: "Read an owned card — Anis Developers" description: "One bought card, with its codes hidden." url: https://developers.anis.ly/reference/read-owned-card/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Owned cards # Read an owned card `GET /v1/wallets/{walletId}/cards/{soldCardId}` One bought card, with its codes hidden. - **Permission:** `cards:read` - **Authentication:** Signed read - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.OwnedCards.GetAsync(walletId, soldCardId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `soldCardId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key. No body, no nonce. Headers: `Signature-Input`, `Signature`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [MaskedCard](#type-maskedcard) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Card not found](https://developers.anis.ly/errors/card-not-found.md) | `card_not_found` | 404 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### MaskedCard | Field | Type | Notes | | --- | --- | --- | | `id` (required) | UUID | The sold card’s id — what revealing one card takes. | | `orderOperationId` | UUID | The operation id of the order that bought it. | | `invoiceId` | any | The invoice it is on — what revealing a whole invoice takes. | | `card` | object | The catalogue card it was sold from. | | `card.id` | UUID | The catalogue card’s id. | | `card.name` | [LocalizedText](#type-localizedtext) | The catalogue card’s name, in Arabic and English. | | `serialNumberMasked` | string | The serial number, partly hidden. The full codes come only from a reveal. | | `credentialAvailable` (required) | boolean | Whether this card’s codes can be revealed. | | `purchasedAt` | string (date-time) | When it was bought. | ### LocalizedText | Field | Type | Notes | | --- | --- | --- | | `ar` | string | The Arabic text. | | `en` | string | The English text. | --- --- title: "Reveal a card — Anis Developers" description: "Shows one bought card’s codes. Sends no body." url: https://developers.anis.ly/reference/reveal-card/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Owned cards # Reveal a card `POST /v1/wallets/{walletId}/cards/{soldCardId}/reveal` Shows one bought card’s codes. Sends no body. - **Permission:** `cards:reveal` - **Authentication:** Signed change - **Staff-set limits it counts toward:** All requests, Reveals - **.NET SDK:** `anis.OwnedCards.RevealAsync(walletId, soldCardId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `soldCardId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key, with a one-time nonce and a digest of the body. Headers: `Signature-Input`, `Signature`, `Content-Digest`, `Nonce`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** No body at all — not even `{}`. Any byte is refused with `validation_failed`. The signature still covers the digest of the empty body. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [RevealedCredential](#type-revealedcredential) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Replay detected](https://developers.anis.ly/errors/replay-detected.md) | `replay_detected` | 409 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Card not found](https://developers.anis.ly/errors/card-not-found.md) | `card_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Reveal not allowed](https://developers.anis.ly/errors/reveal-not-allowed.md) | `reveal_not_allowed` | 409 | | [Business subscription required](https://developers.anis.ly/errors/business-subscription-required.md) | `business_subscription_required` | 409 | | [Wallet disabled](https://developers.anis.ly/errors/wallet-disabled.md) | `wallet_disabled` | 409 | | [Wallet expired](https://developers.anis.ly/errors/wallet-expired.md) | `wallet_expired` | 409 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### RevealedCredential | Field | Type | Notes | | --- | --- | --- | | `soldCardId` (required) | UUID | The sold card these codes belong to. | | `serialNumber` | string | The card’s serial number. A secret: store it where you keep secrets and never log it. | | `voucher` | string | The card’s voucher code. A secret: store it where you keep secrets and never log it. | | `revealedAt` | string (date-time) | When it was revealed. | --- --- title: "Reveal an invoice — Anis Developers" description: "Shows the codes of every card on an invoice — all of them or none, up to 100. Sends no body." url: https://developers.anis.ly/reference/reveal-invoice/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Owned cards # Reveal an invoice `POST /v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` Shows the codes of every card on an invoice — all of them or none, up to 100. Sends no body. - **Permission:** `cards:reveal` - **Authentication:** Signed change - **Staff-set limits it counts toward:** All requests, Reveals - **.NET SDK:** `anis.OwnedCards.RevealInvoiceAsync(walletId, invoiceId)` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `walletId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | | `invoiceId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Signed with your key, with a one-time nonce and a digest of the body. Headers: `Signature-Input`, `Signature`, `Content-Digest`, `Nonce`, `X-Anis-Date`, `Accept-Language` (optional). The SDKs set all of them for you. **Body:** No body at all — not even `{}`. Any byte is refused with `validation_failed`. The signature still covers the digest of the empty body. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [RevealedCredentialCollection](#type-revealedcredentialcollection) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Replay detected](https://developers.anis.ly/errors/replay-detected.md) | `replay_detected` | 409 | | [Account not authorised](https://developers.anis.ly/errors/binding-not-authorized.md) | `binding_not_authorized` | 403 | | [Account inactive](https://developers.anis.ly/errors/account-inactive.md) | `account_inactive` | 403 | | [Wallet not granted](https://developers.anis.ly/errors/wallet-not-granted.md) | `wallet_not_granted` | 404 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Reveal not allowed](https://developers.anis.ly/errors/reveal-not-allowed.md) | `reveal_not_allowed` | 409 | | [Invoice too large to reveal](https://developers.anis.ly/errors/invoice-reveal-limit-exceeded.md) | `invoice_reveal_limit_exceeded` | 409 | | [Business subscription required](https://developers.anis.ly/errors/business-subscription-required.md) | `business_subscription_required` | 409 | | [Wallet disabled](https://developers.anis.ly/errors/wallet-disabled.md) | `wallet_disabled` | 409 | | [Wallet expired](https://developers.anis.ly/errors/wallet-expired.md) | `wallet_expired` | 409 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### RevealedCredentialCollection | Field | Type | Notes | | --- | --- | --- | | `items` (required) | list of [RevealedCredential](#type-revealedcredential) | Every card on the invoice with its codes — all of them or none, at most 100. | ### RevealedCredential | Field | Type | Notes | | --- | --- | --- | | `soldCardId` (required) | UUID | The sold card these codes belong to. | | `serialNumber` | string | The card’s serial number. A secret: store it where you keep secrets and never log it. | | `voucher` | string | The card’s voucher code. A secret: store it where you keep secrets and never log it. | | `revealedAt` | string (date-time) | When it was revealed. | --- --- title: "Check your signature — Anis Developers" description: "The signature self-check: runs your call through Anis’s full checks and reports exactly what Anis saw — the method, address, path and query it signed over, the key it found and the permissions in force. It changes nothing. Sends exactly `{}`." url: https://developers.anis.ly/reference/check-signature/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Diagnostics # Check your signature `POST /v1/diagnostics/signature` The signature self-check: runs your call through Anis’s full checks and reports exactly what Anis saw — the method, address, path and query it signed over, the key it found and the permissions in force. It changes nothing. Sends exactly `{}`. - **Permission:** `diagnostics:use` - **Authentication:** Signed change - **Staff-set limits it counts toward:** All requests - **.NET SDK:** `anis.Diagnostics.CheckSignatureAsync()` - The right first call when a signature will not verify. ## Request Signed with your key, with a one-time nonce and a digest of the body. Headers: `Signature-Input`, `Signature`, `Content-Digest`, `Nonce`, `X-Anis-Date`. The SDKs set all of them for you. **Body:** Exactly the two bytes `{}` — nothing else. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [SignatureDiagnostic](#type-signaturediagnostic) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 403 | Refused: not allowed. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invalid credentials](https://developers.anis.ly/errors/invalid-credentials.md) | `invalid_credentials` | 401 | | [Insufficient scope](https://developers.anis.ly/errors/insufficient-scope.md) | `insufficient_scope` | 403 | | [Source address not allowed](https://developers.anis.ly/errors/source-ip-not-allowed.md) | `source_ip_not_allowed` | 403 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Replay detected](https://developers.anis.ly/errors/replay-detected.md) | `replay_detected` | 409 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | ## Types ### SignatureDiagnostic | Field | Type | Notes | | --- | --- | --- | | `routeId` (required) | string | The route Anis matched your call to. | | `method` (required) | string | The method, as Anis saw it. | | `authority` (required) | string | The host (and port, if not the default), as Anis saw it — lower-cased. | | `path` (required) | string | The path, as Anis saw it. | | `canonicalQuery` (required) | string | The query, as Anis saw it, without its leading `?`. | | `requestKind` (required) | string | How Anis expects this route to be signed. Values: `safeRead`, `bodylessNonceMutation`, `orderMutation`, `enrollmentToken`, `public` | | `requiredScope` (required) | string | The permission this route needs. | | `coveredComponents` (required) | list of string | The signed components this route requires, in order. | | `keyId` (required) | UUID | The key id Anis found your signature under. | | `partnerId` (required) | UUID | The partner that key belongs to. | | `applicationId` (required) | UUID | The application that key belongs to. | | `policyVersion` (required) | integer | The version of your application’s permissions that applied. It goes up each time Anis staff change them. | | `effectiveScopes` (required) | list of string | The permissions in force for this call. | | `receivedAt` (required) | string (date-time) | When Anis received the call. | --- --- title: "Read an invitation — Anis Developers" description: "The invitation’s state and when it expires. Authorised with the enrolment token, not a signature." url: https://developers.anis.ly/reference/read-invitation/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Enrolment # Read an invitation `GET /v1/enrollments/{invitationId}` The invitation’s state and when it expires. Authorised with the enrolment token, not a signature. - **Permission:** None - **Authentication:** Enrolment token - **Staff-set limits it counts toward:** None - **.NET SDK:** `enrollment.GetAsync()` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `invitationId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Authorised with `Authorization: Enrollment `; not signed. Headers: `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [EnrollmentState](#type-enrollmentstate) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invitation invalid](https://developers.anis.ly/errors/invitation-invalid.md) | `invitation_invalid` | 401 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### EnrollmentState | Field | Type | Notes | | --- | --- | --- | | `invitationId` | UUID | The invitation. | | `applicationId` | UUID | The application being enrolled. | | `state` | string | `pendingInvitation`, `pendingPublicKey`, `pendingProof`, `pendingApproval`, `active` or `unavailable`. | | `expiresAt` | string (date-time) | When the invitation expires — about a day after it is issued, by default. | --- --- title: "Submit a public key — Anis Developers" description: "Sends the PUBLIC half of a new P-256 key, and receives the challenge to prove you hold the private half." url: https://developers.anis.ly/reference/submit-key/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Enrolment # Submit a public key `POST /v1/enrollments/{invitationId}/keys` Sends the PUBLIC half of a new P-256 key, and receives the challenge to prove you hold the private half. - **Permission:** None - **Authentication:** Enrolment token - **Staff-set limits it counts toward:** None - **.NET SDK:** `enrollment.SubmitKeyAsync(request)` - Save the private key before you call this: an invitation takes exactly one key. ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `invitationId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Authorised with `Authorization: Enrollment `; not signed. Headers: `Accept-Language` (optional). The SDKs set all of them for you. **Body:** [EnrollmentKeyRequest](#type-enrollmentkeyrequest), as JSON. | Field | Type | Notes | | --- | --- | --- | | `publicJwk` (required) | [PublicJwk](#type-publicjwk) | The PUBLIC half of a new P-256 key only. A private member is refused. | | `notBefore` (required) | string (date-time) | Only the length of the window (to `expiresAt`) is used — kept between 1 day and 2 years by default — and it starts when Anis staff activate the key. | | `expiresAt` (required) | string (date-time) | The end of the validity you ask for. See `notBefore`. | | `cidrs` | list of string | The networks you will call from, proposed for Anis staff to confirm. | ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [EnrollmentKeyResult](#type-enrollmentkeyresult) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invitation invalid](https://developers.anis.ly/errors/invitation-invalid.md) | `invitation_invalid` | 401 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Public key invalid](https://developers.anis.ly/errors/key-proof-invalid.md) | `key_proof_invalid` | 422 | | [Invitation already used](https://developers.anis.ly/errors/key-duplicate.md) | `key_duplicate` | 409 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### EnrollmentKeyRequest | Field | Type | Notes | | --- | --- | --- | | `publicJwk` (required) | [PublicJwk](#type-publicjwk) | The PUBLIC half of a new P-256 key only. A private member is refused. | | `notBefore` (required) | string (date-time) | Only the length of the window (to `expiresAt`) is used — kept between 1 day and 2 years by default — and it starts when Anis staff activate the key. | | `expiresAt` (required) | string (date-time) | The end of the validity you ask for. See `notBefore`. | | `cidrs` | list of string | The networks you will call from, proposed for Anis staff to confirm. | ### PublicJwk | Field | Type | Notes | | --- | --- | --- | | `kty` (required) | string | Always `EC`. Values: `EC` | | `crv` (required) | string | Always `P-256`. Values: `P-256` | | `x` (required) | string | The key’s x coordinate: exactly 32 bytes, base64url without padding. | | `y` (required) | string | The key’s y coordinate: exactly 32 bytes, base64url without padding. | | `kid` | string | In Anis’s signing keys: the key an answer’s signature names. Treat it as an opaque name. Not sent when you submit your own key. | | `use` | string | Intended use, when published. | | `alg` | string | Algorithm, when published. | ### EnrollmentKeyResult | Field | Type | Notes | | --- | --- | --- | | `keyId` (required) | UUID | Your key id: the `keyid` every signature will carry once the key is active. | | `thumbprint` (required) | string | The key’s fingerprint. Give it to Anis staff through the channel you agreed; they confirm the key with it. | | `challenge` (required) | string | The challenge your proof answers. It is returned only here: keep it until the proof is accepted. | | `challengeGeneration` | integer | Which challenge this is; a restarted enrolment issues the next one. | --- --- title: "Prove you hold the key — Anis Developers" description: "Sends the proof of possession. Once accepted, the key waits for Anis staff to record its fingerprint and confirm it." url: https://developers.anis.ly/reference/submit-proof/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Enrolment # Prove you hold the key `POST /v1/enrollments/{invitationId}/proof` Sends the proof of possession. Once accepted, the key waits for Anis staff to record its fingerprint and confirm it. - **Permission:** None - **Authentication:** Enrolment token - **Staff-set limits it counts toward:** None - **.NET SDK:** `enrollment.ProveAsync(submitted, key)` - A proof that does not verify, or comes more than about 30 minutes after the key submission, is not refused: the answer says `proofState: "failed"`. Check for `"accepted"`. - After five failed proofs, further proofs are refused as `rate_limited` until Anis staff restart the enrolment. ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `invitationId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Authorised with `Authorization: Enrollment `; not signed. Headers: `Accept-Language` (optional). The SDKs set all of them for you. **Body:** [EnrollmentProofRequest](#type-enrollmentproofrequest), as JSON. | Field | Type | Notes | | --- | --- | --- | | `keyId` (required) | UUID | The key id from the key submission. | | `challengeGeneration` (required) | integer | The challenge generation you are answering. | | `signature` (required) | string | ECDSA P-256/SHA-256 over the proof message, 64 bytes (not DER), base64url without padding. | ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [EnrollmentStatus](#type-enrollmentstatus) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 409 | Refused: conflicts with the current state. | [Problem](#refusals) | | 422 | Refused: the request breaks a rule. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invitation invalid](https://developers.anis.ly/errors/invitation-invalid.md) | `invitation_invalid` | 401 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Validation failed](https://developers.anis.ly/errors/validation-failed.md) | `validation_failed` | 422 | | [Challenge replaced](https://developers.anis.ly/errors/challenge-expired.md) | `challenge_expired` | 409 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### EnrollmentProofRequest | Field | Type | Notes | | --- | --- | --- | | `keyId` (required) | UUID | The key id from the key submission. | | `challengeGeneration` (required) | integer | The challenge generation you are answering. | | `signature` (required) | string | ECDSA P-256/SHA-256 over the proof message, 64 bytes (not DER), base64url without padding. | ### EnrollmentStatus | Field | Type | Notes | | --- | --- | --- | | `keyId` | UUID | Your key id. | | `challengeGeneration` | integer | The current challenge generation. | | `proofState` | string | `pending`, `accepted` or `failed` — a proof that did not verify or came too late. After five failed proofs, ask Anis staff to restart the enrolment. | | `approvalState` | string | `pending` while Anis staff have not confirmed the key, `approved` once it is active, `notApplicable` otherwise. | | `state` | string | `pendingProof`, `pendingApproval`, `active` — the key signs requests — or `unavailable` (revoked, expired or replaced). | | `expiresAt` | string (date-time) | When the current step expires. | --- --- title: "Read enrolment status — Anis Developers" description: "Where the key stands: waiting for proof, waiting for Anis staff, active, or no longer available." url: https://developers.anis.ly/reference/read-enrollment-status/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Enrolment # Read enrolment status `GET /v1/enrollments/{invitationId}/status` Where the key stands: waiting for proof, waiting for Anis staff, active, or no longer available. - **Permission:** None - **Authentication:** Enrolment token - **Staff-set limits it counts toward:** None - **.NET SDK:** `enrollment.GetStatusAsync()` ## Parameters | Name | In | Type | Notes | | --- | --- | --- | --- | | `invitationId` (required) | `path` | UUID | A UUID, lower-case with hyphens. | ## Request Authorised with `Authorization: Enrollment `; not signed. Headers: `Accept-Language` (optional). The SDKs set all of them for you. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [EnrollmentStatus](#type-enrollmentstatus) | | 401 | Refused: not authenticated. | [Problem](#refusals) | | 404 | Refused: not found, or not yours. | [Problem](#refusals) | | 429 | Refused: a limit was reached. | [Problem](#refusals) | | 503 | No decision: a dependency was unavailable. | [Problem](#refusals) | Every answer is signed by Anis; the SDKs check it before you see it. ## Refusals Every refusal is a signed problem. Branch on its `code`; each links to what it means and what to do. | Error | Code | Status | | --- | --- | --- | | [Invitation invalid](https://developers.anis.ly/errors/invitation-invalid.md) | `invitation_invalid` | 401 | | [Not found](https://developers.anis.ly/errors/resource-not-found.md) | `resource_not_found` | 404 | | [Rate limited](https://developers.anis.ly/errors/rate-limited.md) | `rate_limited` | 429 | | [Service unavailable](https://developers.anis.ly/errors/dependency-unavailable.md) | `dependency_unavailable` | 503 | | [Request timeout](https://developers.anis.ly/errors/request-timeout.md) | `request_timeout` | 504 | | [Internal error](https://developers.anis.ly/errors/internal-error.md) | `internal_error` | 500 | ## Types ### EnrollmentStatus | Field | Type | Notes | | --- | --- | --- | | `keyId` | UUID | Your key id. | | `challengeGeneration` | integer | The current challenge generation. | | `proofState` | string | `pending`, `accepted` or `failed` — a proof that did not verify or came too late. After five failed proofs, ask Anis staff to restart the enrolment. | | `approvalState` | string | `pending` while Anis staff have not confirmed the key, `approved` once it is active, `notApplicable` otherwise. | | `state` | string | `pendingProof`, `pendingApproval`, `active` — the key signs requests — or `unavailable` (revoked, expired or replaced). | | `expiresAt` | string (date-time) | When the current step expires. | --- --- title: "Anis’s public signing keys — Anis Developers" description: "Every public key that may sign Anis’s answers — including the next one before a rotation and the previous one after it. The only route that needs no signature. Clients cache it and fetch it again when an answer names a key they have not seen." url: https://developers.anis.ly/reference/signing-keys/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [API reference](https://developers.anis.ly/reference.md) · Anis signing keys # Anis’s public signing keys `GET /.well-known/partner-signing-keys.json` Every public key that may sign Anis’s answers — including the next one before a rotation and the previous one after it. The only route that needs no signature. Clients cache it and fetch it again when an answer names a key they have not seen. - **Permission:** None - **Authentication:** Public - **Staff-set limits it counts toward:** None - **.NET SDK:** `Fetched and cached by the SDK` ## Request No authentication. **Body:** none. ## Responses | Status | Meaning | Body | | --- | --- | --- | | 200 | Success. | [SigningKeySet](#type-signingkeyset) | ## Types ### SigningKeySet | Field | Type | Notes | | --- | --- | --- | | `keys` (required) | list of [PublicJwk](#type-publicjwk) | Every public key that may sign an answer — including the next one before a rotation and the previous one after it. | ### PublicJwk | Field | Type | Notes | | --- | --- | --- | | `kty` (required) | string | Always `EC`. Values: `EC` | | `crv` (required) | string | Always `P-256`. Values: `P-256` | | `x` (required) | string | The key’s x coordinate: exactly 32 bytes, base64url without padding. | | `y` (required) | string | The key’s y coordinate: exactly 32 bytes, base64url without padding. | | `kid` | string | In Anis’s signing keys: the key an answer’s signature names. Treat it as an opaque name. Not sent when you submit your own key. | | `use` | string | Intended use, when published. | | `alg` | string | Algorithm, when published. | --- --- title: "Invalid credentials — Anis Developers" description: "Anis could not authenticate the call: the key id is unknown, the key is not active (not yet confirmed, revoked or expired), the signature does not match the request, the signature is too old or signed on a clock that is out, or the body does not match its `Content-Digest`. All of these get this same answer on purpose, so a refusal never tells anyone which one to work around." url: https://developers.anis.ly/errors/invalid-credentials/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Keys and signatures # Invalid credentials - **Code:** `invalid_credentials` - **Status:** 401 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `InvalidCredentialsException` ## What it means Anis could not authenticate the call: the key id is unknown, the key is not active (not yet confirmed, revoked or expired), the signature does not match the request, the signature is too old or signed on a clock that is out, or the body does not match its `Content-Digest`. All of these get this same answer on purpose, so a refusal never tells anyone which one to work around. ## What to do - Check that the key id and the private key you sign with belong together. - Check that the key is active: the enrolment status reports `active` once Anis staff confirm it. - Check your server clock, and sign each attempt afresh — never reuse an old signature. - Compute the `Content-Digest` over the exact bytes you send. - Run the signature self-check: it reports exactly what Anis saw. If the self-check is refused too, the key itself is the problem. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/invalid-credentials", "title": "Invalid credentials", "status": 401, "code": "invalid_credentials", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Signature expired — Anis Developers" description: "Reserved. Anis does not send this code today: a signature that is too old, or signed on a clock that is out, is answered `invalid_credentials` — on purpose, so a refusal never says which part of a signature failed." url: https://developers.anis.ly/errors/signature-expired/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Keys and signatures # Signature expired - **Code:** `signature_expired` - **Status:** 401 - **If this was an order:** This request bought nothing - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `InvalidCredentialsException` ## What it means Reserved. Anis does not send this code today: a signature that is too old, or signed on a clock that is out, is answered `invalid_credentials` — on purpose, so a refusal never says which part of a signature failed. ## What to do - Handle it like [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) if you ever receive it. - Keep your server clock synchronised (NTP), and sign every attempt afresh. ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/signature-expired", "title": "Signature expired", "status": 401, "code": "signature_expired", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Invalid content digest — Anis Developers" description: "Reserved. Anis does not send this code today: a body that does not match its `Content-Digest` is answered `invalid_credentials`." url: https://developers.anis.ly/errors/invalid-content-digest/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Keys and signatures # Invalid content digest - **Code:** `invalid_content_digest` - **Status:** 400 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AnisApiException` ## What it means Reserved. Anis does not send this code today: a body that does not match its `Content-Digest` is answered `invalid_credentials`. ## What to do - Handle it like [`invalid_credentials`](https://developers.anis.ly/errors/invalid-credentials.md) if you ever receive it. - Compute the digest over the exact bytes you send, and check that no proxy re-encodes request bodies. ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/invalid-content-digest", "title": "Invalid content digest", "status": 400, "code": "invalid_content_digest", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Replay detected — Anis Developers" description: "The request reused a nonce your application had already used. The SDKs make a fresh nonce for every attempt, so this usually means something between you and Anis — a proxy, a load balancer or an automatic retry — sent the same request twice. This copy was refused; the copy that arrived first may have been processed." url: https://developers.anis.ly/errors/replay-detected/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Keys and signatures # Replay detected - **Code:** `replay_detected` - **Status:** 409 - **If this was an order:** May still complete — resume with the same id - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `ReplayDetectedException` > **The order may still complete** > > Resume it with the **same** operation id and the same body. A new operation id can buy the cards a second time. ## What it means The request reused a nonce your application had already used. The SDKs make a fresh nonce for every attempt, so this usually means something between you and Anis — a proxy, a load balancer or an automatic retry — sent the same request twice. This copy was refused; the copy that arrived first may have been processed. ## What to do - On an order: the first copy may have bought the cards. Resume with the SAME operation id and the same body — never a new id. - On any other call: send it again. A new signature carries a new nonce. - Find what re-sends your requests and switch it off for Anis calls. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/replay-detected", "title": "Replay detected", "status": 409, "code": "replay_detected", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Source address not allowed — Anis Developers" description: "Anis’s own network protection refused the address the call came from. This is not about your application’s allowed networks — a call from outside those is answered `insufficient_scope`." url: https://developers.anis.ly/errors/source-ip-not-allowed/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Source address not allowed - **Code:** `source_ip_not_allowed` - **Status:** 403 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means Anis’s own network protection refused the address the call came from. This is not about your application’s allowed networks — a call from outside those is answered `insufficient_scope`. ## What to do - Contact Anis with the request id. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/source-ip-not-allowed", "title": "Source address not allowed", "status": 403, "code": "source_ip_not_allowed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Insufficient scope — Anis Developers" description: "Your application may not make this call: its permissions do not include the one this route needs, or the call came from an address outside the application’s allowed networks. Both get this same answer on purpose." url: https://developers.anis.ly/errors/insufficient-scope/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Insufficient scope - **Code:** `insufficient_scope` - **Status:** 403 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means Your application may not make this call: its permissions do not include the one this route needs, or the call came from an address outside the application’s allowed networks. Both get this same answer on purpose. ## What to do - Read your profile: it lists the permissions your application has right now. - Check that you call from one of the networks agreed with Anis. - Ask Anis staff to change either; the change applies to your next call. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/insufficient-scope", "title": "Insufficient scope", "status": 403, "code": "insufficient_scope", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Account not authorised — Anis Developers" description: "The Anis business account behind your application is not in a state that allows partner access — for example it is not an active account with a Business subscription." url: https://developers.anis.ly/errors/binding-not-authorized/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Account not authorised - **Code:** `binding_not_authorized` - **Status:** 403 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The Anis business account behind your application is not in a state that allows partner access — for example it is not an active account with a Business subscription. ## What to do - Contact Anis to restore the account; retrying does not help until then. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/binding-not-authorized", "title": "Account not authorised", "status": 403, "code": "binding_not_authorized", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Not found — Anis Developers" description: "What you asked for does not exist, or it is not yours. Both get the same answer on purpose — for example an order id that belongs to another application looks exactly like one that was never used." url: https://developers.anis.ly/errors/resource-not-found/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · The request itself # Not found - **Code:** `resource_not_found` - **Status:** 404 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `ResourceNotFoundException` ## What it means What you asked for does not exist, or it is not yours. Both get the same answer on purpose — for example an order id that belongs to another application looks exactly like one that was never used. ## What to do - Check the id. For an order, use the operation id you sent when you placed it. ## Where you can meet it - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/resource-not-found", "title": "Not found", "status": 404, "code": "resource_not_found", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Card not found — Anis Developers" description: "The sold card is not in this wallet, or it is not yours. Both get the same answer." url: https://developers.anis.ly/errors/card-not-found/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · The request itself # Card not found - **Code:** `card_not_found` - **Status:** 404 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `ResourceNotFoundException` ## What it means The sold card is not in this wallet, or it is not yours. Both get the same answer. ## What to do - Check the sold-card id and the wallet it was bought from; the owned-cards list shows both. ## Where you can meet it - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/card-not-found", "title": "Card not found", "status": 404, "code": "card_not_found", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Wallet not granted — Anis Developers" description: "The wallet in the address is not granted to your application — or does not exist. Both get the same answer, so the refusal never reveals which wallets exist." url: https://developers.anis.ly/errors/wallet-not-granted/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · The request itself # Wallet not granted - **Code:** `wallet_not_granted` - **Status:** 404 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `ResourceNotFoundException` ## What it means The wallet in the address is not granted to your application — or does not exist. Both get the same answer, so the refusal never reveals which wallets exist. ## What to do - Use a wallet id from the wallets list, which shows exactly the wallets your application may use. - Ask Anis staff to grant the wallet if you need it. ## Where you can meet it - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/wallet-not-granted", "title": "Wallet not granted", "status": 404, "code": "wallet_not_granted", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Validation failed — Anis Developers" description: "The request broke a rule of the API. The answer never names the field, on purpose, so it cannot be used to probe for valid ids or prices." url: https://developers.anis.ly/errors/validation-failed/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · The request itself # Validation failed - **Code:** `validation_failed` - **Status:** 422 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `ValidationFailedException` ## What it means The request broke a rule of the API. The answer never names the field, on purpose, so it cannot be used to probe for valid ids or prices. ## What to do - Prices must be decimal strings with exactly three decimals, and the total must be the unit price times the quantity, exactly. - `externalReference` must be 1 to 100 characters: Latin letters and digits, space and `- _ . : / #`. - Pass a paging cursor back exactly as received. - A reveal takes no body at all; the signature self-check takes exactly `{}`. ## Where you can meet it - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/validation-failed", "title": "Validation failed", "status": 422, "code": "validation_failed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Operation id already used — Anis Developers" description: "This operation id was already used with a DIFFERENT order body. This request was not placed. The order stored under that id is untouched — and if you were resuming it, it may have completed." url: https://developers.anis.ly/errors/idempotency-conflict/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Operation id already used - **Code:** `idempotency_conflict` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `IdempotencyConflictException` ## What it means This operation id was already used with a DIFFERENT order body. This request was not placed. The order stored under that id is untouched — and if you were resuming it, it may have completed. ## What to do - If you meant to resume: send exactly the body you stored with that id — same card, quantity, prices and reference. - If this is a genuinely new purchase: use a new operation id. - One id must always mean one purchase with one body. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/idempotency-conflict", "title": "Operation id already used", "status": 409, "code": "idempotency_conflict", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Order still processing — Anis Developers" description: "The order was accepted and has no outcome yet." url: https://developers.anis.ly/errors/operation-processing/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Order still processing - **Code:** `operation_processing` - **Status:** 202 - **If this was an order:** May still complete — resume with the same id - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `OrderProcessing (a result, not an exception)` > **The order may still complete** > > Resume it with the **same** operation id and the same body. A new operation id can buy the cards a second time. ## What it means The order was accepted and has no outcome yet. ## What to do - Wait for the time in `Retry-After`, then send the SAME order again — same operation id, same body. That is what moves it forward. - Reading the order’s status shows where it stands, but does not move it forward. - Never place it again under a new id: that can buy the cards twice. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/operation-processing", "title": "Order still processing", "status": 202, "code": "operation_processing", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Allowed-debt consent required — Anis Developers" description: "The wallet can cover this purchase only by using the account’s allowed debt, and the order did not consent to that. Debt is never used without an explicit yes." url: https://developers.anis.ly/errors/allowed-debt-consent-required/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Allowed-debt consent required - **Code:** `allowed_debt_consent_required` - **Status:** 402 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AnisApiException` ## What it means The wallet can cover this purchase only by using the account’s allowed debt, and the order did not consent to that. Debt is never used without an explicit yes. ## What to do - Place a new order with `useAllowedDebt: true` — only if you mean to spend debt — under a new operation id. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/allowed-debt-consent-required", "title": "Allowed-debt consent required", "status": 402, "code": "allowed_debt_consent_required", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Insufficient balance — Anis Developers" description: "The wallet cannot cover the purchase. Nothing was charged, and the order is closed: sending the same operation id again returns this same answer." url: https://developers.anis.ly/errors/insufficient-balance/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Insufficient balance - **Code:** `insufficient_balance` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `InsufficientBalanceException` ## What it means The wallet cannot cover the purchase. Nothing was charged, and the order is closed: sending the same operation id again returns this same answer. ## What to do - Top up the wallet, then place a new order with a new operation id. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/insufficient-balance", "title": "Insufficient balance", "status": 409, "code": "insufficient_balance", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Purchase not allowed — Anis Developers" description: "A rule on the Anis business account forbids this purchase." url: https://developers.anis.ly/errors/purchase-not-allowed/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Purchase not allowed - **Code:** `purchase_not_allowed` - **Status:** 403 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means A rule on the Anis business account forbids this purchase. ## What to do - Contact Anis about the account’s purchase rules; send a new order only once the purchase is allowed. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/purchase-not-allowed", "title": "Purchase not allowed", "status": 403, "code": "purchase_not_allowed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Order refused — Anis Developers" description: "The purchase conflicts with the current state of the Anis business account, and it was refused. It carries the code `purchase_not_allowed`." url: https://developers.anis.ly/errors/order-refused/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Order refused - **Code:** `purchase_not_allowed` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The purchase conflicts with the current state of the Anis business account, and it was refused. It carries the code `purchase_not_allowed`. ## What to do - Place a new order with a new operation id only after the account’s state has changed. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/order-refused", "title": "Order refused", "status": 409, "code": "purchase_not_allowed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Spending allowance used up — Anis Developers" description: "The Anis business account’s spending allowance is used up. This is not a request rate: waiting does not reset it." url: https://developers.anis.ly/errors/owner-limit-exceeded/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Limits # Spending allowance used up - **Code:** `owner_limit_exceeded` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `LimitExceededException` ## What it means The Anis business account’s spending allowance is used up. This is not a request rate: waiting does not reset it. ## What to do - Do not retry in a loop. - Place a new order, with a new operation id, only once you know the allowance has changed. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/owner-limit-exceeded", "title": "Spending allowance used up", "status": 409, "code": "owner_limit_exceeded", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Daily limit reached — Anis Developers" description: "The Anis business account’s daily purchase limit is reached. No reset time is given." url: https://developers.anis.ly/errors/daily-limit-exceeded/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Limits # Daily limit reached - **Code:** `daily_limit_exceeded` - **Status:** 429 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `LimitExceededException` ## What it means The Anis business account’s daily purchase limit is reached. No reset time is given. ## What to do - Do not retry in a loop. - Place a new order, with a new operation id, once you know the limit has cleared. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/daily-limit-exceeded", "title": "Daily limit reached", "status": 429, "code": "daily_limit_exceeded", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Rate limited — Anis Developers" description: "Too many requests in the current window. The limit is either one Anis staff set on your application or wallet — counting all requests, orders only, or reveals only — or Anis’s own protection. Nothing was recorded against the request. During enrolment it also means the proof has failed five times." url: https://developers.anis.ly/errors/rate-limited/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Limits # Rate limited - **Code:** `rate_limited` - **Status:** 429 - **If this was an order:** This request bought nothing - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `RateLimitedException` ## What it means Too many requests in the current window. The limit is either one Anis staff set on your application or wallet — counting all requests, orders only, or reveals only — or Anis’s own protection. Nothing was recorded against the request. During enrolment it also means the proof has failed five times. ## What to do - Wait for the time in `Retry-After` when it is present; otherwise back off exponentially. - Then send again. An order may keep its operation id: a refusal at the door records nothing against it. - During enrolment, after five failed proofs: ask Anis staff to restart the enrolment. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/rate-limited", "title": "Rate limited", "status": 429, "code": "rate_limited", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Wallet disabled — Anis Developers" description: "The wallet is disabled, so it cannot be used right now." url: https://developers.anis.ly/errors/wallet-disabled/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Wallet disabled - **Code:** `wallet_disabled` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The wallet is disabled, so it cannot be used right now. ## What to do - Contact Anis; send a new request once the wallet is enabled again. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/wallet-disabled", "title": "Wallet disabled", "status": 409, "code": "wallet_disabled", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Wallet expired — Anis Developers" description: "The wallet can no longer be used because the account or its Business subscription has expired." url: https://developers.anis.ly/errors/wallet-expired/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Wallet expired - **Code:** `wallet_expired` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The wallet can no longer be used because the account or its Business subscription has expired. ## What to do - Contact Anis to resolve the expiry, then send a new request. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/wallet-expired", "title": "Wallet expired", "status": 409, "code": "wallet_expired", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Business subscription required — Anis Developers" description: "The Anis business account behind your application has no active Business subscription." url: https://developers.anis.ly/errors/business-subscription-required/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Business subscription required - **Code:** `business_subscription_required` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The Anis business account behind your application has no active Business subscription. ## What to do - Restore the Business subscription with Anis, then send a new request. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/business-subscription-required", "title": "Business subscription required", "status": 409, "code": "business_subscription_required", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Account inactive — Anis Developers" description: "The Anis business account behind your application is inactive." url: https://developers.anis.ly/errors/account-inactive/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Permissions and account # Account inactive - **Code:** `account_inactive` - **Status:** 403 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means The Anis business account behind your application is inactive. ## What to do - Contact Anis to confirm or re-enable the account, then send a new request. ## Where you can meet it - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/account-inactive", "title": "Account inactive", "status": 403, "code": "account_inactive", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Currency not supported — Anis Developers" description: "The currency in the request is not the wallet’s currency." url: https://developers.anis.ly/errors/currency-not-supported/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · The request itself # Currency not supported - **Code:** `currency_not_supported` - **Status:** 422 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `ValidationFailedException` ## What it means The currency in the request is not the wallet’s currency. ## What to do - Send the prices in the currency the catalogue shows for this wallet. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/currency-not-supported", "title": "Currency not supported", "status": 422, "code": "currency_not_supported", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Card unavailable — Anis Developers" description: "The card cannot be sold to this wallet now — it is no longer offered, or it does not exist." url: https://developers.anis.ly/errors/card-unavailable/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Card unavailable - **Code:** `card_unavailable` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `OutOfStockException` ## What it means The card cannot be sold to this wallet now — it is no longer offered, or it does not exist. ## What to do - Read the catalogue again and choose a card it offers; place a new order with a new operation id. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/card-unavailable", "title": "Card unavailable", "status": 409, "code": "card_unavailable", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Quantity unavailable — Anis Developers" description: "The card cannot be sold in this quantity: it is sold out, there is not enough stock, or the quantity is outside the card’s own minimum or maximum." url: https://developers.anis.ly/errors/quantity-unavailable/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Quantity unavailable - **Code:** `quantity_unavailable` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `OutOfStockException` ## What it means The card cannot be sold in this quantity: it is sold out, there is not enough stock, or the quantity is outside the card’s own minimum or maximum. ## What to do - Read the catalogue again and place a new order with an available quantity and a new operation id. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/quantity-unavailable", "title": "Quantity unavailable", "status": 409, "code": "quantity_unavailable", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Price changed — Anis Developers" description: "The price you sent is not the price this wallet pays now. Nothing was charged. The price checked is the card’s `unitPrice` for this wallet — the other prices in the catalogue are for display." url: https://developers.anis.ly/errors/price-changed/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Orders # Price changed - **Code:** `price_changed` - **Status:** 409 - **If this was an order:** This request bought nothing - **Send it again unchanged?** No — change something first - **.NET SDK:** `PriceChangedException` ## What it means The price you sent is not the price this wallet pays now. Nothing was charged. The price checked is the card’s `unitPrice` for this wallet — the other prices in the catalogue are for display. ## What to do - Read the catalogue again and place a new order at the current `unitPrice`, under a new operation id. ## Where you can meet it - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/price-changed", "title": "Price changed", "status": 409, "code": "price_changed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Reveal not allowed — Anis Developers" description: "This card’s codes cannot be shown in its current state." url: https://developers.anis.ly/errors/reveal-not-allowed/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Revealing cards # Reveal not allowed - **Code:** `reveal_not_allowed` - **Status:** 409 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `AuthorizationException` ## What it means This card’s codes cannot be shown in its current state. ## What to do - Do not retry until you know the card’s state has changed. Contact Anis with the request id if you think it should be revealable. ## Where you can meet it - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/reveal-not-allowed", "title": "Reveal not allowed", "status": 409, "code": "reveal_not_allowed", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Invoice too large to reveal — Anis Developers" description: "Revealing a whole invoice is limited to 100 cards, and this invoice has more." url: https://developers.anis.ly/errors/invoice-reveal-limit-exceeded/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Revealing cards # Invoice too large to reveal - **Code:** `invoice_reveal_limit_exceeded` - **Status:** 409 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `AnisApiException` ## What it means Revealing a whole invoice is limited to 100 cards, and this invoice has more. ## What to do - Reveal the cards you need one by one. ## Where you can meet it - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/invoice-reveal-limit-exceeded", "title": "Invoice too large to reveal", "status": 409, "code": "invoice_reveal_limit_exceeded", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Invitation invalid — Anis Developers" description: "The invitation or its token is unknown, already used or expired. An invitation is valid for about a day by default." url: https://developers.anis.ly/errors/invitation-invalid/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Enrolling a key # Invitation invalid - **Code:** `invitation_invalid` - **Status:** 401 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `EnrollmentRefusedException` ## What it means The invitation or its token is unknown, already used or expired. An invitation is valid for about a day by default. ## What to do - Ask Anis staff for a new invitation. ## Where you can meet it - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/invitation-invalid", "title": "Invitation invalid", "status": 401, "code": "invitation_invalid", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Challenge replaced — Anis Developers" description: "The proof answered a challenge that has been replaced: Anis staff restarted the enrolment, which issues a new invitation and a new challenge. (A proof that is simply too late is not refused — it comes back with `proofState: \"failed\"`.)" url: https://developers.anis.ly/errors/challenge-expired/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Enrolling a key # Challenge replaced - **Code:** `challenge_expired` - **Status:** 409 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `EnrollmentRefusedException` ## What it means The proof answered a challenge that has been replaced: Anis staff restarted the enrolment, which issues a new invitation and a new challenge. (A proof that is simply too late is not refused — it comes back with `proofState: "failed"`.) ## What to do - Use the new invitation token Anis staff sent, and enrol again from the key submission. - If your key lives in a vault that needs an approval to sign, arrange that approval before you submit the key: the proof must follow within about 30 minutes. ## Where you can meet it - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/challenge-expired", "title": "Challenge replaced", "status": 409, "code": "challenge_expired", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Public key invalid — Anis Developers" description: "The public key you submitted is not a usable P-256 public key. (A proof of possession that does not verify is not refused — it comes back with `proofState: \"failed\"`.)" url: https://developers.anis.ly/errors/key-proof-invalid/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Enrolling a key # Public key invalid - **Code:** `key_proof_invalid` - **Status:** 422 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `EnrollmentRefusedException` ## What it means The public key you submitted is not a usable P-256 public key. (A proof of possession that does not verify is not refused — it comes back with `proofState: "failed"`.) ## What to do - Send only the public half: `kty` `EC`, `crv` `P-256`, and `x` and `y` of exactly 32 bytes each, base64url without padding. - Do not send a private member (`d`). ## Where you can meet it - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/key-proof-invalid", "title": "Public key invalid", "status": 422, "code": "key_proof_invalid", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Invitation already used — Anis Developers" description: "This invitation has already taken a key — for example, the key submission was sent again after its answer was lost. An invitation takes exactly one key." url: https://developers.anis.ly/errors/key-duplicate/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · Enrolling a key # Invitation already used - **Code:** `key_duplicate` - **Status:** 409 - **If this was an order:** Never returned when buying - **Send it again unchanged?** No — change something first - **.NET SDK:** `EnrollmentRefusedException` ## What it means This invitation has already taken a key — for example, the key submission was sent again after its answer was lost. An invitation takes exactly one key. ## What to do - Ask Anis staff to restart the enrolment; it issues a new invitation. A new key pair on the same invitation is refused the same way. - Next time, save the key submission’s answer (it carries the challenge) before anything else. ## Where you can meet it - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/key-duplicate", "title": "Invitation already used", "status": 409, "code": "key_duplicate", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Service unavailable — Anis Developers" description: "Anis could not reach a decision because something it depends on was unavailable." url: https://developers.anis.ly/errors/dependency-unavailable/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · No decision reached # Service unavailable - **Code:** `dependency_unavailable` - **Status:** 503 - **If this was an order:** May still complete — resume with the same id - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `DependencyUnavailableException` > **The order may still complete** > > Resume it with the **same** operation id and the same body. A new operation id can buy the cards a second time. ## What it means Anis could not reach a decision because something it depends on was unavailable. ## What to do - On an order: the purchase may still complete. Resume with the SAME operation id and the same body after a short wait — never a new id. - On a read: retry after a short wait. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/dependency-unavailable", "title": "Service unavailable", "status": 503, "code": "dependency_unavailable", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Request timeout — Anis Developers" description: "Anis could not reach a decision in time." url: https://developers.anis.ly/errors/request-timeout/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · No decision reached # Request timeout - **Code:** `request_timeout` - **Status:** 504 - **If this was an order:** May still complete — resume with the same id - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `DependencyUnavailableException` > **The order may still complete** > > Resume it with the **same** operation id and the same body. A new operation id can buy the cards a second time. ## What it means Anis could not reach a decision in time. ## What to do - On an order: the purchase may still complete. Resume with the SAME operation id and the same body — never a new id. - On a read: retry after a short wait. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/request-timeout", "title": "Request timeout", "status": 504, "code": "request_timeout", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ``` --- --- title: "Internal error — Anis Developers" description: "Something failed inside Anis before a decision was reached. If the answer is marked `Idempotency-Replayed: true`, it is instead the recorded answer of an order that is already closed — nothing was bought." url: https://developers.anis.ly/errors/internal-error/ language: en --- > Every page of this documentation: https://developers.anis.ly/llms.txt [Error codes](https://developers.anis.ly/errors.md) · No decision reached # Internal error - **Code:** `internal_error` - **Status:** 500 - **If this was an order:** May still complete — resume with the same id - **Send it again unchanged?** Yes — see What to do - **.NET SDK:** `DependencyUnavailableException` > **The order may still complete** > > Resume it with the **same** operation id and the same body. A new operation id can buy the cards a second time. ## What it means Something failed inside Anis before a decision was reached. If the answer is marked `Idempotency-Replayed: true`, it is instead the recorded answer of an order that is already closed — nothing was bought. ## What to do - On an order: resume with the SAME operation id after a short wait — unless the answer is marked replayed, in which case the order is closed. - On a read: retry after a short wait. - If it persists, contact Anis with the request id. ## Where you can meet it - GET [Read your profile](https://developers.anis.ly/reference/read-profile.md) `/v1/profile` - GET [List wallets](https://developers.anis.ly/reference/list-wallets.md) `/v1/wallets` - GET [Read a wallet](https://developers.anis.ly/reference/read-wallet.md) `/v1/wallets/{walletId}` - GET [List catalogue categories](https://developers.anis.ly/reference/list-categories.md) `/v1/wallets/{walletId}/catalog/categories` - GET [List subcategories of a category](https://developers.anis.ly/reference/list-subcategories.md) `/v1/wallets/{walletId}/catalog/categories/{categoryId}/subcategories` - GET [Read a subcategory](https://developers.anis.ly/reference/read-subcategory.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}` - GET [List cards for sale](https://developers.anis.ly/reference/list-cards.md) `/v1/wallets/{walletId}/catalog/subcategories/{subcategoryId}/cards` - POST [Place an order](https://developers.anis.ly/reference/create-order.md) `/v1/wallets/{walletId}/orders` - GET [Read an order](https://developers.anis.ly/reference/read-order.md) `/v1/orders/{operationId}` - GET [List owned cards](https://developers.anis.ly/reference/list-owned-cards.md) `/v1/wallets/{walletId}/cards` - GET [Read an owned card](https://developers.anis.ly/reference/read-owned-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}` - POST [Reveal a card](https://developers.anis.ly/reference/reveal-card.md) `/v1/wallets/{walletId}/cards/{soldCardId}/reveal` - POST [Reveal an invoice](https://developers.anis.ly/reference/reveal-invoice.md) `/v1/wallets/{walletId}/invoices/{invoiceId}/cards/reveal` - POST [Check your signature](https://developers.anis.ly/reference/check-signature.md) `/v1/diagnostics/signature` - GET [Read an invitation](https://developers.anis.ly/reference/read-invitation.md) `/v1/enrollments/{invitationId}` - POST [Submit a public key](https://developers.anis.ly/reference/submit-key.md) `/v1/enrollments/{invitationId}/keys` - POST [Prove you hold the key](https://developers.anis.ly/reference/submit-proof.md) `/v1/enrollments/{invitationId}/proof` - GET [Read enrolment status](https://developers.anis.ly/reference/read-enrollment-status.md) `/v1/enrollments/{invitationId}/status` ## Example The body of the refusal. Its `type` is the address of this page. ``` { "type": "https://developers.anis.ly/errors/internal-error", "title": "Internal error", "status": 500, "code": "internal_error", "requestId": "01J9R2K8T4V6XQ0M3B7C5D9E1F" } ```