{
  "openapi": "3.1.0",
  "info": {
    "title": "Animica Pay Merchant API",
    "version": "10.1.0",
    "summary": "Accept ANM payments: payment intents, refunds, balance, hosted checkout and HMAC-signed webhooks.",
    "description": "The merchant REST API behind https://pay.animica.dev — hosted checkout, payment links, invoices and POS for the Animica chain (L1 chain id 1, addresses bech32m `anim1…`, signatures ML-DSA-65).\n\n## Money\n- 1 ANM = 10^9 base units (nano-ANM, `nANM`). API responses report `anm_amount`, `merchant_amount` and `protocol_fee` as **integer nANM decimal strings**, plus `*_formatted` display strings.\n- **All amounts are JSON strings, never JSON numbers.** A JSON number loses precision before it reaches the server and is refused with `amount_must_be_string`.\n- `amount` is a human quantity of the stated `currency` (`\"100\"` with currency `ANM` means 100 ANM, `\"49.99\"` with `USD` means dollars). To specify base units exactly, send `amount_nanm` instead. Sending both is a 400.\n- Fee: **2.00% (200 bps)** per successful payment, floored, with `merchant_amount + protocol_fee == anm_amount` exactly. The fee is not refundable.\n\n## Authentication\nEvery `/api/v1` route requires a **secret** API key (`ask_live_…` / `ask_test_…`) as `Authorization: Bearer <key>`. Publishable keys (`apk_…`) are refused. Keys are never accepted from cookies. Test-mode keys never move real ANM.\n\n## Idempotency\n`Idempotency-Key` is **required** on payment-intent creation. A retry with the same key and body replays the original response (with header `idempotent-replay: true`); the same key with a different body is a 409 `idempotency_key_reused`.\n\n## Settlement\nA payment is `PAID` only after a successful execution receipt at the required confirmation depth (currently 12). Inclusion in a block is not settlement on this chain, and a reorg can return a settled intent to `AWAITING_PAYMENT` (webhook `payment.reorged`). Fulfil orders from webhooks, not from anything observable in the browser.\n\n## Rate limits\nPer key: 100 requests/min (burst 100). Per IP: 300 requests/min. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; a 429 carries `Retry-After` (seconds).\n\n## Webhooks\nDeliveries are HMAC-SHA256 signed. The signed string is `v1:<unix_seconds>:<event_id>:<raw_body>` under your endpoint secret (`whsec_…`, shown once). Verify against the **raw** request body, compare in constant time, and reject timestamps older than 300 s. See the top-level `webhooks` section.\n\nHuman docs: https://pay.animica.dev/docs — machine index: https://pay.animica.dev/llms.txt",
    "contact": { "name": "Animica", "email": "ai@3vdc.com", "url": "https://animica.org" },
    "license": { "name": "Apache-2.0", "identifier": "Apache-2.0" }
  },
  "servers": [{ "url": "https://pay.animica.dev" }],
  "security": [{ "bearerApiKey": [] }],
  "tags": [
    { "name": "payment-intents", "description": "Create, list, fetch and cancel payment intents. One intent is satisfied by exactly one on-chain transaction, matched by payment reference + recipient — never by amount." },
    { "name": "refunds", "description": "A refund on this chain is a NEW merchant-signed transfer, never a reversal. Cap = captured gross; the 2% protocol fee is not refundable. Refused in test mode." },
    { "name": "balance", "description": "Settled vs pending merchant balance in nANM." },
    { "name": "public", "description": "Unauthenticated surfaces: health probe, hosted checkout pages and the embeddable widget. Checkout pages are documented for reference only — they serve HTML to customers, not JSON to integrations." }
  ],
  "paths": {
    "/api/v1/payment-intents": {
      "post": {
        "tags": ["payment-intents"],
        "operationId": "createPaymentIntent",
        "summary": "Create a payment intent",
        "description": "Server-priced: you state what to charge in your currency; the server decides the nANM amount, the 2.00% fee split, the payment address and the payment reference. Client-supplied `merchant_address`, `treasury_address`, `protocol_fee`, `merchant_amount` or `protocol_fee_bps` are refused with `field_not_settable`.\n\nWith `currency: \"USD\"` (or another fiat currency) the server locks an exchange-rate quote; if no rate source is configured the request is refused with `currency_unsupported` rather than priced by guesswork — send `currency: \"ANM\"` with an exact amount instead.\n\nRedirect the customer to `checkout_url` from the response. Default intent expiry is 30 minutes.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreatePaymentIntentRequest" },
              "examples": {
                "anm": {
                  "summary": "Charge 25 ANM",
                  "value": { "amount": "25", "currency": "ANM", "merchant_order_id": "19482", "description": "Order #19482" }
                },
                "usd": {
                  "summary": "Charge $49.99 (server locks the ANM rate)",
                  "value": { "amount": "49.99", "currency": "USD", "merchant_order_id": "19482", "description": "WooCommerce Order #19482", "metadata": { "platform": "woocommerce" } }
                },
                "base_units": {
                  "summary": "Charge exactly 25,000,000,000 nANM",
                  "value": { "amount_nanm": "25000000000", "currency": "ANM" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentIntent" } } }
          },
          "200": {
            "description": "Idempotent replay — the same `Idempotency-Key` and body were seen before; this is the original response.",
            "headers": {
              "idempotent-replay": { "description": "`true` when this response is a replay of the first attempt.", "schema": { "type": "string", "enum": ["true"] } }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentIntent" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": {
            "description": "`idempotency_key_reused` — this `Idempotency-Key` was already used with a different request body.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "tags": ["payment-intents"],
        "operationId": "listPaymentIntents",
        "summary": "List payment intents",
        "description": "Tenant- and mode-scoped to the API key. Cursor pagination via `starting_after`.",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "status", "in": "query", "schema": { "$ref": "#/components/schemas/PaymentIntentStatus" } },
          { "name": "starting_after", "in": "query", "description": "Intent id to page after.", "schema": { "type": "string" } },
          { "name": "merchant_order_id", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "A page of payment intents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "has_more", "data"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "has_more": { "type": "boolean" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/PaymentIntent" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/payment-intents/{id}": {
      "get": {
        "tags": ["payment-intents"],
        "operationId": "getPaymentIntent",
        "summary": "Fetch a payment intent",
        "description": "Another merchant's id returns 404 (never 403), so ids cannot be enumerated.",
        "parameters": [{ "$ref": "#/components/parameters/IntentId" }],
        "responses": {
          "200": { "description": "The payment intent.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentIntent" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/payment-intents/{id}/cancel": {
      "post": {
        "tags": ["payment-intents"],
        "operationId": "cancelPaymentIntent",
        "summary": "Cancel a payment intent",
        "description": "Only intents that have not reached a terminal or in-flight-settlement state can be cancelled; otherwise 409 `not_cancellable`.",
        "parameters": [{ "$ref": "#/components/parameters/IntentId" }],
        "responses": {
          "200": { "description": "The cancelled intent.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentIntent" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "`not_cancellable` — the intent is in a state that cannot be cancelled.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/refunds": {
      "post": {
        "tags": ["refunds"],
        "operationId": "createRefund",
        "summary": "Create a refund",
        "description": "Refundable states: `PAID` or `PARTIALLY_REFUNDED`; anything else is 409 `not_refundable`. Omit `amount`/`amount_nanm` to refund the full `merchant_amount` (what you received — the 2% fee is not refundable). A refund is a **new** transfer that you sign: the response carries `requires_merchant_signature` and an `unsigned_transfer` to sign from your payout address. Test mode is refused outright — a refund moves real value.",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKeyOptional" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateRefundRequest" },
              "examples": {
                "full": { "summary": "Full refund of the merchant amount", "value": { "payment_intent": "pay_X9m2ABC", "reason": "customer request" } },
                "partial": { "summary": "Partial refund of 5 ANM", "value": { "payment_intent": "pay_X9m2ABC", "amount": "5" } }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Refund created; signature may still be required.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refund" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "`not_refundable` (intent state) or `refund_refused` (e.g. over the refundable cap).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/refunds/{id}": {
      "get": {
        "tags": ["refunds"],
        "operationId": "getRefund",
        "summary": "Fetch a refund",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "The refund.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Refund" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/api/v1/balance": {
      "get": {
        "tags": ["balance"],
        "operationId": "getBalance",
        "summary": "Fetch merchant balance",
        "description": "Scoped to the key's mode. `settled` has confirmed on chain; `pending` is detected but not yet at the required depth — reported separately because treating pending as spendable is how a merchant over-ships.",
        "responses": {
          "200": { "description": "The balance.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": ["public"],
        "operationId": "healthz",
        "summary": "Health probe",
        "description": "Unauthenticated routing probe. Says only whether to send traffic here — no internal detail.",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } }
          },
          "503": { "description": "Degraded — do not route traffic here." }
        }
      }
    },
    "/c/{id}": {
      "get": {
        "tags": ["public"],
        "operationId": "checkoutPage",
        "summary": "Hosted checkout page (documentation only)",
        "description": "**Not a JSON API.** The customer-facing HTML checkout for a payment intent — this is the `checkout_url` returned by intent creation. Send customers here; do not scrape it. Live payment state is pushed to the open page over SSE at `/c/{id}/events`. With `?embed=1` it runs inside the widget iframe under a merchant-scoped `frame-ancestors` CSP.",
        "security": [],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Payment intent id." }],
        "responses": { "200": { "description": "HTML checkout page.", "content": { "text/html": {} } }, "404": { "description": "Unknown intent." } }
      }
    },
    "/c/{id}/events": {
      "get": {
        "tags": ["public"],
        "operationId": "checkoutEvents",
        "summary": "Checkout status stream (documentation only)",
        "description": "**Server-Sent Events** stream consumed by the hosted checkout page itself. Not a merchant integration surface — fulfil orders from signed webhooks, never from anything observable in a browser.",
        "security": [],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "SSE stream of intent state changes.", "content": { "text/event-stream": {} } } }
      }
    },
    "/widget.js": {
      "get": {
        "tags": ["public"],
        "operationId": "widgetJs",
        "summary": "Embeddable checkout widget (documentation only)",
        "description": "The `<script src=\"https://pay.animica.dev/widget.js\">` embed. Opens the hosted checkout for an intent id in a modal iframe and reports status to the merchant page over a strict postMessage protocol. `PAID` is the only status that fires `onSuccess` — and fulfilment still comes from the webhook, not the widget. Create the intent on your server with a secret key; never put a secret key in a page.",
        "security": [],
        "responses": { "200": { "description": "JavaScript, cacheable (ETag, max-age 300).", "content": { "application/javascript": {} } } }
      }
    }
  },
  "webhooks": {
    "payment-event": {
      "post": {
        "operationId": "webhookPaymentEvent",
        "summary": "Signed event delivery to your endpoint",
        "description": "POSTed to the webhook URL you register in the dashboard, signed with your endpoint secret (`whsec_…`, shown once).\n\n**Verification (mandatory before any order mutation):**\n1. Read the **raw** request body bytes — never re-serialize parsed JSON.\n2. Build `v1:<Animica-Timestamp>:<Animica-Event-ID>:<raw_body>`.\n3. Compute HMAC-SHA256 over that string with your secret; hex-encode.\n4. Constant-time compare against each `v1=<hex>` entry in `Animica-Signature` (several entries may be present during secret rotation — any one match verifies).\n5. Reject if `|now - Animica-Timestamp| > 300` seconds. The timestamp is the delivery time, and it is inside the signed string, so a captured body cannot be replayed with a fresh timestamp.\n\n**Idempotency:** the event `id` names the fact, not the delivery — the same settlement re-announced keeps the same `evt_…` id. Deduplicate on it; retries are normal.\n\n**`payment.confirmed` is the only success signal.** `payment.detected`/`payment.confirming` are progress, not settlement. `payment.reorged` means a previously confirmed settlement was reversed by a chain reorg — un-fulfil or hold the order.\n\nAnswer with any 2xx quickly; anything else is retried.",
        "parameters": [
          { "name": "Animica-Signature", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Comma-separated `v1=<64 lowercase hex chars>` entries (one per active secret during rotation)." },
          { "name": "Animica-Event-ID", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event id, e.g. `evt_…` — also inside the signed string and the body." },
          { "name": "Animica-Timestamp", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Unix seconds of the delivery attempt. Inside the signed string. Reject beyond ±300 s." }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEvent" },
              "example": {
                "id": "evt_5f2c1a9d3e8b47c6a0d1f2e3b4a5c6d7e8f90a1b2c3d4e5f",
                "type": "payment.confirmed",
                "created": 1765700000,
                "data": {
                  "id": "pay_X9m2ABC",
                  "status": "PAID",
                  "mode": "live",
                  "currency": "USD",
                  "merchant_order_id": "19482",
                  "amount_nanm": "41825250000",
                  "merchant_amount": "40988745000",
                  "protocol_fee": "836505000",
                  "protocol_fee_bps": 200,
                  "payment_reference": "ref_7Qk3…",
                  "transaction_hash": "0x…",
                  "block_height": 73170,
                  "confirmations": 12,
                  "required_confirmations": 12,
                  "reason": null
                }
              }
            }
          }
        },
        "responses": {
          "2XX": { "description": "Acknowledged. Any non-2xx (and any timeout) is retried; every retry is signed with a fresh delivery timestamp." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Secret API key from the dashboard: `ask_live_…` or `ask_test_…`. Secret keys only — publishable `apk_…` keys cannot call this API. Send only in the `Authorization` header; a key found in a cookie is refused. Keys are hashed at rest and shown once."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": { "type": "string", "maxLength": 255 },
        "description": "Required on intent creation. Use a stable per-order value (e.g. `woo_order_19482`) so a retry returns the existing intent instead of creating a second one. Same key + same body → replay of the original response (`idempotent-replay: true`); same key + different body → 409 `idempotency_key_reused`; first attempt still running → 429, retry after the indicated delay."
      },
      "IdempotencyKeyOptional": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string", "maxLength": 255 },
        "description": "Recommended: a stable per-refund value makes retries safe."
      },
      "IntentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Payment intent id, e.g. `pay_X9m2ABC`."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request. Codes include `idempotency_key_required`, `amount_must_be_string`, `invalid_amount`, `invalid_metadata`, `invalid_url`, `invalid_request`, `invalid_status`, `field_not_settable`, `currency_unsupported`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing, malformed or invalid API key — every rejection reason is the same opaque 401 by design.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "The account is suspended, or the operation would move real funds from a context that must not (e.g. a test-mode refund).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such resource for this merchant. Another merchant's id is also a 404, never a 403.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "TooManyRequests": {
        "description": "Rate limited (`rate_limited`), or an idempotent create is still in flight. Honor `Retry-After`.",
        "headers": {
          "Retry-After": { "schema": { "type": "string" }, "description": "Seconds to wait." },
          "X-RateLimit-Limit": { "schema": { "type": "string" } },
          "X-RateLimit-Remaining": { "schema": { "type": "string" } },
          "X-RateLimit-Reset": { "schema": { "type": "string" }, "description": "Seconds until the bucket refills." }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "MoneyNanm": {
        "type": "string",
        "pattern": "^[0-9]+$",
        "description": "Integer nANM base units as a decimal string. 1 ANM = 10^9 nANM. Never a JSON number.",
        "examples": ["25000000000"]
      },
      "PaymentIntentStatus": {
        "type": "string",
        "enum": ["CREATED", "AWAITING_PAYMENT", "PAYMENT_DETECTED", "CONFIRMING", "PAID", "EXPIRED", "UNDERPAID", "OVERPAID", "FAILED", "REFUNDED", "PARTIALLY_REFUNDED"],
        "description": "The 11-state intent lifecycle. `PAID` is reachable only with a successful execution receipt at the required confirmation depth; a reorg can move `PAID` back to `AWAITING_PAYMENT`."
      },
      "PaymentIntent": {
        "type": "object",
        "required": ["object", "id", "status", "mode", "currency", "anm_amount", "merchant_amount", "protocol_fee", "protocol_fee_bps", "payment_address", "payment_reference", "checkout_url", "required_confirmations"],
        "properties": {
          "object": { "type": "string", "const": "payment_intent" },
          "id": { "type": "string", "examples": ["pay_X9m2ABC"] },
          "status": { "$ref": "#/components/schemas/PaymentIntentStatus" },
          "mode": { "type": "string", "enum": ["live", "test"] },
          "currency": { "type": "string", "description": "The currency the charge was stated in.", "examples": ["ANM", "USD"] },
          "requested_amount": { "type": ["string", "null"], "description": "The amount as you requested it, in `currency` units (or nANM when created via `amount_nanm`)." },
          "anm_amount": { "$ref": "#/components/schemas/MoneyNanm" },
          "anm_amount_formatted": { "type": "string", "description": "Exact decimal ANM display string (grouped, trailing zeros trimmed).", "examples": ["25", "41.82525"] },
          "merchant_amount": { "allOf": [{ "$ref": "#/components/schemas/MoneyNanm" }], "description": "What you receive: `anm_amount` minus the protocol fee. `merchant_amount + protocol_fee == anm_amount` exactly." },
          "protocol_fee": { "allOf": [{ "$ref": "#/components/schemas/MoneyNanm" }], "description": "The 2.00% protocol fee in nANM, floored." },
          "protocol_fee_bps": { "type": "integer", "const": 200 },
          "merchant_order_id": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "payment_address": { "type": "string", "description": "bech32m `anim1…` address the customer pays." },
          "payment_reference": { "type": "string", "description": "The reference committed inside the transaction's signed bytes. Identity of a payment is reference + recipient, never amount." },
          "checkout_url": { "type": "string", "format": "uri", "description": "Hosted checkout page — send the customer here.", "examples": ["https://pay.animica.dev/c/pay_X9m2ABC"] },
          "transaction_hash": { "type": ["string", "null"] },
          "confirmations": { "type": "integer" },
          "required_confirmations": { "type": "integer", "description": "Confirmations required before `PAID` (currently 12)." },
          "metadata": { "type": "object" },
          "created_at": { "type": ["string", "null"], "format": "date-time" },
          "expires_at": { "type": ["string", "null"], "format": "date-time", "description": "Default TTL 30 minutes." },
          "completed_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "CreatePaymentIntentRequest": {
        "type": "object",
        "description": "Send exactly one of `amount` (human units of `currency`) or `amount_nanm` (exact base units; only with currency `ANM`). Both together is a 400. Amounts must be strings — JSON numbers are refused (`amount_must_be_string`).",
        "oneOf": [{ "required": ["amount"] }, { "required": ["amount_nanm"] }],
        "properties": {
          "amount": { "type": "string", "description": "Human quantity of `currency` as a decimal string: `\"100\"` with `ANM` = 100 ANM (max 9 decimal places); `\"49.99\"` with `USD` = dollars (max 2 decimal places).", "examples": ["25", "49.99"] },
          "amount_nanm": { "allOf": [{ "$ref": "#/components/schemas/MoneyNanm" }], "description": "Exact integer base units. Only valid with currency `ANM`." },
          "currency": { "type": "string", "default": "ANM", "description": "`ANM`, or a fiat code like `USD` (requires the server's rate source; otherwise 400 `currency_unsupported`)." },
          "merchant_order_id": { "type": "string", "maxLength": 120, "description": "Your order id. Keep it unique per order and reuse it as the Idempotency-Key basis." },
          "description": { "type": "string", "maxLength": 500 },
          "metadata": { "type": "object", "description": "Arbitrary JSON object, at most 4096 bytes serialized." },
          "success_url": { "type": "string", "format": "uri", "description": "Absolute http(s) URL the customer returns to after payment." },
          "cancel_url": { "type": "string", "format": "uri" }
        }
      },
      "CreateRefundRequest": {
        "type": "object",
        "required": ["payment_intent"],
        "properties": {
          "payment_intent": { "type": "string", "description": "The `pay_…` intent to refund. Must be `PAID` or `PARTIALLY_REFUNDED`." },
          "amount": { "type": "string", "description": "Decimal ANM string. Omit both amount fields to refund the full `merchant_amount`." },
          "amount_nanm": { "$ref": "#/components/schemas/MoneyNanm" },
          "reason": { "type": "string", "maxLength": 300 }
        }
      },
      "Refund": {
        "type": "object",
        "required": ["object", "id", "status", "amount"],
        "properties": {
          "object": { "type": "string", "const": "refund" },
          "id": { "type": "string" },
          "payment_intent": { "type": ["string", "null"] },
          "status": { "type": "string", "description": "e.g. `REQUIRES_SIGNATURE` until you sign the transfer." },
          "amount": { "$ref": "#/components/schemas/MoneyNanm" },
          "amount_formatted": { "type": "string" },
          "reason": { "type": ["string", "null"] },
          "requires_merchant_signature": { "type": "boolean", "description": "A refund is a NEW transfer you sign from your payout address — never a reversal." },
          "unsigned_transfer": { "type": ["object", "null"], "description": "The transfer to sign when a signature is still required." },
          "transaction_hash": { "type": ["string", "null"] },
          "created_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Balance": {
        "type": "object",
        "required": ["object", "mode", "currency", "settled", "pending"],
        "properties": {
          "object": { "type": "string", "const": "balance" },
          "mode": { "type": "string", "enum": ["live", "test"] },
          "currency": { "type": "string", "const": "ANM" },
          "settled": { "allOf": [{ "$ref": "#/components/schemas/MoneyNanm" }], "description": "Confirmed on chain at the required depth." },
          "settled_formatted": { "type": "string" },
          "pending": { "allOf": [{ "$ref": "#/components/schemas/MoneyNanm" }], "description": "Detected but not yet deep enough. Not spendable." },
          "pending_formatted": { "type": "string" },
          "protocol_fees_paid": { "$ref": "#/components/schemas/MoneyNanm" },
          "payout_address": { "type": ["string", "null"], "description": "bech32m `anim1…` payout address." }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "type", "created", "data"],
        "properties": {
          "id": { "type": "string", "description": "Deterministic event id (`evt_…`) derived from (merchant, type, data) — the same fact keeps the same id across retries and re-drives. Deduplicate on it.", "examples": ["evt_5f2c1a9d3e8b47c6a0d1f2e3b4a5c6d7e8f90a1b2c3d4e5f"] },
          "type": {
            "type": "string",
            "enum": ["payment.created", "payment.detected", "payment.confirming", "payment.confirmed", "payment.failed", "payment.expired", "payment.underpaid", "payment.overpaid", "payment.reorged", "refund.created", "refund.completed", "invoice.paid"],
            "description": "`payment.confirmed` is the only success signal. `payment.reorged` means a previously confirmed settlement was reversed."
          },
          "created": { "type": "integer", "description": "Unix seconds when the FACT happened (not the delivery — that clock is `Animica-Timestamp`)." },
          "data": { "$ref": "#/components/schemas/WebhookEventData" }
        }
      },
      "WebhookEventData": {
        "type": "object",
        "description": "Full accounting so an order can be reconciled without a follow-up call. All money fields are integer nANM decimal strings.",
        "properties": {
          "id": { "type": "string", "description": "Payment intent id." },
          "status": { "$ref": "#/components/schemas/PaymentIntentStatus" },
          "mode": { "type": "string", "enum": ["live", "test"] },
          "currency": { "type": "string" },
          "merchant_order_id": { "type": ["string", "null"] },
          "amount_nanm": { "$ref": "#/components/schemas/MoneyNanm" },
          "merchant_amount": { "$ref": "#/components/schemas/MoneyNanm" },
          "protocol_fee": { "$ref": "#/components/schemas/MoneyNanm" },
          "protocol_fee_bps": { "type": "integer", "const": 200 },
          "payment_reference": { "type": "string" },
          "transaction_hash": { "type": ["string", "null"] },
          "block_height": { "type": ["integer", "null"] },
          "confirmations": { "type": "integer" },
          "required_confirmations": { "type": "integer" },
          "reason": { "type": ["string", "null"], "description": "Present on underpaid/overpaid/reorged transitions." }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["type", "code", "message", "request_id"],
            "properties": {
              "type": { "type": "string", "enum": ["invalid_request", "api_error"], "description": "`api_error` = 5xx (message is generic; quote `request_id` when reporting)." },
              "code": { "type": "string", "description": "Machine-readable code, e.g. `invalid_amount`, `unauthorized`, `not_found`, `idempotency_key_reused`, `not_cancellable`, `not_refundable`, `rate_limited`, `method_not_allowed`." },
              "message": { "type": "string" },
              "request_id": { "type": "string" },
              "retryAfter": { "type": "integer", "description": "Seconds; present alongside 429." }
            }
          }
        },
        "examples": [{ "error": { "type": "invalid_request", "code": "unauthorized", "message": "malformed API key", "request_id": "d71fe122c4762bd2" } }]
      }
    }
  }
}
