{
  "info": {
    "name": "WorthyPayments Public API",
    "description": "Generated from docs/endpoints/E1-public-api.openapi.yaml via apps/merchant-dashboard/scripts/sync-developer-postman.mjs. Set {{base_url}} (default includes /v1) and {{api_key}}.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Payment Intents",
      "item": [
        {
          "name": "Create a payment intent (server-only scope; Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/payment_intents",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "payment_intents"
              ]
            },
            "description": "Decide the amount on YOUR server, then hand the browser only the returned\n`client_secret`. Nothing downstream ever reads an amount from the buyer's side.\n\n```\nPOST /v1/payment_intents        Authorization: Bearer sk_..\n  { \"amount\": 4999, \"currency\": \"usd\" }\n→ { \"id\": \"pi_...\", \"client_secret\": \"pi_..._secret_...\", \"status\": \"requires_payment_method\" }\n```\n\n`amount` is an integer in the smallest currency unit — `4999` is $49.99. A float\nis rejected rather than rounded, because `49.99` would otherwise charge 49 cents.\n\nThe intent is stored as a checkout session, which is what makes the amount\nauthoritative: the charge path reads the total from the session, not the request.\n`description` names the single ad-hoc line; `reference` is your own order or cart\nid and is echoed back to you, never shown to the buyer.\n\n**Metadata.** `metadata` is your own key/value pairs, with the same rules as\n`POST /v1/charges`: up to 50 keys, keys 1–40 characters without square brackets,\nstring values up to 500 characters (a number or boolean is stored as its text), and\na key sent with an empty value is not stored. It is answered with `reference`: on\ncreate and update, and on retrieve to secret and restricted keys only, never to the\npublishable key the buyer's page holds. The charge the intent makes **copies it once,\nwhen the charge is made**, so `GET /v1/charges/{id}`, the charge list and the\n`charge.*` webhooks carry it; changing the intent's metadata later does not change a\ncharge already made. `checkout.session.completed` carries it too. A GDPR customer\ndeletion clears it. `PATCH /v1/payment_intents/{id}` merges into it.\n\n**3DS.** `three_ds.request` is the same request `POST /v1/charges` takes (`automatic`\n| `any` | `challenge` | `challenge_mandated`, EMV 3DS indicators 01 / 03 / 04), set\nhere by your server and applied to the charge the Payment Element confirms; the\nElement runs the challenge itself. Your rules still outrank it: when they ask for\n3DS the challenge is mandated whatever `request` says. It applies to a card the\nbuyer enters for this payment on the `ev:` rail (the Element's default), and to a\nsaved card: one saved with its encrypted number kept (every card saved from\n2026-10-18) answers `requires_action` and is authenticated in place. A saved card with\nno kept number (saved earlier, or imported) is declined `soft_decline_3ds` with\nnothing charged (200 `requires_payment_method`) and the Element asks the buyer to\nre-enter that card, which is then authenticated and charged (and, when it is the same\ncard, keeps its encrypted number for next time). The same happens when your rules ask\nfor 3DS on a saved card (the route's Attempt 3DS, the 3DS risk band, a customer marked\nRequire 3DS, another fraud rule): the buyer is at the Element, so a saved card is not\ntreated as merchant-initiated here; on a re-entry you receive `charge.failed` for the\ndeclined attempt, then the events of the re-entered card. A wallet (its own SCA) or a `wp:`-sealed card pays without it. Every\nread of the intent answers it back as `three_ds.request` (`automatic` when unset).\n\n**Saving the card for later payments.** `setup_future_usage` (`off_session` or\n`on_session`) saves the card this intent is paid with to `customer` once the payment is\napproved, whatever the buyer's page sends: the Payment Element draws no \"save this\ncard\" box for such an intent, and `save_payment_method` on confirm changes nothing.\nUse `off_session` when you will charge the card without the buyer present (a\nsubscription renewal), `on_session` when the buyer will pay with it again at your\ncheckout; both save the card the same way, and this payment is the card's initial\nstored-credential transaction (customer-initiated), whose network id later\nmerchant-initiated charges on the card carry. `customer` is required with it, or the\ncreate is a 400 `parameter_missing` with `param: customer`. The outcome is the\nintent's `card_save`, as for a ticked box: a fresh card is `saved` (or `failed`); a\nwallet payment (Apple Pay, tokenised Google Pay) cannot be saved and answers\n`skipped` with `failure_code: card_not_saveable`, while the payment still succeeds; a\ncard the buyer picks from their saved cards is already saved and carries no\n`card_save`. Tell the buyer at your checkout that the card is kept for later payments.\nEvery read answers it back as `setup_future_usage` (`null` when unset).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 4999,\n  \"currency\": \"usd\",\n  \"description\": \"Order\",\n  \"reference\": \"ord_1234\",\n  \"receipt_email\": \"jane@example.com\",\n  \"customer\": \"cus_3f0c9d1e-7a2b-4c5d-8e9f-0a1b2c3d4e5f\",\n  \"metadata\": {\n    \"order_id\": \"8812\",\n    \"order_number\": \"1234\"\n  },\n  \"setup_future_usage\": \"off_session\",\n  \"three_ds\": {\n    \"request\": \"challenge\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve a payment intent by its client secret (browser-safe).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/payment_intents/:client_secret",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "payment_intents",
                ":client_secret"
              ]
            },
            "description": "Scope `none`, so the Element can poll it after confirming to learn the outcome. `reference` (your order id from create) and `metadata` (your key/value pairs, `{}` when none) are answered to secret and restricted keys only; a publishable key, which the buyer's page holds, never receives them.\n\n**This is what you poll after a `processing` confirm.** When the intent's charge outcome is not known yet, this read first asks for it — **it never charges** — and answers the result: `succeeded` (with `latest_charge`), `requires_payment_method` with `last_payment_error.decline_code: payment_not_completed` (nothing was charged; the buyer can pay again on the same intent), `requires_action` with `next_action.three_ds.session_id` while the buyer is still in a 3-D Secure challenge (an intent awaiting its customer is never reported as not charged; a challenge left for 15 minutes counts as abandoned), or still `processing`. Poll every few seconds; any other intent is read as it stands, with nothing extra done.\n\n**Warning:** Keyed on the **client secret**, not the `pi_...` id. The id is not secret — it appears in dashboards, logs and webhook payloads — so accepting it here would turn every leaked id into a readable intent. The secret is the only credential that addresses one intent, which is exactly the capability-URL contract the hosted checkout session already uses.\n\nThe response omits `client_secret`: the caller already holds it, and echoing it on every read only widens where it can be logged. Tenancy-scoped — an intent belonging to another merchant or the other env mode 404s.\n\n**A `succeeded` intent carries its receipt and card-save result.** For a `succeeded` intent with a `latest_charge` (read as it stands, or just resolved by the refresh above), this read makes one transaction read under your key's merchant and mode and adds `charge` (the same named receipt subset confirm returns), `card_save` and, when the card was saved, the top-level `payment_method` — the same object confirm answered (see `PaymentIntentCardSave`). That read is bounded at 5 seconds: when it fails or does not answer in time, the intent is returned as it stands, still `succeeded`, without `charge`, `card_save` and `payment_method`. Every other status makes no extra call and carries none of the three."
          }
        },
        {
          "name": "Update a payment intent's amount, reference, customer, receipt email or metadata (server-only scope).",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/payment_intents/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "payment_intents",
                ":id"
              ]
            },
            "description": "The cart changed. Write the new total onto the intent instead of minting a\nsecond one and leaving the first to expire unpaid. Or what the intent says about\nthe payment changed: its `reference`, `customer`, `receipt_email` or `metadata`.\nSend only what changes; everything you leave out stays as it is.\n\n**Keyed on the `pi_...` id, NOT the client secret** — the opposite of every other\nroute on this resource, and deliberately so. The secret lives in the buyer's page,\nso a secret-addressed re-price would hand the buyer the one decision the Payment\nElement exists to keep on your server. The id is not a capability, and reaching\nthis route also needs the server-only scope `payment_intents:create` — the same\nscope that named the first price, because naming a new one is the same authority.\n\n**The client secret does not change.** An element already mounted against this\nintent keeps working and simply sees the new amount on its next read; there is\nnothing to re-mount. The response omits `client_secret` for that reason.\n\nOnly an unpaid intent moves. Once a charge is in flight or complete, or the intent\nexpired, the call fails (`failed_precondition`) rather than silently doing nothing,\nso a late cart edit can never disagree with what the card was charged. The same\nholds for every field here. Retrying is safe — the body states what the intent\nshould END UP as, so applying it twice lands on the same intent, which is why no\n`Idempotency-Key` is required.\n\n**The details.**\n\n- `metadata` is **merged**, as on Stripe: a key you send is set, a key you send\n  with an empty value (`\"\"`) is removed, and keys you do not send are kept. There\n  is no way to remove every key at once (`metadata: null` is a 400); send each\n  with `\"\"`. The `POST /v1/charges` rules apply to what you send, and the intent\n  can hold at most 50 keys after the merge.\n- `reference` and `receipt_email` replace what the intent holds; `\"\"` or `null`\n  clears them.\n- `customer` names a customer the same way create does, with the same check (an\n  existing customer of this key's merchant and mode). It replaces the intent's\n  customer; it cannot be removed (`null` or `\"\"` is a 400). **It cannot change once\n  a payment attempt on the intent started**, even one that was declined: that\n  attempt, and any card it saved, belong to the customer the intent had then.\n  Sending the customer the intent already has is not a change and is accepted.\n  Changing it never moves a card already saved for any customer.\n- A charge copies the intent's `reference` and `metadata` when it is made: an edit\n  after a declined attempt reaches the next attempt's charge, never one already made.\n\nWith an `amount` and details in one request, the details are applied first, then\nthe amount. Everything is validated before either, and a refused customer changes\nnothing; if the amount then fails (the intent stopped being open in between) the\ndetails stay changed, and sending the same request again finishes it. A request\nwith neither an `amount` nor a detail is refused on `amount`, as before.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 7500,\n  \"currency\": \"usd\",\n  \"description\": \"Order\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Confirm a payment intent from the browser.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/payment_intents/:client_secret/confirm",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "payment_intents",
                ":client_secret",
                "confirm"
              ]
            },
            "description": "Attach an instrument and charge the intent. Scope `none` — the capability is\nthe client secret, so this is the one write the browser makes.\n\n**Confirming from your server.** The IP the payment is scored on is the connection we\nsee. With your publishable key in the buyer's browser that is the buyer. With a secret\nor restricted key it is your server, so it is recorded as your server's address and never\ncounted as a buyer IP, as on `POST /v1/charges`. To give a server-side confirm the\nbuyer's own IP and browser, send `device_session` (from worthypayments.js\n`getDeviceSession()` in the buyer's browser). An unknown or expired session is a `400`\nwith `param: device_session` and nothing is charged; if our fraud service cannot read it,\nthe confirm goes ahead without it.\n\n**Important:** `amount` and `currency` are refused here, not ignored. The price was\nfixed when your server created the intent. Silently dropping the field would\nlet an integrator who believes they are changing the price ship a checkout\nthat charges the original amount forever, with nothing to notice.\n\nPass **exactly one** instrument:\n\n| Field | What it is |\n|---|---|\n| `card` | `ev:` field tokens (from `POST /test/tokens` in TEST, or an Evervault-encrypted card rail) |\n| `wp_token` | a `wp:v1:` HPKE envelope |\n| `payment_method` | a saved card id from a customer session |\n| `wallet` | an Apple/Google Pay credential from the wallet sheet |\n\nThere are **four**, not three. The refusal you get for sending none names all four\n(`pass \\`card\\`, \\`wp_token\\`, \\`payment_method\\`, or \\`wallet\\``), and a table that\nlisted three sent readers looking for a wallet route that does not exist.\n\nPrefer `mountPaymentElement` for browser payments (SAQ A). Do not call non-public\nSDK helpers such as `tokenize()` from the host page.\n\nTwo is refused rather than resolved: downstream picks a single instrument,\nso the other would be dropped silently and the buyer could be billed\nsomething the caller did not intend.\n\n**3-D Secure.** A `requires_action` response carries\n`next_action.three_ds.session_id`. Run the challenge, then call this endpoint\nagain with the **same instrument** plus `three_ds.session_id`. The instrument\nis required on every confirm, including this one. `three_ds` takes only\n`session_id` here: `three_ds.request` is your server's choice at create, and on\nconfirm it is a 400 `parameter_invalid` (`param: three_ds.request`).\n\n\nYou do not need to withhold it to be safe: substitution is prevented\nserver-side, where the 3DS session is bound to the amount it authenticated\nand the charge key is derived from the session.\n\nA decline is a normal outcome, not an error: you get `200` with\n`status: \"requires_payment_method\"` and `last_payment_error`, and the buyer\ncan try another card against the same intent.\n\n\n`last_payment_error` is `{ type: \"card_error\", code: \"card_declined\", decline_code,\nmessage, network_status }`. `decline_code` is the normalized reason\n(`insufficient_funds`, `duplicate_transaction`, `payment_not_completed`, …), absent\nwhen none is known. `message` is the human text: the issuer's, or the gateway's own\nwords when the gateway refused the request (e.g. `Duplicate transaction REFID:…`).\n`network_status` (a WorthyPayments extension, the vocabulary of a charge's\n`outcome.network_status`) is `not_sent_to_network` when the gateway refused the request\nbefore it reached the card network, `declined_by_network` when the issuer or network\ndeclined it, and absent when not known (`payment_not_completed` included). Branch the\nbuyer's wording on it: `processing_error` is also an issuer answer, so the code alone\ncannot say whether the card was ever seen.\n\n**The four statuses this can return** (always `200`):\n\n| `status` | What it means |\n|---|---|\n| `succeeded` | The money moved. Fulfil the order. |\n| `requires_action` | 3-D Secure challenge — see above. |\n| `requires_payment_method` | Declined. `last_payment_error` says why; the buyer can retry on the same intent. |\n| `processing` | **The outcome is not known yet.** Poll the retrieve endpoint, never this one — see below. |\n\n**The charge rides along.** When the confirm resolves to one, the response carries\n`charge` — what a receipt shows. It is read back after the payment, so it is absent\n— never partial, never wrong — when that read does not answer.\n\n\n⛔ **A named subset, because this route is browser-reachable.** Confirm runs under\n`RequireScopes(\"none\")`: a publishable key calls it, and a publishable key is\npublic. The server charge carries more — the merchant's acquiring identity\n(`mid`, `processor` = Admin → Acquirers → Code of the acquirer Payment Workflow\nchose for this sale),\nthe references another system asks for later (`network_transaction_id`, `arn`,\n`rrn`, `settlement`) and the gateway's `avs_result` / `cvv_result` verdicts. None\nof that answers \"what did the buyer just pay\", and publishing verification\nverdicts to a page the buyer can read is how a card-testing script learns which\naddresses pass. Read them from `GET /v1/charges/{id}` with a secret key.\n\n\nNo instrument reference crosses either: no token, no vault id, no PAN, no CVV, and\nno `card.bin`.\n\n\n**Whether the card was saved.** When the buyer consented (`save_payment_method: true`),\na `succeeded` intent carries `card_save` (`PaymentIntentCardSave`) and, when the card\nwas saved, the top-level `payment_method` (`pm_…`). Both are read from the persisted\ntransaction in the same bounded read as `charge`, before the receipt is filtered, and\n`charge` never carries them. The landed replay of an already-paid intent and a\nconfirm recovered after a forwarding fault answer the same value. They are\n**absent** — never `null` — when the payment did not ask to save the card, when the\nintent is not `succeeded` (a decline, a challenge, `processing`), and when the read\nfails or does not answer within 5 seconds (then `charge` is absent too; the payment's\nstatus and HTTP code are unchanged). `card_save.status: \"pending\"` means the save may\nstill be running or its outcome is unknown, and **it can stay pending**: no later read\nof this intent moves it. A save that completes later sends the webhook\n`payment_method.attached` only for a newly saved card, `payment_method.updated` when\nit renewed a card the customer already had (same card, new expiry), and no event when\nthat card was already saved with the same expiry. So settle a lasting `pending` on your\nserver with `GET /v1/customers/{id}/payment_methods` (secret key, `customers:read`);\nnever wait for `payment_method.attached`.\n\n\n**The returned `pm_…` id is the saved card's public id.** Charge it from your server\nwith `POST /v1/charges` (`payment_method.payment_method_id`) or find it in\n`GET /v1/customers/{id}/payment_methods`. A later browser confirm with a saved card\ntakes it as `payment_method`, together with the `X-Customer-Session` header of the\ncustomer who owns it. Two places carry the same card's bare id, without the `pm_`\nprefix: the customer-session list (`GET /v1/customer_sessions/payment_methods`, or the\nSDK's `savedCards()`) and the `payment_method.attached` / `payment_method.updated` /\n`payment_method.detached` webhooks. Confirm takes the bare id too.\n\n\n`processing` is reachable and must be handled. It means the gateway did not tell us\nin time whether the card was charged, or that an earlier confirm of this intent is\nstill charging (a buyer who pressed Pay again while the first one was in flight). Confirm answers it at once — it does not wait —\nand it is final for this call. Do **not** fulfil on it, and do **not** poll this\nendpoint: confirming is a payment attempt, not a status read. Learn the outcome by\npolling `GET /v1/payment_intents/{client_secret}` every few seconds — that read asks\nfor the outcome without charging — or wait for the `charge.succeeded` webhook. The\nintent moves to `succeeded`, or to `requires_payment_method` with\n`last_payment_error.decline_code: payment_not_completed` when nothing was charged\n(while a 3-D Secure challenge is still open it reads `requires_action`).\nTreating `processing` as success is the one mistake that ships goods for money that\nnever arrived.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"card\": {\n    \"number\": \"ev:AQEB...\",\n    \"cvv\": \"ev:AQEB...\",\n    \"exp_month\": \"12\",\n    \"exp_year\": \"2029\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Create a setup intent — save and verify a card with no payment (server-only scope; Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/setup_intents",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "setup_intents"
              ]
            },
            "description": "For a $0 trial sign-up, \"add a payment method\" in an account page, or a new card for a\nsubscription: your server opens a setup intent for an existing customer and hands the\nbrowser its `client_secret`; the Payment Element mounted with that secret collects the\ncard and confirms it. Nothing is charged.\n\n```\nPOST /v1/setup_intents        x-api-key: sk_..\n  { \"customer\": \"cus_...\", \"usage\": \"off_session\" }\n→ { \"id\": \"seti_...\", \"client_secret\": \"seti_..._secret_...\", \"status\": \"requires_payment_method\" }\n```\n\nThe confirm verifies the card with a $0 authorization (an NMI `validate`) and saves it\nto `customer` in the same gateway request, flagged as the card's initial\nstored-credential transaction (customer-initiated). Later merchant-initiated charges on\nthe saved card (`recurring: true`, `initiated_by: \"merchant\"` on `POST /v1/charges`)\nrefer to that verification. The card is saved exactly as a card saved with a payment\nis: you receive `payment_method.attached` (or `payment_method.updated` when the customer\nalready had that card) and `setup_intent.succeeded`.\n\n`usage` is what you will do with the card: `off_session` (the default) when you will\ncharge it without the buyer present, `on_session` when the buyer will pay with it again\nat your checkout. Both save the card the same way. `three_ds.request` (`automatic` |\n`any` | `challenge` | `challenge_mandated`) asks for 3-D Secure on the verification; the\nElement runs the challenge. Your other 3DS rules apply when the saved card is later\ncharged.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customer\": \"cus_3f0c9d1e-7a2b-4c5d-8e9f-0a1b2c3d4e5f\",\n  \"usage\": \"off_session\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List setup intents, newest first (server-only scope).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/setup_intents",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "setup_intents"
              ],
              "query": [
                {
                  "key": "customer",
                  "value": "",
                  "description": "Only this customer's setup intents (`cus_<uuid>` or the bare uuid).",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size, 1-100 (default 10).",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "The `next_cursor` of the previous page.",
                  "disabled": true
                }
              ]
            },
            "description": "Your server's view of the setup intents it created, in this key's merchant and mode,\nnewest first, optionally for one customer. Cursor-paged like `GET /v1/customers`: pass\n`next_cursor` back as `starting_after` while `has_more` is true. No item carries a\n`client_secret`.\n\n```\nGET /v1/setup_intents?customer=cus_...&limit=10        x-api-key: sk_..\n→ { \"object\": \"list\", \"data\": [ { \"id\": \"seti_...\", \"status\": \"canceled\", ... } ],\n    \"has_more\": true, \"next_cursor\": \"...\" }\n```"
          }
        },
        {
          "name": "Cancel a setup intent (server-only scope).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/setup_intents/:id/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "setup_intents",
                ":id",
                "cancel"
              ]
            },
            "description": "Ends an open setup intent: it takes no further confirm (a confirm of it is 400\n`setup_intent_unexpected_state`). Use it when the buyer abandons the form or you\ncreated a duplicate. Keyed on the `seti_` id, never the client secret.\n\n```\nPOST /v1/setup_intents/seti_.../cancel        x-api-key: sk_..\n  { \"cancellation_reason\": \"abandoned\" }\n→ { \"id\": \"seti_...\", \"status\": \"canceled\", \"cancellation_reason\": \"abandoned\", ... }\n```\n\n- A canceled intent is answered as it stands; the first reason is kept.\n- A `succeeded` intent cannot be canceled (400 `setup_intent_unexpected_state`): its\n  card is saved, so detach the card instead (`DELETE /v1/customers/{id}/payment_methods/{pmId}`).\n- While a confirm of the intent is running it is 409 `setup_intent_in_progress`:\n  that attempt may be saving a card. Retrieve the intent, then cancel it if it is\n  still open.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"cancellation_reason\": \"abandoned\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve a setup intent by its client secret (browser-safe).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/setup_intents/:client_secret",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "setup_intents",
                ":client_secret"
              ]
            },
            "description": "Scope `none`, so the Element (or your page) can read it with a publishable key. Keyed on the client secret, never the `seti_` id, and tenancy-scoped: another merchant's or the other mode's intent is a 404. The answer omits `client_secret`. A `succeeded` intent names the saved card in `payment_method`; read its brand, last four and expiry with `GET /v1/customers/{id}/payment_methods`. An intent whose last attempt failed is still `requires_payment_method` and carries `last_setup_error`."
          }
        },
        {
          "name": "Confirm a setup intent with the buyer's card (browser-safe; the Payment Element calls this).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/setup_intents/:client_secret/confirm",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "setup_intents",
                ":client_secret",
                "confirm"
              ]
            },
            "description": "The Payment Element mounted with the setup intent's `client_secret` calls this for you;\ncall it yourself only with your own Evervault-encrypted fields. The card must be\nEvervault-encrypted (`ev:`): a raw number or a `wp:` envelope is refused. The confirm\nruns one $0 verification that saves the card to the intent's customer.\n\n- **Saved:** 200 `succeeded` with `payment_method` (`pm_…`). Confirming a succeeded\n  intent again answers the same card and verifies nothing.\n- **Declined or not verifiable:** 200 `requires_payment_method` with\n  `last_setup_error` — `code` `card_declined` (with the issuer's `decline_code`) or\n  `processing_error` (the card could not be read or verified right now). The buyer can\n  try again on the same intent; after five failed attempts it accepts no more (400\n  `setup_intent_attempts_exceeded`) and you create a new one. You receive\n  `setup_intent.setup_failed`.\n- **Refused by fraud screening:** before the card is verified, the attempt meets the\n  same fraud screening as a payment: your blocked customers and cards, card-testing\n  detection, and your velocity rules (per IP, email, card). A refusal is a declined\n  attempt, as a payment's fraud block is: 200 `requires_payment_method` with\n  `last_setup_error` `card_declined` / `decline_code` `fraudulent`, counted toward the\n  five, and nothing is sent to the card network. Which rule fired is never answered.\n- **Canceled:** a canceled intent takes no card (400 `setup_intent_unexpected_state`).\n- **3-D Secure:** when the intent was created with a `three_ds.request` other than\n  `automatic`, the first confirm answers 200 `requires_action` with\n  `next_action.three_ds.session_id`; the Element runs the challenge and confirms again\n  with the same card and `three_ds.session_id`. A session that did not authenticate is\n  402 `card_error` / `card_declined` with `decline_code` `three_ds_failed` (or\n  `three_ds_card_not_enrolled`), and nothing is verified.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"card\": {\n    \"number\": \"ev:Tk9D:number:QUJDREVGR0hJSktMTU5PUA==:$\",\n    \"cvv\": \"ev:Tk9D:cvc:MTIz:$\",\n    \"exp_month\": \"12\",\n    \"exp_year\": \"2034\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Charges",
      "item": [
        {
          "name": "Create a charge (tokenized payment method only).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges"
              ]
            },
            "description": "Proxies to `PaymentsOrchestrationService/Charge`. `capture_method: manual` (or\n`capture: false`) creates an auth-only charge you later `/capture`. A real decline\nis a **402** `card_error`. The `Idempotency-Key` header (or `idempotency_key` body\nfield) makes retries safe.\n\n**The buyer's IP when your server sends the charge (`device_session`).** The IP a\ncharge carries for fraud scoring (per-IP velocity, the IP blocklist, the cascade) is\nalways the connection we see, never a body field. With a **publishable key** in the\nbuyer's browser that is the buyer. With a **secret or restricted key** it is your\nserver, so it is recorded as your server's address and never counted as a buyer IP\n(one server address shared by every buyer of your store must not block them all at\nonce). To give such a charge the buyer's own IP and browser, call\n`getDeviceSession()` in worthypayments.js when the buyer pays (it calls\n`POST /v1/device_sessions` with your publishable key), post the `ds_…` id to your\nserver, and send it as `device_session`. Optional and additive: a charge without one\nbehaves as before. A session belongs to the account and mode that created it, lasts\n30 minutes and may be sent on more than one charge in that time (an idempotent retry,\na second card after a decline). An unknown or expired session is a `400` with\n`param: device_session`, and that refusal never consumes the `Idempotency-Key`: retry\nwith a fresh session, or without one. If our fraud service cannot read the session the\ncharge goes ahead without it rather than failing.\n\n**Capture and the merchant's Payment Workflow.**\n\n- **`capture_method: manual` is always honoured.** The charge comes back\n  `status: requires_capture`, `captured: false`, and stays that way until you\n  `/capture` or `/void` it.\n- **`automatic` (the default) can be held by the merchant's route.** If the route the\n  charge takes has a Capture step set to **Manual**, the charge is authorized only and\n  the merchant captures it later. If the step is **Delayed**, the charge is authorized\n  only and captured automatically after the route's delay (1–168 hours). Either way\n  it comes back `status: authorized`, `captured: false`.\n- **Read the outcome, not your request.** Check `captured` on the response before\n  treating the funds as settled.\n\n**Declines, including gateway refusals.** Every decline is a **402** with `type:\ncard_error`, `code: card_declined` and a `decline_code`. An issuer decline carries the\nissuer's reason (`insufficient_funds`, `do_not_honor`, …). When the gateway itself\nrefuses the request — the card was never sent to the card network — the charge is\ndeclined at once (not left `pending`) and `decline_code` is one of\n`duplicate_transaction` (the gateway already holds an identical recent payment),\n`invalid_merchant`, `invalid_transaction` or `processing_error`, with the gateway's own\ntext in `message` (e.g. `Duplicate transaction REFID:3154897712` — quote the REFID to\ngateway support). Do not retry `duplicate_transaction`: look the payment up with\n`GET /v1/charges` first, it may already be taken. A real gateway fault is not a\ndecline: it stays a **502** `api_error` with generic wording.\n\n**Payment Workflow rules are declines too.** A route that requires a CVV, AVS or\npostal-code match, an amount range or a currency, or that blocks anonymous IPs,\nrefuses before the gateway with the same **402** `card_error` / `card_declined` and one\n`decline_code`: `incorrect_cvc`, `incorrect_zip`, `incorrect_address`,\n`amount_too_large`, `amount_too_small`, `currency_not_supported` or `fraudulent` (the\nanonymous-IP rule), with a buyer-facing `message`. Nothing was charged; the same\n`Idempotency-Key` replays the refusal, so retry a corrected request under a new key.\n\n**Fraud blocks are not declines here.** A charge our fraud screening refuses is a\n**400** `invalid_request_error`, `code: failed_precondition`, with a `message` that\nstarts `FRAUD_BLOCKED` (see the 400 below). A block by the scored fraud screen is\nrecorded as a failed charge, and the error carries **`charge`**, that charge's id\n(`TXN-…`): `GET /v1/charges/{id}` reads it back as `status: failed`, `failure_code:\nfraudulent`, `failure_message: \"Your card was declined.\"`, `outcome.type: blocked`, and\nyour `charge.failed` / `transaction.failed` webhooks fire for it with\n`decline.declineOrigin: \"PLATFORM\"`. It is refused before any 3-D Secure step-up, never\nfirst sent through a challenge. A fraud block 400 **without** `charge` is otherwise\nidentical, and the card was never charged. It is either a fraud pre-screen (velocity)\nblock, or a rare scored block for which the payments service returned no charge id:\nits record may not have been written, or it was written but the answer naming it was\nlost. In that second case the failed charge exists and its `charge.failed` /\n`transaction.failed` webhooks fire for a `TXN-…` this 400 never named. The 400 is\nstored under the `Idempotency-Key` and replayed exactly for 24 h, `charge` (or its\nabsence) included, with no second charge and no second webhook. Do not retry a fraud\nblock under the same key: it replays the same refusal. A new key is a new attempt: it\nis screened again, and a second refusal is a second failed charge.\n\n**An unknown outcome is not stored as the key's answer.** When the payments service\ncannot tell yet whether the card was charged, this answers the charge with `status:\npending` and releases the `Idempotency-Key` instead of storing that `pending`: a retry\nwith the SAME key asks again and gets the real outcome (the charge, or a 402), never a\nfrozen `pending`. Never retry it under a new key — that is a second charge if the first\nlands. A pending ACH debit (accepted, in transit) is a real answer and is still\nreplayed for 24 h.\n\n**PCI — accepted card / payment_method shapes.** This edge is outside the card-data\nenvironment, so a raw card number is refused on *every* field with\n`raw_pan_rejected` (400). Tokenize first. Each field is allowlisted to the grammar\nits rail actually uses — two card rails are first-class:\n\n| field | accepts |\n|---|---|\n| `card.number` + `card.cvv` (and `token` / `vault_token` / `cvv_token`) | Evervault tokens: `ev:` + at least 16 token characters. The number rail is handed to the gateway as the card number; CVV is a separate `ev:` token. |\n| `card.wp_token` | one `wp:v1:` HPKE envelope that seals PAN, expiry and CVV together. Prefer this over `number`+`cvv` when present. There is **no** separate `wp:` CVV field — it rides inside the envelope. |\n| `payment_method_id`, or a bare string `payment_method` | a saved payment-method id issued by this API. |\n| `nmi_vault_id` | a gateway vault id issued by this API. |\n| `wallet_token` | an Evervault wallet token. A wallet payment usually arrives as the `wallet` block instead. |\n\nA value that is not a raw PAN but is not a recognized token either is rejected with\n`pan_encryption_required` (card rail) or `parameter_invalid` (vault/wallet rails) —\nthe shapes are allowlisted, so an unrecognized token is never forwarded.\n\n**A publishable key cannot charge a saved card.** `payment_method_id`, `nmi_vault_id`\nand a bare string `payment_method` are refused for a `pk_` key with `403\npermission_denied` (`param: payment_method`), before the card is looked up. The key is\npublic, so it would let anyone charge any of your saved cards. Charge a saved card from\nyour server with a secret key, or confirm a PaymentIntent with a customer session.\nFresh `ev:` / `wp:` cards and wallets are unchanged.\n\n**3DS step-up.** A usable challenge returns `requires_action` with a nonempty\n`next_action.three_ds.session_id`. Authentication can precede charge creation:\nomit the charge `id` until authentication completes; preserve any real transaction id.\nResubmit the same amount, currency and instrument with `three_ds.session_id` and\nthe **same Idempotency-Key**. `requires_action` is not stored as a terminal response.\nA saved card your server charges here without `initiated_by: customer` is\nmerchant-initiated and is never stepped up: it is charged without 3DS whatever asked\n(route, risk band, Require-3DS customer, a fraud rule); a blocking rule still blocks it.\nWith the buyer paying (payment intent, hosted checkout, or a saved card charged here\nwith `initiated_by: customer`) a saved card is authenticated on its kept encrypted\nnumber; one with none is declined `soft_decline_3ds` (402 `card_error/card_declined`)\nwith nothing charged, and the Payment Element (or your checkout) re-collects it.\nAuthenticated payments, ACH and wallets retain their exemptions.\nDeterministic provider rejection is `processing_error`; temporary initiation or\nsession-storage failure is a sanitized 503 `api_error`. An unusable challenge is\na 502 `api_error` (\"3-D Secure could not be started\").\n\n**Requesting 3DS.** `three_ds.request` asks for 3DS on this charge even when your\nrules would not. Each value is the EMV 3DS challenge indicator the issuer receives:\n\n| `three_ds.request` | 3DS runs | The issuer is told |\n|---|---|---|\n| `automatic` (default) | only when your fraud rules or the route's Require 3DS ask | a rule-driven challenge is mandated (04) |\n| `any` | always | no challenge preference (01): frictionless whenever the issuer allows |\n| `challenge` | always | a challenge is requested (03); the issuer may still authenticate frictionlessly |\n| `challenge_mandated` | always | a challenge is mandated (04) |\n\nA rule outranks a request: when your rules also ask for 3DS, the challenge is\nmandated whatever `request` says, so `any` cannot soften a challenge your risk\nrules demanded. The step-up then works exactly as above (`requires_action`, run the\nchallenge, resubmit with `three_ds.session_id` and the same key; repeating `request`\non the resubmit is fine). 3DS can only run on a card entered for this payment\n(`card.number` as an `ev:` token): a request on a saved card, a gateway vault id, a\n`wp:` envelope or a wallet is a 400 `parameter_invalid` (`param:\nthree_ds.request`). To authenticate a saved card, collect it again (the Payment\nElement, or `tokenize`) and send it as `card.number`; a payment intent does this\nfor you. Omit it, or send `automatic`, to charge those as before. An\nunknown key inside `three_ds` is refused too, so a misspelling cannot silently\ncharge without the 3DS you asked for.\n\n**3D Secure threading.** If you ran 3DS in your own checkout (see `/v1/3ds/*`),\npass the session id as `three_ds.session_id`. The edge fetches that session's\n**authoritative** result (`GetThreeDsSession`, cavv/eci from the provider — never a\nbrowser value) and threads the real `cavv`, `eci`, `ds_trans_id`, protocol\n`version` and `cardholder_auth` into the gateway request so liability shifts. A\nsession id that is not your merchant's is rejected upstream, never charged as\nun-authenticated.\n\n**A session that did not authenticate never charges.** Nothing reaches the card\nnetwork unless the session authenticated with a cryptogram (fully, or an\n\"attempted\" authentication that carries one):\n\n- **Failed** — 402 `card_error` / `card_declined`, `param: three_ds.session_id`.\n  `decline_code` is `three_ds_failed` (the cardholder failed or canceled the\n  challenge, or the issuer rejected it), `three_ds_card_not_enrolled` (the card\n  cannot be authenticated: charge it again without `three_ds` if you accept no\n  liability shift, or ask for another card) or `processing_error` (a 3DS\n  network fault: run a new session). `GET /v1/3ds/sessions/{id}` names the exact\n  `failure_reason`. Stored under the Idempotency-Key like any decline; a new\n  attempt is a new session and a new key.\n- **Not finished** — 409 `three_ds_session_incomplete`: the challenge is still\n  open. Not stored; resubmit with the same key once it completes.\n- **Result unreadable** — 503 `three_ds_result_unavailable`: the session\n  authenticated but its result could not be fetched just now. Not stored; retry\n  with the same key.\n\n**Wallet Pay (Apple Pay / Google Pay).** Send a `wallet` block *instead of* a card\ninstrument — the wallet **is** the instrument:\n\n```json\n{\n  \"amount\": 1999, \"currency\": \"usd\",\n  \"wallet\": {\n    \"type\": \"apple_pay\",\n    \"token\": \"ev:…\",       // encrypted network token (DPAN) from the wallet sheet\n    \"exp_month\": 12, \"exp_year\": 2029,\n    \"cryptogram\": \"ev:…\",  // encrypted TAVV\n    \"eci\": \"5\",            // as the wallet emitted it; we zero-pad for the gateway\n    \"card_last4\": \"4973\",  // optional: the buyer's own card inside the wallet\n    \"card_brand\": \"visa\"   // optional: that card's network\n  }\n}\n```\n\nPrefer the Payment Element for wallets — it collects Apple Pay / Google Pay inside\nour iframe (public `mountPaymentElement`). If you build a custom wallet path, forward\nthe wallet credential as the wallet provider emits it. Both `token` and `cryptogram`\nare Evervault ciphertext and are decrypted in transit; a raw DPAN never reaches this\nAPI.\n\n`card_last4` / `card_brand` are optional: the last four digits and network of the\nbuyer's **own card** inside the wallet (the funding card), which the wallet sheet\nstates beside the device token (`mountWalletButton` puts them on the credential). The\ntoken carries other digits; this is the number the buyer's bank, a receipt and a\ndispute alert name, and the charge reports it back as `card.wallet.card_last4`. They\nare never refused: anything other than exactly four digits, or a brand that is not a\nplain network name, is dropped and the charge runs as it would without them. Neither\nwallet hands out a BIN, so there is no `card_bin`.\n\nRules, each a **400** naming the offending field:\n\n- `wallet` alongside `card` / `payment_method` is refused. The gateway would charge\n  the wallet and drop the card, so we will not guess which you meant.\n- `wallet.cryptogram` is **required for Apple Pay** (always a network token).\n  Google Pay may omit it — that is the `PAN_ONLY` shape, charged as an ordinary\n  card sale. The SDK does not offer PAN_ONLY by default: it is not\n  SCA-satisfied, so a buyer your rules flag for 3DS would be declined after\n  completing the Google sheet with no way to recover inside it.\n- `wallet` with `three_ds.session_id` is refused: the wallet cryptogram already\n  satisfies SCA, and the gateway rejects a wallet sale carrying 3DS fields.\n\nNo 3DS step-up ever runs on a wallet charge. Availability is per merchant and per\nbuyer device — check `/v1/wallet-config` before offering the button, and let the\nSDK decide whether the device can actually pay.\n\nThe charge you get back — here, and on every later read — reports the wallet as\n`card.wallet: { \"type\": \"apple_pay\", \"card_last4\": \"4973\", \"card_brand\": \"visa\" }` (or\n`\"google_pay\"`), and the webhook raised when the charge is decided (`charge.succeeded`\non an automatic capture, `charge.authorized`, `charge.failed`) carries it as\n`detail.walletType`, `detail.walletCardLast4` and `detail.walletCardBrand`. On a wallet\ncharge `card.last4` / `card.bin` belong to the device token, not the buyer's physical\ncard; `card.wallet.card_last4` is the physical card's, `null` when the wallet did not\nstate it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 1999,\n  \"currency\": \"usd\",\n  \"payment_method\": {\n    \"type\": \"card\",\n    \"token\": \"ev:card_tok_9c2b…\",\n    \"last4\": \"4242\",\n    \"exp_month\": 12,\n    \"exp_year\": 2030\n  },\n  \"customer\": {\n    \"email\": \"buyer@example.com\",\n    \"name\": \"Ada Lovelace\"\n  },\n  \"description\": \"Order 1234\",\n  \"capture_method\": \"automatic\",\n  \"risk_decision_token\": \"cmR0MXxtYV8xfGNoa19hYmN8YWxsb3d8NDk5OXx1c2Q.9f8e7d…\",\n  \"metadata\": {\n    \"order_id\": \"1234\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List charges (keyset pagination), newest first.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Filter by status. Accepts the same values this API *returns* on a charge —\n`succeeded` covers every captured state (including a refunded charge, which\ndid succeed), `failed` covers declines and errors. The raw processor\nstatuses (`APPROVED`, `DECLINED`, `REFUNDED`, …) are also accepted for\nbackwards compatibility. An unrecognized value is a 400, not a 500 — including\nan inherited JavaScript property name such as `constructor` or `__proto__`.\n\n`requires_action` is accepted as a filter (it resolves to the same in-flight\nrows as `pending`), but READS never emit it — see the note on the 200 below.\n",
                  "disabled": true
                },
                {
                  "key": "created",
                  "value": "",
                  "description": "Only charges created in this window, in Unix **seconds**, sent as bracketed keys:\n`created[gte]=1733600000&created[lt]=1733686400`. `created[gte]` and\n`created[lte]` are inclusive, `created[gt]` and `created[lt]` exclusive, and a bare\n`created=1733600000` matches that one second. A bound in milliseconds, or not a\nwhole number of seconds, is a 400 `parameter_invalid_integer` naming it; a bound\nsent twice, or `created=` together with `created[...]`, is a 400\n`parameter_invalid`. This is creation time, not last change: `GET /v1/events` is\nthe change feed.\n",
                  "disabled": true
                }
              ]
            },
            "description": "Lists both immediate sales and authorizations. An authorization you have not\ncaptured yet **is** a charge here and lists with `status: requires_capture`, the\nsame status `POST /v1/charges` returned for it — so a manual-capture integration\nsees its charges. (Conventional semantics. The legacy API filtered to sales only, which\nhid manual-capture charges permanently.) Refunds are not charges; list them at\n`GET /v1/refunds`."
          }
        },
        {
          "name": "Record the buyer's own connection for a charge your server sends (publishable key).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/device_sessions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "device_sessions"
              ]
            },
            "description": "Called from the **buyer's browser** by worthypayments.js `getDeviceSession()`; you\nnormally never call it yourself. We record the IP this request comes from, its\n`User-Agent` header and, optionally, the browser's fingerprint, and answer an opaque\n`ds_…` id. Your server sends that id as `device_session` on `POST /v1/charges`, and the\ncharge then carries the buyer's IP and browser instead of your server's. Neither the IP\nnor the user agent can be named in the body: an `ip` field is refused.\n\n```\nPOST /v1/device_sessions      Authorization: Bearer pk_..\n  { \"fingerprint\": \"8f3c1d2e\" }\n→ { \"id\": \"ds_K7Q2M4ZJ3WXH5N6P8R2T4V6YB3\", \"object\": \"device_session\",\n    \"livemode\": false, \"created\": 1760000000, \"expires_at\": 1760001800 }\n```\n\nThe session belongs to the account and mode (test or live) of the key that created it\nand lasts 30 minutes; it may be presented on more than one charge in that time.\n**Publishable keys only:** called with a secret or restricted key, the connection would\nbe your server again, so it is refused with `403`. The id holds no buyer data and is\nuseless without your secret key.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"fingerprint\": \"8f3c1d2e\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve a charge's verification and fraud result.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges/:id/risk",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges",
                ":id",
                "risk"
              ]
            },
            "description": "AVS/CVV result codes plus the fraud score and verdict for a charge. These live on the\ntransaction row (02 stamps the fraud snapshot back from scoring), so this is a\nprojection of the same data as `GET /v1/charges/:id`. Requires `payments:read`.\n\n`fraud_assessed` is the gate: when it is `false` the charge was never scored, and\n`fraud_score` / `fraud_decision` / `fraud_case_id` are `null` (a 0 score and \"not\nscored\" are different facts). AVS/CVV codes are emitted whenever the gateway returned\nthem, independent of scoring."
          }
        },
        {
          "name": "Retrieve a charge.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges",
                ":id"
              ],
              "query": [
                {
                  "key": "expand[]",
                  "value": "",
                  "description": "`attempts` adds the charge's `attempts` list (see below). Also read as `expand[0]`\nor `expand`; repeat it for more than one value. A value this endpoint cannot expand\nis ignored, not refused. Not offered on `GET /v1/charges`.\n",
                  "disabled": true
                }
              ]
            },
            "description": "**Acquirer identity is stamped from Payment Workflow, not invented.** At\ncharge time the merchant's Payment Workflow picks which acquirer this sale\ngoes to (the routes on that road, the conditions of this payment). Those\nprocessors all ride one NMI security key. The NMI `processor_id` on the sale\nis that acquirer's **Connector code (`processor_id`)** / charge code. Public\n`processor` is that same row's **Code** (`NMI`, `PAYARC`, `NUVEI`,\n`MAVERICK`, or any later registry row). **Connector codes** map raw gateway\nkeys onto the acquirer. Those two admin fields are not this public\n`processor` value.\n\n\nThe gateway's own answer also rides on the charge: `processor_transaction_id`,\n`mid` when the row has one, the response and processor-response pairs, the\n`avs_result` / `cvv_result` verdicts with their raw issuer letters, the statement\ndescriptor and the per-state timestamps. They are reflected, not computed, and\neach is **omitted when the row has nothing in it** rather than emitted empty.\n`response_code` / `response_message` are the gateway's own pair; a fresh\n`POST /v1/charges` response reports them too once the gateway has answered (a\n`pending` charge carries neither).\n\nAn authorized, uncaptured charge carries **`capture_before`**: the last moment it can\nbe captured. That is the card network's window, 4 days 18 hours for a Visa\nmerchant-initiated charge and 7 days otherwise (Stripe's\n`payment_method_details.card.capture_before`). Past it the charge is canceled\n(`status: canceled`, **`cancellation_reason: expired`**) and `charge.expired` fires\nwith `charge.canceled`. A void you requested carries no `cancellation_reason`.\n\n\n**`metadata`** is the `metadata` you sent on `POST /v1/charges`, as stored; `{}` for a\ncharge that carried none, one made before it was stored, and a sale synced from your\ngateway. `GET /v1/charges` returns it on every item too.\n\n\n`outcome` says who answered. A `failed` charge the gateway refused before it reached\nthe card network reads `{ \"network_status\": \"not_sent_to_network\", \"type\": \"invalid\",\n\"seller_message\": \"The payment was not processed: <the gateway's text>\", \"risk_level\":\n\"normal\" }` (`\"The payment was not processed.\"` when the gateway gave no text). A\n`failed` charge our fraud screening blocked before any gateway call (the payments\nservice's decline origin `PLATFORM`) reads `{ \"network_status\": \"not_sent_to_network\",\n\"type\": \"blocked\", \"seller_message\": \"This payment was blocked by fraud screening.\",\n\"risk_level\": \"highest\" }`, with `failure_code: fraudulent` and `failure_message: \"Your\ncard was declined.\"`. That is the charge a fraud-blocked `POST /v1/charges` 400 names\nin `charge`. Every other failure (an issuer decline, including an issuer `processing_error`) reads\n`declined_by_network` / `issuer_declined`. `GET /v1/charges` reports the same\n`outcome` for the same charge.\n\n\n⚠️ `arn`, `rrn` and `settlement` are absent on a fresh charge by design: they come\nfrom the acquirer days later. Read them back here after settlement.\n\n\nAVS and CVV are the *gateway* answering. Your own engine's score, decision, reason\ncodes and case id are on `GET /v1/charges/{id}/risk`, behind `payments:read`.\n\n\n`created` is the charge's own creation time (unix seconds) and is stable across\npolls. An uncaptured authorization reports `requires_capture`, the same status\n`POST /v1/charges` returned for it.\n\n\n`card.wallet` is `{ \"type\": \"apple_pay\" }` or `{ \"type\": \"google_pay\" }` when the\ncharge was an Apple Pay / Google Pay payment: one we processed (a `wallet` block on\n`POST /v1/charges`, the Payment Element or hosted checkout), or an approved live sale\nwe synced from your NMI gateway once NMI has named its wallet. A new synced sale is\ntagged usually a few minutes after it syncs; one synced before this existed is tagged\nwhen a one-off history check reaches it; no webhook is sent when it arrives. Until then,\nand on every other charge (a declined or test-mode synced sale included), it is `null`:\nwe never guess a wallet. On a wallet charge `card.last4` and `card.bin` belong to the **device token**,\nnot the buyer's physical card — match a dispute or a receipt accordingly.\n`GET /v1/charges` and `POST /v1/charges` report the same value.\n\n\n`card.wallet.card_last4` / `card.wallet.card_brand` are the buyer's **own card** inside\nthe wallet — the last four and network the wallet sheet stated beside the device token,\nand the number an issuer's dispute alert for this charge names. Both are `null` when\nthe wallet did not state them (a charge made before this was recorded, a custom\nintegration that does not forward them, or a sale synced from your gateway, which names\nthe wallet but not the card inside it); `card_brand` is also `null` for a network\noutside the ones `card.brand` publishes. There is never a BIN: neither wallet hands\none out.\n\n\n**`expand[]=attempts`** adds `attempts`: every processor this charge was tried on, in\nthe order Payment Workflow tried them, and how each attempt ended. A soft decline\n(`200`, `201`, `202`, `260`, `05`, `51`, `61`, `65`, `N7`) is retried on the next\nprocessor in the route's cascade; `203`, `204` and `250` never are, and a wallet or\n`wp:` token charge gets one attempt. Without `expand[]` the charge is exactly as above.\nNeeds a secret or restricted key with `payments:read`; a publishable key never sees it.\n\n```json\n\"attempts\": [\n  { \"processor\": \"PAYARC\", \"processor_label\": \"Payarc Main\", \"outcome\": \"declined\",\n    \"reconciled\": false, \"decline_code\": \"insufficient_funds\", \"response_code\": \"202\",\n    \"cascade\": false, \"final\": false, \"attempted_at\": \"2026-10-19T09:15:00.120Z\",\n    \"duration_ms\": 210 },\n  { \"processor\": \"MAVERICK\", \"processor_label\": null, \"outcome\": \"approved\",\n    \"reconciled\": false, \"decline_code\": null, \"response_code\": \"100\",\n    \"cascade\": true, \"final\": true, \"attempted_at\": \"2026-10-19T09:15:00.400Z\",\n    \"duration_ms\": 95 }\n]\n```\n\nEvery key is present on every attempt; `null` means not known.\n`processor` / `processor_label` name the processor the way the charge's own pair does.\n`outcome` is `approved`, `declined`, `error` (the gateway refused the request or\nanswered something unusable), `timed_out` (no answer came back) or `pending` (still in\nflight). `reconciled: true` marks an attempt whose own answer was lost and whose\napproval was then confirmed by looking the sale up at the gateway; its `outcome` is\nthat approval. `decline_code` is the normalized decline (the vocabulary of\n`failure_code`); `response_code` is the gateway's code for that attempt (the\nvocabulary of the charge's `response_code`). `cascade` is true on every attempt after\nthe first. `final` marks the attempt that produced the charge's result. `attempted_at`\nis when it was sent (RFC 3339) and `duration_ms` how long it took.\n\nA charge with one attempt answers one entry. A charge our fraud screening blocked\nanswers `[]`: no processor saw it. On a charge made before attempts were published,\nthe attempts carry no `processor_label`, `attempted_at` or (except on the final\nattempt) `decline_code`: those were not recorded then. A charge with no recorded\ntrail (made before 2026-07-31, or a sale\nsynced from your gateway) answers the single attempt its own record proves, with\n`cascade: true` when it was reached by a cascade whose earlier attempts were not kept."
          }
        },
        {
          "name": "Capture an authorized charge (Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges/:id/capture",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges",
                ":id",
                "capture"
              ]
            },
            "description": "Optional partial `amount` (cents). Proxies to `PaymentsOrchestrationService/Capture` (atomic claim).\n\n**Secret key only.** A publishable key is refused with `403 permission_denied`\nbefore anything reaches upstream: it is public, and a buyer holding their own charge\nid could otherwise capture a few cents of the authorization and release the rest.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 1500,\n  \"currency\": \"usd\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Void an authorized charge (Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/charges/:id/void",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "charges",
                ":id",
                "void"
              ]
            },
            "description": "Cancels an authorization before it is captured, proxying to\n`PaymentsOrchestrationService/Void`. Requires `payments:charge` and a key whose\nexec-mode can charge. The `Idempotency-Key` is required here, so a retried void\nresolves to the first answer instead of issuing a second one.\n\nAn authorization is released. A captured charge whose batch has not settled yet is\nvoided at the gateway too, so it never reaches the buyer's statement. A settled charge\n(or one the gateway refuses to void because its batch closed) is reversed with a full\nrefund instead.\n\n**Secret key only.** Because a void can become a full refund, a publishable key is\nrefused with `403 permission_denied`, exactly as it is on `POST /v1/refunds`. The\nkey is public and the buyer sees their own charge id, so allowing it would let a\nbuyer refund themselves after delivery."
          }
        }
      ]
    },
    {
      "name": "Refunds",
      "item": [
        {
          "name": "Create a refund (server-only scope; Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/refunds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "refunds"
              ]
            },
            "description": "`charge` is required. Omit `amount` for a full refund of what remains, or pass cents for\na partial.\n- **One charge, one card.** Each request refunds exactly one charge, back to the card\n  that charge was paid with. There is no bulk refund and no refund by customer.\n- **Partial refunds.** You may refund one charge several times until its captured amount\n  is used up.\n- **Gateway-synced charges.** A charge synced from your gateway is refundable like one\n  made through the API.\n- **Rate limit.** No per-minute refund budget; only the general per-key request ceiling\n  applies. On a `429 rate_limit_exceeded`, back off by `Retry-After`.\n- **Refunding past spending instead of a payout.** See the **Refund a customer's past\n  charges** guide.\n\n**Refused before any money moves** (`400`; the message names the reason):\n- `ALREADY_FULLY_REFUNDED` (`failed_precondition`): nothing is left to refund.\n- `REFUND_EXCEEDS_REMAINING` (`invalid_argument`): `amount` is more than what remains.\n  Platform refunds and refunds made in your gateway's own portal both count.\n- `TRANSACTION_DISPUTED` (`failed_precondition`): a chargeback or a matched pre-dispute\n  alert is open on the charge. The cardholder may already be getting the money back, so\n  a refund on top would pay twice.\n- `REFUND_WINDOW_EXPIRED` (`failed_precondition`): the charge is more than 180 days old.\n  For a gateway-synced charge, that means 180 days from the gateway's own date.\n- `INVALID_STATE_FOR_REFUND` (`failed_precondition`): the charge never captured any\n  money.\n- `REFUND_CURRENCY_MISMATCH` (`invalid_argument`): `currency` is not the charge's.\n\nProxies to `PaymentsOrchestrationService/Refund`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"charge\": \"tx_3PkC9d2eZvKY\",\n  \"amount\": 500,\n  \"currency\": \"usd\",\n  \"reason\": \"requested_by_customer\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List refunds (type=REFUND; optional parent charge filter).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/refunds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "refunds"
              ],
              "query": [
                {
                  "key": "charge",
                  "value": "",
                  "description": "Parent charge id filter. It lists the refunds made through the API. A refund made in your gateway's own portal points at the gateway's id and is not listed under the charge's `id`. Read the charge's `amount_refunded` for the full total.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                }
              ]
            },
            "description": "Lists refunds for the key's merchant and env-mode, paged with `limit` and\n`starting_after`.\n\n★ The scope is `payments:read`, not a refunds-specific one. A key granted only\n`refunds:create` can issue refunds and will still be refused here — worth checking\nfirst if this returns 403 on a key that demonstrably refunds."
          }
        },
        {
          "name": "Get refund by id (type=REFUND under tenancy).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/refunds/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "refunds",
                ":id"
              ]
            },
            "description": "Fetches one refund, scoped to the key's merchant and env-mode. Takes the same\n`payments:read` scope as the list, not `refunds:create`."
          }
        }
      ]
    },
    {
      "name": "Subscriptions",
      "item": [
        {
          "name": "Start a subscription (server-only scope; Idempotency-Key required).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/subscriptions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "subscriptions"
              ]
            },
            "description": "`customer` and `plan` are required. Unless the plan is trialing this takes the\nopening charge on the customer's stored payment method, so it rides the same\nmoney-path fence as `/v1/charges`: a FRAUD_ONLY key can never reach it, and a\npublishable key can never hold `subscriptions:write`.\n\nA retried POST with the same `Idempotency-Key` resolves to the SAME subscription\nrather than starting a second one on the same card.\n\nWhen the customer has no saved card, or when `payment_behavior` is\n`default_incomplete`, the response is `status: incomplete` and\n`latest_invoice.payment_intent.client_secret` for the existing Payment Element.\nCompleting that secret pays the opening invoice and activates the subscription.\nDo not put `plan` or `interval` on the Element.\n\nProxies to `BillingService/CreateSubscription`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customer\": \"cus_9fK2pQ\",\n  \"plan\": \"plan_4mN7xB\",\n  \"payment_behavior\": \"default_incomplete\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List subscriptions.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/subscriptions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "subscriptions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "customer",
                  "value": "",
                  "description": "Return only subscriptions belonging to this customer id.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Lifecycle filter — active, trialing, incomplete, past_due, paused, canceled, suspended.",
                  "disabled": true
                }
              ]
            },
            "description": "Lists subscriptions belonging to the key's merchant and env-mode, paged with\n`limit` and `starting_after`. Reading is `subscriptions:read` — unlike create and\ncancel, which move recurring money and are server-side `subscriptions:write`.\nNarrow the page with `customer` for a single buyer, or with `status`."
          }
        },
        {
          "name": "Change the plan or the card, pause, or schedule a cancellation (server-only scope).",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/subscriptions/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "subscriptions",
                ":id"
              ]
            },
            "description": "Send **exactly one** of `plan`, `paused`, `cancel_at_period_end`, or\n`default_payment_method`. Sending none, or more than one, is refused with\n`parameter_invalid` — each maps to a different billing operation and combining them\nhas no defined order.\n\n- `plan` moves the subscription to another plan. Upstream prorates.\n- `paused: true` pauses; `false` resumes. `resumes_at` (ISO-8601) is only\n  accepted alongside `paused: true` and schedules the automatic resume.\n- `cancel_at_period_end: true` schedules the cancellation. **There is no undo** —\n  `false` is refused rather than accepted as a no-op, because accepting it would\n  read as \"this un-cancelled\" while the subscription still lapsed at period end.\n- `default_payment_method` (`pm_…`) switches the card later renewals charge, with\n  no charge now. Typically the card a setup intent just saved: create a setup intent\n  for the subscription's customer, let the buyer save the new card with the Payment\n  Element, then send its `payment_method` here. The card must be an active saved card\n  of the subscription's own customer (another customer's card, or a detached one, is\n  404 `resource_missing`). A canceled subscription is refused. A `past_due`\n  subscription keeps its retry schedule, and the next retry charges the new card.\n\nNo `Idempotency-Key`: every operation here is naturally idempotent (pausing a\npaused subscription, or moving to the plan already in force, is a no-op), and a\nkey would only add a way for a legitimate correction to be swallowed as a\nduplicate.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"plan\": \"plan_pro_monthly\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve a subscription.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/subscriptions/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "subscriptions",
                ":id"
              ]
            },
            "description": "Fetches one subscription, scoped to the key's merchant and env-mode. Requires\n`subscriptions:read`, the same scope as the list."
          }
        },
        {
          "name": "Cancel a subscription (server-only scope).",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/subscriptions/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "subscriptions",
                ":id"
              ]
            },
            "description": "Defaults to cancelling AT PERIOD END — the customer keeps the time they have\nalready paid for and the subscription simply stops renewing. Pass\n`{\"immediately\": true}` to terminate mid-period instead.\n\nProxies to `BillingService/CancelSubscription`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"immediately\": false,\n  \"reason\": \"customer_request\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Create a subscription plan.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/plans",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "plans"
              ]
            },
            "description": "Proxies `BillingService/CreatePlan`. Same object the dashboard writes. Amount is\ninteger minor units. Interval is `day` / `week` / `month` / `year` (case-insensitive;\nstored and listed as `DAY` / `WEEK` / `MONTH` / `YEAR`). There is no `minute` or\n`second` interval. Requires `plans:write`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Pro\",\n  \"amount\": 1999,\n  \"currency\": \"usd\",\n  \"interval\": \"month\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List subscription plans.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/plans",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "plans"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "active",
                  "value": "",
                  "description": "`true` returns only plans currently accepting subscriptions.",
                  "disabled": true
                },
                {
                  "key": "include_archived",
                  "value": "",
                  "description": "Send `true` to include archived plans alongside live ones. Defaults to false.",
                  "disabled": true
                },
                {
                  "key": "interval",
                  "value": "",
                  "description": "Return only plans that bill on this interval.",
                  "disabled": true
                }
              ]
            },
            "description": "The plans this merchant has defined, and the way to resolve the `plan` id that\n`POST /v1/subscriptions` requires. Proxies `BillingService/ListPlans`. Cursor-paged\nwith `starting_after` (same shape as GET /v1/charges). Scoped to the key's merchant.\nRequires `plans:read`. Create with `POST /v1/plans` or in the dashboard.\n\n`amount` is in MINOR UNITS like every other money field on this API, and carries the\ncurrency the plan was priced in.\n\nArchived plans are hidden unless `include_archived=true`: an archived plan cannot\ntake a new subscription, so listing one by default would hand back an id that\n`POST /v1/subscriptions` then refuses."
          }
        },
        {
          "name": "Fetch a plan.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/plans/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "plans",
                ":id"
              ]
            },
            "description": "Proxies `BillingService/GetPlan`. Scoped to the key's merchant. Requires\n`plans:read`. The id accepts either the prefixed `plan_...` form the subscription\nobject shows or the bare uuid. A plan belonging to another merchant or the other\nenv mode answers `404`, not `403` — the id is not evidence the caller may see it."
          }
        },
        {
          "name": "Run this subscription's renewal now (TEST keys only).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/test/subscriptions/:id/bill",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "test",
                "subscriptions",
                ":id",
                "bill"
              ]
            },
            "description": "Stripe Test Clock analogue. Proxies to `BillingService/BillSubscriptionNow`,\nthe same function the calendar renew worker uses. A LIVE key is refused with\n`test_mode_only` before 11 is touched. Idempotency-Key required. Scope\n`subscriptions:write`.\n\nCHARGE_AUTOMATICALLY (default): charges the saved card for the next period\nand writes a PAID invoice listed on `GET /v1/invoices`. SEND_INVOICE:\nfinalizes and emails an OPEN invoice, no card charge. A decline is\n`subscription.past_due`, not `invoice.payment_failed`."
          }
        }
      ]
    },
    {
      "name": "Customers",
      "item": [
        {
          "name": "Create a customer.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers"
              ]
            },
            "description": "`name` is split into first/last for the domain. Proxies to\n`CustomerService/CreateCustomer`. Customers are unique per email (per merchant and\nenv-mode): an email that already has a customer is a `409` `already_exists`, and\n`GET /v1/customers?email=` finds it. A retry with the same body `idempotency_key`\nand the same email returns the customer the first call created.\n\nOnly `email`, `name`, `phone` and `idempotency_key` are read; any other field\n(`external_id`, `metadata`, `address`) is ignored, not refused.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"buyer@example.com\",\n  \"name\": \"Ada Lovelace\",\n  \"phone\": \"+15551230000\",\n  \"idempotency_key\": \"8f14e45f-ceea-467d-9c9b-2b3f0a1c9d21\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List customers (keyset pagination).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "created",
                  "value": "",
                  "description": "Only customers created in this window, in Unix **seconds**, sent as bracketed keys:\n`created[gte]=1733600000&created[lt]=1733686400`. `created[gte]` and\n`created[lte]` are inclusive, `created[gt]` and `created[lt]` exclusive, and a bare\n`created=1733600000` matches that one second. A bound in milliseconds, or not a\nwhole number of seconds, is a 400 `parameter_invalid_integer` naming it; a bound\nsent twice, or `created=` together with `created[...]`, is a 400\n`parameter_invalid`.\n",
                  "disabled": true
                }
              ]
            },
            "description": "Lists customer records for the key's merchant and env-mode, paged with `limit` and\n`starting_after`. Requires `customers:read`, which stays closed to publishable keys\nbecause it exposes the merchant's whole book. To let a browser show one returning\nbuyer their saved cards, mint a customer session instead and use the scoped grant."
          }
        },
        {
          "name": "Update a customer (POST-to-update, by convention).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id"
              ]
            },
            "description": "Only explicit fields are forwarded (never a raw body spread — blocks a\n`merchantAccountId` spoof). Send `name` (auto-split) or `first_name`/`last_name`.\nProxies to `CustomerService/UpdateCustomer`.\n\n**How `name` is split.** The last whitespace-separated token becomes\n`last_name`; everything before it becomes `first_name`. So `Mehmet Ali Yılmaz`\nis stored as `Mehmet Ali` / `Yılmaz`, and a single-token name is stored as\n`first_name` with an empty `last_name`. It is a heuristic, not a parser: a\ndouble surname such as `José García López` keeps only `López`. Send\n`first_name` and `last_name` yourself when you know the split — doing so\nskips the heuristic entirely.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"ada2@example.com\",\n  \"name\": \"Ada Byron\",\n  \"phone\": \"+15551239999\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve a customer.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id"
              ]
            },
            "description": "Proxies to `CustomerService/GetCustomer` under `(merchantAccountId, envMode)` from the key."
          }
        },
        {
          "name": "Everything needed to decide what this customer may use.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/state",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "state"
              ]
            },
            "description": "One call in place of a state machine. Without it, granting access means listing\nsubscriptions, filtering to the live ones, mapping plan ids to your own tiers, and\nkeeping that derived state correct against a stream of subscription events — the\nsame code, with the same edge cases, in every integration.\n\nReturns `object: \"customer_state\"` with the customer's identity (`id` is\n`cus_<uuid>`, as `GET /v1/customers` returns it), the **entitling** subscriptions\nonly (`active_subscriptions`), and the saved cards as brand + last4. Cacheable\nagainst your own user id; refresh it when a customer or subscription webhook\narrives.\n\nRequires `customers:read`."
          }
        },
        {
          "name": "The same state, keyed by your own customer id.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/external/:externalId/state",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                "external",
                ":externalId",
                "state"
              ]
            },
            "description": "Identical response to `GET /v1/customers/{id}/state`, addressed by the customer's\n`external_id`. No API call sets `external_id` today (`POST /v1/customers` and\n`POST /v1/customers/{id}` do not read it), so a customer you create has none and\nthis route answers `404` for any id you choose: use `/v1/customers/{id}/state`.\n\nA separate path, not a query flag, on purpose: `?id_type=external` silently falls\nback to our-id when the parameter is dropped or misspelled, and a lookup in the\nwrong namespace does not error — it returns the wrong customer, or none.\n\nRequires `customers:read`."
          }
        },
        {
          "name": "List one customer's charges.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/transactions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "transactions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size, 1-100. Defaults to 10.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "The previous page's `next_cursor` (an opaque cursor, not a charge id). A charge id is not decoded and returns the first page again.",
                  "disabled": true
                }
              ]
            },
            "description": "The charges of a single customer, newest first, in the same row shape as\n`GET /v1/charges`.\n\n**Which rows.**\n- Charges only: sales and authorizations, including those synced from your gateway.\n- Never a refund, void or capture row. A refund is not a charge; read refunds with\n  `GET /v1/refunds?charge=`.\n\n**What each row says.**\n- `amount_refunded` is what the charge has already given back. Refunds made through\n  the API and refunds made in your gateway's own portal are both included.\n- `refunded` is `true` once it covers the whole captured amount.\n- `disputed` is `true` while a chargeback or a matched pre-dispute alert is open on the\n  charge. `POST /v1/refunds` refuses such a charge (`TRANSACTION_DISPUTED`).\n- `livemode` follows your key.\n- `card` is the card the charge was paid with: `brand`, `bin` (left out when none is\n  held) and `last4`.\n- A failed charge carries `failure_code`, `failure_message` and an `outcome` that says\n  whether the gateway refused it before it reached the card network\n  (`not_sent_to_network`), our fraud screening blocked it (`not_sent_to_network`,\n  `type: blocked`, `failure_code: fraudulent`) or the issuer declined it\n  (`declined_by_network`), as `GET /v1/charges/{id}` does.\n\nTogether these are what you need to work out how much of a customer's past spending\nyou can still refund (see the **Refund a customer's past charges** guide).\n\n**One customer, several cards.** A customer's charges can have been paid with more than\none card, and a refund goes back to the card of the charge it refunds. Refund only the\ncharges on the card(s) of the person you owe, matched by `brand`, `bin` and `last4`.\n\n**Visibility.** Merchant sale exclusions apply before pagination, using the same\npolicy as `GET /v1/charges`. Hidden sales are absent. If the policy is unavailable,\nthe request fails with `503`; it never falls back to unfiltered customer history.\n\n**Paging.** Pass the previous page's `next_cursor` as `starting_after`.\n\n**Unknown customer.** An unknown customer id returns an empty list, not a `404`.\n\n⚠️ Requires **`payments:read`**, not `customers:read`: the rows are payment data,\nso the scope follows the data rather than the path."
          }
        }
      ]
    },
    {
      "name": "Customer Sessions",
      "item": [
        {
          "name": "Mint a scoped grant to read one customer's saved cards (server-only).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customer_sessions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customer_sessions"
              ]
            },
            "description": "Your server names the customer; the browser gets a token that can read that\ncustomer's payment methods and nothing else. Pass it to the Element as\n`customerSessionSecret`.\n\n```\nPOST /v1/customer_sessions      Authorization: Bearer sk_..\n  { \"customer\": \"cus_...\" }\n→ { \"object\": \"customer_session\", \"customer\": \"cus_...\",\n    \"client_secret\": \"...\", \"expires_at\": 1760000000 }\n```\n\nExpires in 30 minutes. It grants **read only**, on **one** customer: it cannot\ncharge, cannot modify anything, and cannot name a different customer. That is why\n`customers:read` itself stays closed to publishable keys — that scope lists every\ncustomer you have.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customer\": \"cus_ae7babf2-8c52-43fe-b2ae-c3395b17add8\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List the session customer's saved cards (browser-safe).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "X-Customer-Session",
                "value": "{{customer_session_secret}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customer_sessions/payment_methods",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customer_sessions",
                "payment_methods"
              ]
            },
            "description": "Scope `none`: the grant is the `X-Customer-Session` header, not the API key.\n\nTwo independent checks must pass. The signature proves the token is one we minted; a tenancy check then proves it belongs to the calling key — without that second step a merchant could replay another merchant's valid token and read their customer's cards.\n\nReturns brand, last4 and expiry only. Vault ids, `ev:`/`wp:` tokens and fingerprints are deliberately absent: the browser selects a card by OUR id, and resolving it to an instrument happens server-side on the charge.\n\n`id` is the bare payment-method id, without the `pm_` prefix that `GET /v1/customers/{id}/payment_methods` and a payment intent's `card_save.payment_method` carry. Pass it as confirm's `payment_method`; confirm takes the `pm_…` form of the same card too."
          }
        }
      ]
    },
    {
      "name": "Payment Methods",
      "item": [
        {
          "name": "Attach (vault) a card to a customer.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/payment_methods",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "payment_methods"
              ]
            },
            "description": "Accepts an Evervault `ev:` token (`token`, `pan`, or `payment_method.token`) — never a\nraw PAN (`raw_pan_rejected`). `set_default` makes it the customer's default. Proxies to\n`CustomerService/VaultCard`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payment_method\": {\n    \"token\": \"ev:card_tok_9c2b…\",\n    \"exp_month\": 12,\n    \"exp_year\": 2030\n  },\n  \"set_default\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List a customer's vaulted cards.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/payment_methods",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "payment_methods"
              ]
            },
            "description": "Proxies to `CustomerService/ListPaymentMethods`."
          }
        },
        {
          "name": "Detach (soft-delete) a card from a customer — nested route.",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:customerId/payment_methods/:pmId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":customerId",
                "payment_methods",
                ":pmId"
              ]
            },
            "description": "Proxies to `CustomerService/RemovePaymentMethod`. Soft-delete only (the pm id keeps\nresolving for downstream consumers)."
          }
        },
        {
          "name": "Detach a card by pm id (blueprint flat route; customer via ?customer=).",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/payment_methods/:pmId",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                "payment_methods",
                ":pmId"
              ],
              "query": [
                {
                  "key": "customer",
                  "value": "",
                  "description": "Required in practice. This route carries no customer in its path, so the id goes here; without it the lookup runs against an empty customer and always answers 404. Prefer `DELETE /v1/customers/{customerId}/payment_methods/{pmId}`, which cannot be called wrong.",
                  "disabled": true
                }
              ]
            },
            "description": "Same effect as the nested route. When used, pass the owning customer via the\n`customer` query param so tenancy resolves. Proxies to `CustomerService/RemovePaymentMethod`."
          }
        }
      ]
    },
    {
      "name": "Checkout Links",
      "item": [
        {
          "name": "Create a hosted checkout link.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/checkout-links",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "checkout-links"
              ]
            },
            "description": "Proxies to `CommerceCheckoutService/CreatePaymentLink`. `name` is required (min length 1).\n`link_type` FIXED requires `amount`. `link_type` SUBSCRIPTION also requires `plan` (an\n11-billing plan id); omitting it is `400 parameter_missing`. There is no idempotency\nkey on the domain RPC.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Order 1234\",\n  \"amount\": 1999,\n  \"currency\": \"usd\",\n  \"link_type\": \"FIXED\",\n  \"description\": \"One pipe wrench\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List checkout links.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/checkout-links",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "checkout-links"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "active",
                  "value": "",
                  "description": "Send `true` for live links only or `false` for inactive ones. Omit for both.",
                  "disabled": true
                },
                {
                  "key": "link_type",
                  "value": "",
                  "description": "Return only links of this type.",
                  "disabled": true
                }
              ]
            },
            "description": "Proxies to `CommerceCheckoutService/ListPaymentLinks`. Cursor-paged with `starting_after`\n(same shape as GET /v1/charges). Scoped to the key's merchant. Requires\n`checkout_links:read`. `active` and `link_type` are optional filters; an absent `active`\nlists both active and inactive links. Money fields are in minor units, absent for a\nDONATION link with no fixed amount."
          }
        },
        {
          "name": "Fetch a checkout link.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/checkout-links/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "checkout-links",
                ":id"
              ]
            },
            "description": "Proxies to `CommerceCheckoutService/GetPaymentLink`. Scoped to the key's merchant.\nRequires `checkout_links:read`."
          }
        }
      ]
    },
    {
      "name": "Products",
      "item": [
        {
          "name": "List products.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/products",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                }
              ]
            },
            "description": "Lists the merchant's catalogue products for the key's env-mode, paged with `limit`\nand `starting_after`. Requires `products:read`."
          }
        },
        {
          "name": "Attach a tax rate to a product.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/products/:id/tax_rates",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                ":id",
                "tax_rates"
              ]
            },
            "description": "Pins one of your tax rates onto a product, so `POST /v1/tax/calculate` taxes that\nproduct's lines with it. Proxies `BillingService/SetProductTaxRate`. Requires\n`tax:write` (server-only; publishable keys are refused).\n\nA product can carry several rates; they stack (for example a state rate plus a city\nrate). Attaching the same rate twice is harmless and returns the same mapping.\n\nThe product id is stored as a reference and is **not** checked against the catalog,\nso use the id you will pass as `line_items[].product` to `POST /v1/tax/calculate` —\nnormally the catalog id from `GET /v1/products`, which is also what the hosted\ncheckout sends. An inactive or deleted rate stays attached but is never applied.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tax_rate\": \"6f1c2a4e-1d7b-4b8e-9b0a-2f5d3c9e7a11\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Detach a tax rate from a product.",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/products/:id/tax_rates/:tax_rate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                ":id",
                "tax_rates",
                ":tax_rate"
              ]
            },
            "description": "Removes the product-to-rate mapping. The tax rate itself is untouched and stays\nattached to any other product. Proxies `BillingService/DeleteProductTaxRate`.\nRequires `tax:write`.\n\nOnce no rate is attached to a product, its lines fall back to your location rates\n(country / state / city), then to your default rate, in `POST /v1/tax/calculate`."
          }
        }
      ]
    },
    {
      "name": "Tokens & Config",
      "item": [
        {
          "name": "Public wallet config for the checkout SDK (Apple/Google Pay).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/wallet-config",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "wallet-config"
              ]
            },
            "description": "Whether Apple Pay and Google Pay are on for this merchant. Wallets run on the\nEvervault rail.\n\n- `EVERVAULT` — the wallet sheet returns an encrypted network token plus a\n  cryptogram, which you post to `/v1/charges` as the `wallet` block.\n  `evervaultMerchantId` is what the button mounts against, so a wallet is only\n  reported enabled when that id is present.\n- `NONE` — no wallet is available for this account.\n\nFails soft: if an upstream errors, the response degrades to \"no wallet\" rather\nthan failing the call. Treat the flags as **fail-closed** — mount a button only\non a literal `true`.\n\nThese flags describe the *account*. Whether the buyer's device can actually pay\nis decided at mount time: Apple Pay is gated on the SDK's availability check, and\nGoogle Pay's web flow runs only in Chromium browsers. A mount that renders\nnothing is a normal outcome, not an error.\n\nNo secrets are returned — only publishable ids."
          }
        },
        {
          "name": "The public half of the WorthyPayments card keypair.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/wp-config",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "wp-config"
              ]
            },
            "description": "What the browser SDK needs to seal a card into a `wp:` envelope. Scope `none`,\nso a publishable key reads it.\n\n**Publishing the public key is the design, not a leak.** Anyone can mint a\nwell-formed `wp:` token; what stops abuse is everything behind it — velocity,\nthrottling, the audience binding, and the single-use `jti`. Secrecy of this\nvalue was never part of the defence.\n\nLike `/v1/evervault-config`, this is **not per-merchant**: one platform\nkeypair serves everyone, and the envelope's `aud` claim is what binds a token\nto a merchant, enforced in 03-gateway-adapter.\n\n`alg` is a suite string a client must recognise or refuse — never ignore it and\nencrypt anyway. Cached for an hour, which is safe across a rotation because 03\nalso accepts the previous key for the overlap window.\n\nReturns `503` when no key is configured, and that response is deliberately not\ncached: a cached 503 would outlive the deploy that fixes it."
          }
        },
        {
          "name": "May this origin embed the Payment Element?",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/element-frameable",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "element-frameable"
              ],
              "query": [
                {
                  "key": "origin",
                  "value": "",
                  "description": "The embedding origin, e.g. `https://shop.example.com`. https only, except loopback http. Optional when the `Origin` header is set.\n",
                  "disabled": true
                }
              ]
            },
            "description": "The element's own server calls this before serving the card frame, and the\nanswer becomes that response's `frame-ancestors`. Scope `none`.\n\n**A boolean, never the domain list.** Returning `allowed_domains` would let\nanyone holding a publishable key — a public string — enumerate every host a\nmerchant runs: staging, internal tools, whatever was allowlisted once and\nforgotten. The caller already knows the origin it is asking about, so a yes/no\ntells it nothing it did not bring.\n\nMatches the same allowlist that governs which origins may charge with the key\n(exact host, or a single-level `*.` wildcard).\n\n**Fails closed on an unconfigured key.** No allowlist means no element —\n\"any site may render a card form on our origin\" is not a safe default for a\nmerchant who simply never filled the field in. This differs on purpose from\nthe charge-time domain check, where an empty list means \"no origin lock\" and\nis correct for a server-to-server call.\n\nQuery `origin` is optional. When it is omitted, the `Origin` header is the\norigin under test. Prefer the header for loopback parents\n(`http://localhost`, `http://127.0.0.1`): a `?origin=http://localhost…`\nquery is HTML 403 at the public edge, and never reaches this handler.\nThe element's in-cluster proxy already sends `Origin` and omits the\nquery for loopback."
          }
        },
        {
          "name": "Evervault team/app ids for the front-end relay.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/evervault-config",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "evervault-config"
              ]
            },
            "description": "Publishable-key-safe (`RequireScopes(\"none\")`) — this is the endpoint the browser SDK\nreads, NOT `/v1/3ds/config`, which requires `threeds:read`: a scope publishable keys\ncannot hold, because opening a 3DS session bills the merchant's acquirer.\n\nIds are resolved per mode: a test key gets the platform's test Evervault app, a live\nkey its live app.\nAn `ev:` token is encrypted against one Evervault app and only that app can decrypt it,\nso hardcoding ids — or reusing a test token in live — produces a charge that fails at\nthe gateway with an opaque error.\n\nFalls back to the edge's own per-mode ids when payments is unreachable, so a transient\nupstream fault still yields the pair for your key's mode rather than an SDK that cannot\nencrypt a card. Returns null ids when nothing is configured.\n\nWallet availability lives on `GET /v1/wallet-config`, also scope `none`."
          }
        },
        {
          "name": "Kount device data collection coordinates for the browser SDK.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/kount-config",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "kount-config"
              ]
            },
            "description": "Publishable-key-safe (`RequireScopes(\"none\")`). Returns the issued client id and which\nKount environment to collect against — no credentials. The browser SDK calls this and\nattaches the resulting session id to `device.kount_session_id` on the charge.\n\nMode comes from the KEY, never a header: a TEST key always collects against the sandbox\ntenant.\n\n`enabled: false` means \"do not attempt collection\" — the merchant is not provisioned, or\ncollection is switched off. Skip DDC and charge anyway; Kount is a fraud signal, not a\ngate, and a config outage must never block a payment.\n\nMerchants must allow `frame-src https://*.kaptcha.com` in their CSP. The collector runs\nin a hidden iframe, so without that entry it silently collects nothing."
          }
        },
        {
          "name": "Mint deterministic fake Evervault tokens (TEST mode only).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/test/tokens",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "test",
                "tokens"
              ]
            },
            "description": "Publishable-key-safe. Rejects a LIVE key with `test_mode_only`. Returns fake `ev:` tokens\nfor building test charges without the Evervault SDK. Never accepts or echoes a real PAN.\n\n⚠️ A TEST charge reaches NMI's sandbox, which accepts **seven** card numbers and\nrefuses everything else with `Bad Request. Please use a valid test card or test ACH\naccount from the documentation.` The brand aliases below mint exactly those. Verified\n2026-09-06; the full table, including the AVS/CVV and decline triggers, is in\n[Testing](../developers/15-testing.md).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"number\": \"4111111111111111\",\n  \"cvc\": \"123\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "3D Secure",
      "item": [
        {
          "name": "Evervault SDK init data + 3DS readiness for the key's environment.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/3ds/config",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "3ds",
                "config"
              ]
            },
            "description": "Returns the 3DS provider client config (team/app id, provider) and an `enabled` flag. `enabled` is true when the platform's 3DS provider is configured for the key's environment (test or live); it is a per-environment platform flag, with no per-merchant or acquirer check. Test mode is configured. When `enabled` is false, `POST /v1/3ds/sessions` and `POST /v1/3ds/retry-decline` return 409 `three_ds_not_enabled` — they do not call the 3DS provider. Surface the flag to the SDK before you try to open a session. Scope `threeds:read`; exec-mode `atomic | payment_only`.\n**Wallet Pay.** The same response carries `apple_pay_enabled`, `google_pay_enabled` and `evervault_merchant_id`. A wallet is on only when the platform flag, the per-merchant toggle **and** a provisioned merchant id all agree; otherwise the flags are `false` and the id is empty.\nTreat these as **fail-closed**: mount a wallet button only on a literal `true`. An older response that omits the fields must mount nothing rather than assume availability. These flags say what the *account* has enabled — whether the buyer's device can actually pay is decided at mount time by the SDK."
          }
        },
        {
          "name": "Open a 3D Secure session for the authenticated merchant.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/3ds/sessions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "3ds",
                "sessions"
              ]
            },
            "description": "Creates a 3DS session against the merchant's acquirer. When `GET /v1/3ds/config` reports `enabled: false`, this route returns **409** `three_ds_not_enabled` and does not call the 3DS provider. The billed merchant + env are derived from the API-key **principal**, never the request body — a `merchant_account_id` in the body is ignored, so a developer can never open (and bill) a session against another merchant's acquirer. The card must be an Evervault `ev:` token (or omitted for a browser-SDK-supplied card); a raw PAN is rejected. Idempotent on the `Idempotency-Key` header — a replay returns the same session. Scope `threeds:create` (server-side); exec-mode `atomic | payment_only`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 5000,\n  \"currency\": \"usd\",\n  \"card\": {\n    \"number\": \"ev:RFVC1234567890123456\",\n    \"exp_month\": \"12\",\n    \"exp_year\": \"2030\",\n    \"bin\": \"411111\",\n    \"last4\": \"1111\"\n  },\n  \"customer\": {\n    \"email\": \"buyer@example.com\"\n  },\n  \"request\": \"challenge\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Retrieve the authoritative 3D Secure result.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/3ds/sessions/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "3ds",
                "sessions",
                ":id"
              ]
            },
            "description": "Returns the **authoritative** authentication result for a session — `cavv`, `eci`, `ds_trans_id`, `cardholder_auth`, `liability_shift` — fetched from the 3DS provider, NOT any browser-submitted value. This response is the sole proof of authentication; pass its session id to `POST /v1/charges` under `three_ds.session_id` to bind it to a charge. Tenancy-scoped: a session id that isn't this key's merchant 404s. Scope `threeds:read`; exec-mode `atomic | payment_only`.\n`status` is `pending`, `authenticated` or `failed`. A charge proceeds only on `authenticated: true` with a `cavv`; any other session is refused by `POST /v1/charges`. A failed session names why in `failure_reason`, empty otherwise: `authentication_failed` (the cardholder did not pass), `canceled` (closed the challenge), `abandoned` (the challenge timed out), `rejected` (the issuer refused to authenticate), `card_not_enrolled` (the card cannot be authenticated: not enrolled, unknown to the issuer, or no supported 3DS version), `challenge_required` (a challenge was needed and the session was opened not to show one) or `processing_error` (a 3DS server, directory or issuer-ACS fault)."
          }
        },
        {
          "name": "Poll 3D Secure liveness from the browser (publishable-safe).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/3ds/sessions/:id/status",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "3ds",
                "sessions",
                ":id",
                "status"
              ]
            },
            "description": "The **browser** view of a session: is the challenge still running, did it finish, did it fail. Scope `none`, so a publishable key can read it — which the authoritative `GET /v1/3ds/sessions/{id}` deliberately cannot be, since that one returns `cavv` and a `pk_` ships in your page source.\n\nReturns liveness only: `status`, `authenticated`, and `cryptogram_present` — a **boolean**, never the cryptogram — plus `failure_reason` on a failed session (the same values as the authoritative route), so a page can tell the buyer something true. The authentication value stays server-side; `POST /v1/charges` fetches it by session id when you pass `three_ds.session_id`.\n\nThis is what the JS SDK's `handle3DS()` polls. Reach for it directly only if you drive the challenge yourself from the browser. From a server, use the authoritative endpoint instead — it carries the proof.\n\nTenancy-scoped exactly like the authoritative route: a session id that isn't this key's merchant 404s. Exec-mode `atomic | payment_only`."
          }
        },
        {
          "name": "Re-open a 3DS session after a soft gateway decline.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/3ds/retry-decline",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "3ds",
                "retry-decline"
              ]
            },
            "description": "A soft-decline recovery: re-opens a 3DS session for the original amount so a step-up can be attempted before giving up on the sale. The original charge is looked up first: an unknown `original_transaction_id` is **404** `resource_missing` and no provider session is opened. When the charge exists but `GET /v1/3ds/config` reports `enabled: false`, the route is **409** `three_ds_not_enabled`. Idempotent on the `Idempotency-Key` header (default derived from `original_transaction_id`) — a duplicate retry returns the same session and never double-bills the acquirer. Same principal-derived tenancy as create. Scope `threeds:create`; exec-mode `atomic | payment_only`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 5000,\n  \"currency\": \"usd\",\n  \"original_transaction_id\": \"txn_9f2c\",\n  \"decline_code\": \"05\",\n  \"card\": {\n    \"number\": \"ev:RFVC1234567890123456\",\n    \"exp_month\": \"12\",\n    \"exp_year\": \"2030\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Fraud",
      "item": [
        {
          "name": "Pre-checkout fraud evaluation (transaction-independent, advisory).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/check",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "check"
              ]
            },
            "description": "Evaluate a buyer BEFORE a card is tokenized — no transaction_id / card token required.\nThe buyer `email` is HMAC-hashed at the edge (raw PII never reaches Fraud-Scoring) and\n`ip` falls back to the trusted client IP. Proxies to `ScoringService/CheckStandalone`.\n\nAfter the inline Cascade check and before velocity, an active device/IP bypass\nlink to a currently blocked customer returns `blocked`/`deny`, risk score 99,\nand the fixed reason `BLOCKED_BYPASS_LINK`. Reasons never include a customer\nor anchor identifier. Cascade sync-block gates both link axes; device-enabled\nadditionally gates device links. The inline-IP switch does not gate IP links.\nMissing/NULL device or sync-block settings default on; stored false disables\nthe matching protection. Earlier allowlist and fraud-disabled decisions win.\n\nADVISORY ONLY: the real `POST /v1/charges` runs its own fraud pass server-side, so a\ndeveloper who skips or ignores /check can never bypass fraud enforcement. FAIL-OPEN —\nan unavailable Fraud-Scoring RPC degrades to `clean`/`allow`. Within the engine,\na cascade-config read error keeps the armed defaults, so a link can still deny;\na link-probe error skips only that probe.\n\nWhen `EMAIL_HASH_KEY` is unset on TEST, the edge still calls CheckStandalone with an\nempty `emailHash` so IP / device / BIN / velocity still score. The verdict is 04's;\n`reasons` also contains `fraud_hash_unavailable` so this is not Radar-on-by-default.\nLIVE without the hash key is 400 `fraud_hash_unavailable`.\n\nRequires scope `check:read` and an API key whose exec-mode is `ATOMIC` or `FRAUD_ONLY`\n(a `PAYMENT_ONLY` key is rejected with `mode_not_allowed`). The mode is read from the\nKEY, never the request body.\n\n**`risk_decision_token`** — a signed receipt for this evaluation, present when token\nsigning is configured. Echo it on `POST /v1/charges` to record which decision a charge\nfollowed. It is EVIDENCE, not authorization: the charge runs the full fraud engine\neither way, and a token can only ever cause a charge to be REJECTED (see below), never\nto skip screening.\n\nThe token binds this merchant, the verdict, and whichever of `amount`+`currency`,\n`card_bin`+`last4` and `fingerprint` you supplied here. Fields you omit are not bound,\nand the matching check is skipped at charge time rather than silently assumed. Send\n`last4` alongside `card_bin` if you want the card binding enforced. Valid for 15\nminutes, single use.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"buyer@example.com\",\n  \"ip\": \"203.0.113.9\",\n  \"fingerprint\": \"fp_9a1c2b\",\n  \"card_bin\": \"424242\",\n  \"last4\": \"4242\",\n  \"billing_country\": \"US\",\n  \"amount\": 4999,\n  \"currency\": \"usd\",\n  \"transaction_type\": \"PURCHASE\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Re-evaluate with the final amount (same stateless CheckStandalone).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/check/recheck",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "check",
                "recheck"
              ]
            },
            "description": "Identical to `POST /v1/check`; call it once the buyer confirms the final amount. Same\nscope (`check:read`) and exec-mode gate. No state is kept between /check and /recheck.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"buyer@example.com\",\n  \"amount\": 5200,\n  \"currency\": \"usd\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Report a BLOCK / UNBLOCK to the merchant blocklist (server-only).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/report",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "report"
              ]
            },
            "description": "Push a fraud decision to the merchant's own blocklist. `action` ∈\n`BLOCK | UNBLOCK | BLOCK_IP | UNBLOCK_IP`. Email actions require `email` (hashed at the\nedge); IP actions require `ip`. Proxies to `ScoringService/AddToBlocklist` or\n`RemoveFromBlocklist`.\n\nRequires scope `report:write`, which is server-only — a publishable key can never call\nit. No exec-mode gate (this is neither the charge nor the fraud-eval flow).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"action\": \"BLOCK\",\n  \"email\": \"fraud@evil.com\",\n  \"reason\": \"chargeback\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List blocklist entries for local SDK cache sync (hashed emails only).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/blocklist",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "blocklist"
              ],
              "query": [
                {
                  "key": "type",
                  "value": "",
                  "description": "Optional type filter (EMAIL | IP | CARD_BIN | DEVICE).",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Opaque page cursor from the previous response. This endpoint pages with `cursor` and `limit`, not `starting_after`, and its `limit` defaults to 100 with a ceiling of 500.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size, clamped 1..500 (default 100).",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated blocklist for a developer's offline cache. Email entries expose only the\nHMAC hash — plaintext is never returned. IP values are returned as stored.\nProxies to `ScoringService/ListMerchantBlocklist` (tenant-scoped). The operator\n`ListBlocklist` RPC is admin-only and is not this path.\nRequires scope `blocklist:read` (publishable keys are denied)."
          }
        },
        {
          "name": "Turn on Require 3DS for a customer.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/three_ds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "three_ds"
              ]
            },
            "description": "Turns the customer's Require 3DS flag on, exactly as the dashboard's control does:\nfrom the next charge on, every card charge for this customer is stepped up to 3DS\nuntil the flag is cleared. Setting it again is harmless and answers the same state,\nso no `Idempotency-Key` is needed. The response is the flag read back after the\nwrite, not an echo of the request.\n\nThe write is recorded against your API key (source `API`), so it can be told apart\nfrom a dashboard change. ACH, Apple Pay and Google Pay with a network token are\nnever stepped up, whatever the flag says (the wallet's own cryptogram stands).\n\nRequires `fraud:write`, a server-side scope (publishable keys are refused).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Chargeback on file\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Read a customer's Require 3DS flag.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/three_ds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "three_ds"
              ]
            },
            "description": "The same flag the dashboard shows under **Restrictions** on the customer's page and\nsets with **Require 3DS** / **Require 3D Secure**. While it is on, every card charge\nfor this customer is stepped up to 3DS. `require_3ds` is true whether the flag was\nset by you, by the dashboard or by a fraud detector — it is the value the charge\npath reads. TEST and LIVE keep separate flags; the key decides which one you read.\n\nAnswers only for this flag. A fraud rule or a Payment Workflow route can still ask\nfor 3DS on a charge when this reads false — see `GET /v1/fraud/rules` and\n`GET /v1/payment_workflow/routes`.\n\nRequires `fraud:read` (publishable keys are denied)."
          }
        },
        {
          "name": "Clear a customer's Require 3DS flag.",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/customers/:id/three_ds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "customers",
                ":id",
                "three_ds"
              ]
            },
            "description": "Clears the flag, as **Turn off** under **Restrictions** does in the dashboard —\nwhoever set it, including a fraud detector. Clearing a flag that is not set answers\nthe same cleared state.\n\nThis lifts only the flag. It is not an override: a fraud rule or a Payment Workflow\nroute that asks for 3DS still steps the charge up, because a platform rule always\noutranks a flag or a request. If `require_3ds` comes back true, something set it\nagain between the write and the read.\n\nRequires `fraud:write`, a server-side scope (publishable keys are refused)."
          }
        },
        {
          "name": "Turn the risk-score 3DS step-up off, on, or move its band.",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/fraud/three_ds_policy",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "fraud",
                "three_ds_policy"
              ]
            },
            "description": "If you decide 3DS yourself with `three_ds.request`, send `risk_step_up.enabled:\nfalse` so your rules are the only trigger. The change applies from the next charge.\n\nThe body sets the whole band: a bound you leave out goes back to its default (70 or\n95), so the request alone says what applies. Sending the same body again is harmless\nand answers the same setting, so no `Idempotency-Key` is needed. The response is\nthe setting read back after the write.\n\nTurning it off lifts only the score band. A charge is still stepped up when a fraud\nrule asks for it, the customer is marked Require 3DS, or the route has Attempt 3DS\non; and a score above your decline threshold still declines.\n\nRequires `fraud:write`, a server-side scope (publishable keys are refused).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"risk_step_up\": {\n    \"enabled\": false\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Read the risk-score 3DS step-up.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/fraud/three_ds_policy",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "fraud",
                "three_ds_policy"
              ]
            },
            "description": "Our fraud engine gives every card charge a risk score from 0 to 100. While\n`risk_step_up.enabled` is true, a charge whose score falls between `min_score` and\n`max_score` (inclusive; 70 and 95 unless you changed them) is stepped up to 3DS,\nwhatever your own rules decide. These are the values the next charge uses. TEST and\nLIVE keep separate settings; the key decides which one you read.\n\nRequires `fraud:read` (publishable keys are denied)."
          }
        },
        {
          "name": "List the fraud rules that apply to your account (read-only).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/fraud/rules",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "fraud",
                "rules"
              ],
              "query": [
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Opaque page cursor from the previous response's `next_cursor`. This endpoint pages with `cursor`, not `starting_after`.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size, clamped 1..100 (default 10).",
                  "disabled": true
                }
              ]
            },
            "description": "The rules on the dashboard's Fraud Rules page, in evaluation order (`priority`\nascending). Each carries its `conditions` (the `field` / `operator` / `value`\ntriples the engine evaluates, joined by `logical_operator`), its `action` and\nwhether it is `enabled`. When several rules match one charge the strongest action\nwins: DECLINE, then REVIEW, then STEP_UP_3DS, then ROUTE, then APPROVE.\n\n`requires_3ds` is true for a rule whose action is `STEP_UP_3DS`. `global: true`\nmarks a platform rule you cannot edit; it applies to every account. Every Payment\nWorkflow route keeps one rule in step with the canvas — `route` names it, and\n`null` means a rule you authored.\n\n**Read-only.** Creating, editing or disabling rules is not available from the API:\nit is a large security surface (a leaked key could switch your fraud rules off)\nand stays in the dashboard. Requires `fraud:read` (publishable keys are denied)."
          }
        },
        {
          "name": "List your Payment Workflow routes (read-only).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/payment_workflow/routes",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "payment_workflow",
                "routes"
              ]
            },
            "description": "The routes drawn on the dashboard's Payment Workflow canvas, in the order they are\nevaluated (`order`, from 1). The catch-all \"All other conditions\" branch is the\nroute with `catch_all: true` and is always last. Each route carries its\n`conditions`, its outcome (`action`: `route` through `processor` and then\n`fallback_processors`, or `decline`) and its gates.\n\n`require_3ds` is the route's 3DS toggle (labelled **Attempt 3DS** on the canvas):\nwhen the winning route has it on, the charge is stepped up. `require_cvv` /\n`require_avs` decline a charge whose CVV or AVS check fails. `min_amount` /\n`max_amount` are integer cents; 0 means no bound. Routes have no on/off switch;\nevery route returned is saved in the workflow. The whole list comes back in one\nresponse.\n\n**Read-only.** Editing routes from the API is not available: routing decides\nwhere money goes, and opening it to API keys needs a separate decision. Requires\n`fraud:read` (publishable keys are denied)."
          }
        }
      ]
    },
    {
      "name": "Events",
      "item": [
        {
          "name": "List delivered events (webhook deliveries; page-based).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/events",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "events"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "",
                  "description": "Filter to one event type (`charge.succeeded`). Applied in SQL, so `limit` still means up to N MATCHING rows.",
                  "disabled": true
                },
                {
                  "key": "created_after",
                  "value": "",
                  "description": "Unix seconds or RFC3339. Anything else is rejected with `parameter_invalid` rather than silently ignored.",
                  "disabled": true
                }
              ]
            },
            "description": "Proxies to `WebhookService/ListDeliveries`. Pagination is **page-based** (not keyset):\n`starting_after` is treated as a 1-based page number and `next_cursor` is the next page\nnumber when a full page is returned.\n\n**A list item WRAPS the delivered envelope; it is not the same shape as the pushed\nbody.** The item has five keys (`id`, `object`, `type`, `created`, `event_id`) and\n`data` holds the whole envelope that was POSTed, so the resource sits at\n`data.data.object` — one level deeper than in a webhook body — and `api_version` /\n`livemode` are at `data.api_version` / `data.livemode`.\n\n**`id` and `event_id` are different ids.** `id` names this *delivery* (fan-out writes\none row per subscribed endpoint) and is the only value `GET /v1/events/{id}` resolves.\n`event_id` names the *occurrence*: it is the `X-Webhook-Id` header and the `id` inside\nthe delivered envelope, shared by every delivery of that occurrence. Dedupe retries on\n`id` and fan-out on `event_id`; `GET /v1/events/{X-Webhook-Id}` answers `404`.\n\n`created` is **unix seconds**, like every other resource here."
          }
        },
        {
          "name": "Retrieve a single delivered event.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/events/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "events",
                ":id"
              ]
            },
            "description": "The `evt_` prefix is stripped to the underlying **delivery** id. Proxies to\n`WebhookService/GetDelivery`.\n\n⚠️ **Keyed on the delivery id — the `id` from `GET /v1/events`, not the `event_id`\nand not the `X-Webhook-Id` header.** Both are `evt_`-prefixed, which is what makes\nthem easy to confuse; passing the occurrence id here answers `404`. To find the\npolled row for a pushed body, list with `type` + `created_after` and match on\n`event_id`.\n\nThe response has the same shape as one list item: `data` is the whole delivered\nenvelope, so the resource is at `data.data.object`, and `created` is unix seconds."
          }
        }
      ]
    },
    {
      "name": "Webhook Endpoints",
      "item": [
        {
          "name": "Create a webhook endpoint (signing secret returned once).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhook-endpoints"
              ]
            },
            "description": "Proxies to `WebhookService/CreateWebhook`. The `secret` is returned **only** on create\n(and on roll-secret) — never on list/get. An `Idempotency-Key` header is honored (else\nderived from the url).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://merchant.example.com/hooks/worthy\",\n  \"enabled_events\": [\n    \"charge.succeeded\",\n    \"charge.refunded\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "List webhook endpoints (no secrets).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhook-endpoints"
              ]
            },
            "description": "Returns every webhook endpoint configured for the key's merchant and env-mode, and\nrequires `webhooks:read`.\n\nUnlike the other collections on this API it takes no `limit` or `starting_after`:\nthe handler accepts no paging parameters and the whole list comes back in one\nresponse."
          }
        },
        {
          "name": "Retrieve a webhook endpoint (no secret).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhook-endpoints/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhook-endpoints",
                ":id"
              ]
            },
            "description": "Proxies to `WebhookService/GetWebhook`."
          }
        },
        {
          "name": "Delete a webhook endpoint.",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhook-endpoints/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhook-endpoints",
                ":id"
              ]
            },
            "description": "Proxies to `WebhookService/DeleteWebhook`. Reports the domain's real `deleted` flag."
          }
        },
        {
          "name": "Rotate a webhook endpoint's signing secret (new secret returned once).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhook-endpoints/:id/roll-secret",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhook-endpoints",
                ":id",
                "roll-secret"
              ]
            },
            "description": "Proxies to `WebhookService/RegenerateSecret`. The new `secret` is shown exactly once."
          }
        }
      ]
    },
    {
      "name": "Health",
      "item": [
        {
          "name": "This specification, served by the API itself.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/openapi.json",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "openapi.json"
              ]
            },
            "description": "The machine-readable contract, always matching the deployed edge: it is generated\nfrom the canonical spec at build time and shipped inside the image, so it cannot\nlag behind the code it describes.\n\n**No authentication** — this is a public document, like a hosted `/openapi.json`\non any public API. Cached for 5 minutes.\n\nPoint a client generator here rather than at a copy you downloaded once."
          }
        },
        {
          "name": "A hosted API reference page, served by the API itself.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/docs",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "docs"
              ]
            },
            "description": "An HTML reference rendered from the specification above, for reading the contract\nstraight off the API host. **No authentication.**\n\nThe full documentation — guides as well as endpoints — is at\n`https://app.worthypayments.com/developers`."
          }
        },
        {
          "name": "Authenticated ping — echoes key mode, type, and merchant.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/ping",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "ping"
              ]
            },
            "description": "The cheapest way to confirm a key works and to see what it actually is. It runs\nunder `RequireScopes(\"none\")`, so a publishable key reaches it too, which makes it\nusable as a browser-side smoke test. The response names the key's mode (`test` or\n`live`), its type (`secret` or `publishable`) and the merchant account it resolves\nto — enough to catch a key that points somewhere other than you expect before you\ndebug it as a charge failure."
          }
        }
      ]
    },
    {
      "name": "Disputes",
      "item": [
        {
          "name": "Aggregate dispute counters and recovery amounts.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/disputes/stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "disputes",
                "stats"
              ],
              "query": [
                {
                  "key": "start_date",
                  "value": "",
                  "description": "YYYY-MM-DD inclusive lower bound.",
                  "disabled": true
                },
                {
                  "key": "end_date",
                  "value": "",
                  "description": "YYYY-MM-DD inclusive upper bound (end of day).",
                  "disabled": true
                },
                {
                  "key": "processor",
                  "value": "",
                  "description": "Count only disputes from this processor. Omit for every processor.",
                  "disabled": true
                },
                {
                  "key": "country",
                  "value": "",
                  "description": "Count only disputes from this issuing country. Omit for every country.",
                  "disabled": true
                }
              ]
            },
            "description": "Roll-up of the merchant's disputes: open/won/lost counts, amount at risk, amount\nrecovered, and win/recovery rates. The dashboard companion to `GET /v1/disputes`.\nCounts every dispute case received in the window — chargebacks, inquiries and retrieval\nrequests, and later rounds such as pre-arbitration — so `needs_response`, `won` and `lost`\ninclude every case you answer, each at its current status. Pre-dispute alerts and\nPayArc RDR resolutions, reversals and credits are not dispute cases and are not counted.\nOptional `start_date` / `end_date` (YYYY-MM-DD) window and `processor` / `country`\nfilters mirror the list. Requires `disputes:read`. Money is in minor units; rates are\nwhole percents (`63` means 63%). `win_rate` is null while no case in the window is\nwon or lost."
          }
        },
        {
          "name": "List disputes changed since a timestamp (polling).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/disputes",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "disputes"
              ],
              "query": [
                {
                  "key": "updated_since",
                  "value": "",
                  "description": "Unix seconds or RFC3339. Exclusive lower bound on `updated_at`.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Filter by lifecycle state. An unrecognised value is ignored (the filter is\ndropped and the full list is returned) rather than rejected — so check your\nspelling if a filtered poll returns more than you expected.\n\n`BUYER_REVIEW`, `APPEALABLE` and `BANK_REVIEW` are deprecated and never emitted;\nthey are kept in the enum for wire compatibility only (WRT-152). Filtering on one\nreturns an empty page. A dispute that is with the issuing bank is `UNDER_REVIEW`.\n\n`RELATED_CASE` is a PayArc related case (a reversal, resolution record,\npre-arbitration, credit or retrieval fulfilment) filed under the dispute that\ncarries its family's state; its `parent_dispute_id` names that dispute.\n",
                  "disabled": true
                },
                {
                  "key": "stage",
                  "value": "",
                  "description": "Filter by escalation stage. An unrecognised value is ignored (the filter is\ndropped and the full list is returned) rather than rejected.\n",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "1–100, default 25.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Opaque cursor — echo back `next_cursor`.",
                  "disabled": true
                }
              ]
            },
            "description": "The pull side of the `dispute.*` webhooks. Same cursor as `GET /v1/pre_dispute_alerts`:\nkeep the largest `updated_at` you have seen and pass it back as `updated_since`. Page\nwith `starting_after` until `has_more` is false.\n\nRequires `disputes:read`, granted by default on new keys (add it in Dashboard →\nDevelopers if an older key answers 403)."
          }
        },
        {
          "name": "Get one dispute by id, processor id, or Kount request id.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/disputes/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "disputes",
                ":id"
              ]
            },
            "description": "`id` accepts this API's dispute id, the processor's dispute id, or\n`kount_alert_id` (the Ethoca/CDRN request id)."
          }
        }
      ]
    },
    {
      "name": "Invoices",
      "item": [
        {
          "name": "List invoices.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/invoices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "invoices"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: the last invoice id from the previous page.",
                  "disabled": true
                },
                {
                  "key": "customer",
                  "value": "",
                  "description": "Return only invoices for this customer id.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "Filter by status, e.g. `open`, `paid`, `void`.",
                  "disabled": true
                }
              ]
            },
            "description": "Proxies `BillingService/ListInvoices`. Cursor-paged with `starting_after`.\nRequires `invoices:read`. Amounts are integer minor units.\n`hosted_invoice_url` is null on a draft and\n`https://app.worthypayments.com/invoices/{id}/view?token=…` after finalize.\nA successful CHARGE_AUTOMATICALLY renewal (including test bill-now)\nwrites a PAID row. Opening create does not mint an invoice in this\nrelease. An empty `data` array is a successful list, not a missing route."
          }
        },
        {
          "name": "Fetch an invoice.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/invoices/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "invoices",
                ":id"
              ]
            },
            "description": "Proxies `BillingService/GetInvoice`. Accepts `in_<uuid>` or the bare uuid.\n`hosted_invoice_url` is null on a draft; after finalize it is\n`https://app.worthypayments.com/invoices/{id}/view?token=…`."
          }
        },
        {
          "name": "Email this invoice (server-only `invoices:write`).",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/invoices/:id/send",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "invoices",
                ":id",
                "send"
              ]
            },
            "description": "Proxies `BillingService/SendInvoice`. Accepts `in_<uuid>` or the bare uuid.\nOPEN or later only — a draft is refused (`finalize first`; finalize stays\non the dashboard). Re-send is allowed and emits `invoice.sent` again.\nA publishable key is refused. There is no `invoice.payment_failed`\nevent; a declined card renewal is `subscription.past_due`."
          }
        }
      ]
    },
    {
      "name": "Metrics",
      "item": [
        {
          "name": "Sales / KPI roll-up.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/metrics",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "metrics"
              ],
              "query": [
                {
                  "key": "period",
                  "value": "",
                  "description": "Preset window, defaulting to `30d`. Ignored when `start_date` and `end_date` are supplied.",
                  "disabled": true
                },
                {
                  "key": "start_date",
                  "value": "",
                  "description": "Start of a custom window, `YYYY-MM-DD`. Use with `end_date` instead of `period`.",
                  "disabled": true
                },
                {
                  "key": "end_date",
                  "value": "",
                  "description": "End of a custom window, `YYYY-MM-DD`. Use with `start_date` instead of `period`.",
                  "disabled": true
                }
              ]
            },
            "description": "The same KPI tiles the dashboard shows: gross / approved / net volume, refunds,\napproval rate and transaction counts. Proxies `AnalyticsService/GetKpiTiles`. Requires\n`metrics:read`. `period` is today|7d|30d|90d|all (default 30d); a custom window is\n`start_date` / `end_date` (YYYY-MM-DD). Money is in minor units; `approval_rate` is a\n0..1 fraction and `approval_rate_bps` the same figure in basis points."
          }
        }
      ]
    },
    {
      "name": "PreDisputeAlerts",
      "item": [
        {
          "name": "List pre-dispute alerts changed since a timestamp (polling).",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/pre_dispute_alerts",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "pre_dispute_alerts"
              ],
              "query": [
                {
                  "key": "updated_since",
                  "value": "",
                  "description": "Unix seconds or RFC3339. Exclusive lower bound on `updated_at`.",
                  "disabled": true
                },
                {
                  "key": "state",
                  "value": "",
                  "description": "NEW | AUTO_REFUNDED | REFUNDED | DISMISSED | ESCALATED.",
                  "disabled": true
                },
                {
                  "key": "source",
                  "value": "",
                  "description": "ETHOCA | CDRN | RDR.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "",
                  "description": "1–100, default 25.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Opaque cursor — echo back `next_cursor`.",
                  "disabled": true
                }
              ]
            },
            "description": "The pull side of the `pre_dispute_alert.*` webhooks. Use it to catch up after a missed\ndelivery, or instead of running an endpoint at all.\n\nThe object here is a **superset** of the webhook payload: it carries the same lifecycle\nfields (`state`, `amount`, `external_id`, `reason_code`) and adds the match evidence,\nthe sub-merchant MID, the processor and the linked sale's gateway id. Reconcile the two\non `id` — but do not assume the field sets are identical.\n\n**How to poll.** Keep the largest `updated_at` you have seen and pass it back as\n`updated_since`. Page with `starting_after` until `has_more` is false, then sleep.\n\nOrdering is by `updated_at` ascending, so an alert updated while you are paging moves\nto the END of the sequence rather than shifting the rows ahead of it. You may therefore\nsee the same alert twice — dedupe on `(id, updated_at)`, exactly as for the webhooks —\nbut you cannot miss one.\n\nAn `updated_since` that is neither unix seconds nor RFC3339 is rejected with 400 rather\nthan ignored: treating a typo as \"from the beginning\" would re-deliver your whole alert\nhistory on a routine poll.\n\n**`auto_rule`** tells you whether the platform resolved the alert by itself and why. On\nan `AUTO_REFUNDED` alert it names the rule that moved the money (`rule_id`,\n`rule_label`, `evaluated_at`). When nothing fired it carries `decision: NONE` and a\n`reason` — including the refusals no rule can override, such as `sale-already-refunded`\nor `operator-excluded`. It is `null` only on alerts the engine never evaluated.\n\n**`match_evidence.card_bin` / `card_last4` are the card the issuer reported**, and they\nmay differ from the card you hold for the linked sale without the link being wrong.\nApple Pay and Google Pay charge a device token rather than the card, and virtual card\nnumbers and reissued cards do the same. Such an alert is linked on the network's own\nidentifiers for the payment, `arn` and `auth_code`, with the amount, the date and the\ncard network. Reconcile on `transaction_id`, `arn` or `auth_code`, never on the card\ndigits.\n\nRequires the `disputes:read` scope. New keys get it by default; an older key may\npredate that — add it in Dashboard → Developers if a call answers 403."
          }
        },
        {
          "name": "Get one pre-dispute alert by id or external id.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/pre_dispute_alerts/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "pre_dispute_alerts",
                ":id"
              ]
            },
            "description": "`id` accepts either this API's alert id or the provider's own `external_id`\n(ETH-9821 / CDRN-4408 / RDR-2841) — whichever you kept. Both are unique per merchant.\n\nAn alert owned by another merchant answers 404, the same as one that does not exist."
          }
        }
      ]
    },
    {
      "name": "Stores",
      "item": [
        {
          "name": "Aggregate storefront counters + revenue.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/stores/stats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "stores",
                "stats"
              ]
            },
            "description": "Roll-up across the merchant's storefronts: total/active/inactive stores, product count,\norder count, revenue. Proxies `CommerceCheckoutService/GetStoreStats`. Requires\n`stores:read`. Money in minor units. Declared before `/v1/stores/{id}` so \"stats\" is\nnot read as a store id."
          }
        },
        {
          "name": "List storefronts.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/stores",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "stores"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "description": "Page size. Defaults to 10 and is capped at 100.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor for the next page: the id of the last row you received. Omit it on the first request, and keep paging while `has_more` is true.",
                  "disabled": true
                },
                {
                  "key": "active",
                  "value": "",
                  "description": "Send `true` for open storefronts only or `false` for closed ones. Omit for both.",
                  "disabled": true
                }
              ]
            },
            "description": "The merchant's storefronts (the \"markets\" in product terms). Cursor-paged with\n`starting_after` (same shape as GET /v1/charges). `active` is an optional filter; an\nabsent one lists both. Read-only, scoped to the key. Requires `stores:read`. Proxies\n`CommerceCheckoutService/ListStores`."
          }
        },
        {
          "name": "Fetch a storefront.",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "x-api-key",
                "value": "{{api_key}}"
              }
            ],
            "url": {
              "raw": "{{base_url}}/stores/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "stores",
                ":id"
              ]
            },
            "description": "Proxies `CommerceCheckoutService/GetStore`. Scoped to the key's merchant. Requires\n`stores:read`."
          }
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.worthypayments.com/v1"
    },
    {
      "key": "api_key",
      "value": "sk_test_xxx"
    },
    {
      "key": "customer_session_secret",
      "value": ""
    }
  ]
}
