{
  "info": {
    "_postman_id": "9f2c7a10-b8d4-4e61-9a03-1c5d7e8f2b40",
    "name": "Capay Global Payment Gateway API",
    "description": "The Capay Global Payment Gateway API: mobile money, bank and card collections, disbursements and refunds across Africa. ## Conventions - Every response, success or error, is a JSON envelope carrying a `request_id` (also sent as the `X-Request-Id` header). Quote it to support to locate the exact invocation. - Money amounts at the API boundary are **decimal strings** with an explicit ISO 4217 currency (never floats, never bare subunit integers). Zero-decimal currencies such as XAF have no fractional part. - The environment (sandbox vs production) is derived from the API key alone: `sk_test_...` keys hit the sandbox simulator, `sk_live_...` keys move real money. There is no other environment switch. - The money-creating endpoints (POST /collections, /payouts, /payouts/batch and /refunds) require an `Idempotency-Key` header, and POST /checkout/initiate accepts one; retrying with the same key can never double-process. ## Webhook signatures Deliveries to your webhook endpoints carry `X-Capay-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, \"{t}.{raw_body}\")>` plus `X-Capay-Event-Id`, `X-Capay-Event-Type` and `X-Capay-Format`. During secret rotation the header carries two `v1` entries; accept the delivery if any matches. Reject deliveries older than 300 seconds and compare in constant time against the raw body. ## Webhook body formats Each endpoint is created with a `format` that fixes the shape of every body it receives (the signature always covers the raw body sent): - `standard` (default): the event envelope `{id, object: \"event\", type, created_at, livemode, data}`. - `simple`: ONE flat JSON object with the keys CRM, ERP and shop connectors map by name. Every key is always present (null when it does not apply) and money is a decimal string in the event's currency, e.g. for `checkout.session.completed`: `{\"type\":\"checkout.session.completed\",\"event_id\":\"evt_...\",\"occurred_at\":\"2026-10-18T17:06:00.876697+00:00\",\"livemode\":false,\"reference\":\"DEP-42\",\"session\":\"cs_...\",\"collection\":\"col_...\",\"payout\":null,\"refund\":null,\"status\":\"completed\",\"amount\":\"5000\",\"currency\":\"XAF\",\"fee\":\"150\",\"net\":\"4850\",\"country\":null,\"method\":null,\"reason_code\":null,\"merchant_reference\":\"DEP-42\",\"fx_rate\":null}`. Events whose payload is a collection, payout or refund (`collection.*`, `payout.*`, `refund.*`, `transaction.*`) fill the matching id key plus `status, amount, currency, country, method, reason_code, merchant_reference` (`reference` repeats `merchant_reference`; a refund also names its parent `collection`); `fx_rate` is reserved and null until a payload carries one. Any other event family keeps every flat key, null, plus its payload under `data`. ## Platform integrations Built for CRM, ERP, e-commerce, gaming and billing platforms (for example forex broker CRMs such as FXBO) that know the amount and their own deposit id before the payer is ready: open a hosted checkout session (POST /checkout/sessions), send the payer to its `url`, verify the signed return link and the callback, pay withdrawals by your own `merchant_reference` (POST /payouts, GET /payouts?merchant_reference=) and reconcile with GET /reports/transactions. Any platform that can call a REST API can integrate. The step-by-step guide, with field mapping for connectors and signature code in Node, Python and PHP, is at https://developer.capayglobal.com/platform-integrations.",
    "version": "1.0.0",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.capayglobal.com/v1",
      "type": "string"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string"
    },
    {
      "key": "collectionId",
      "value": "col_7Qm3k1Xa9Zr4",
      "type": "string"
    },
    {
      "key": "payoutId",
      "value": "po_7Hk3mZ0Qr82Yb",
      "type": "string"
    },
    {
      "key": "payoutBatchId",
      "value": "pob_7Kq2mN9xAbCd",
      "type": "string"
    },
    {
      "key": "refundId",
      "value": "rf_3mB8t1Yq5Le0",
      "type": "string"
    },
    {
      "key": "fxQuoteId",
      "value": "fxq_3kP9tW2yNq7Lm4Hd8Sb1Vc6Z",
      "type": "string"
    },
    {
      "key": "eventId",
      "value": "evt_2n4Kq8Zr41Xa",
      "type": "string"
    },
    {
      "key": "endpointId",
      "value": "we_7Qm2Kp0Zr41Xa",
      "type": "string"
    },
    {
      "key": "linkId",
      "value": "pl_9Xw2mB4qR7Ta",
      "type": "string"
    },
    {
      "key": "sessionId",
      "value": "cs_7Qm3k1Xa9Zr4Tb",
      "type": "string"
    },
    {
      "key": "scenarioKey",
      "value": "success",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "whoami",
      "description": "Verify a key and inspect what it can access.",
      "item": [
        {
          "name": "Verify the API key",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/whoami",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "whoami"
              ]
            },
            "description": "Returns the merchant, app, environment and scopes behind the presented key. The recommended first call of every integration."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "providers",
      "description": "Provider, corridor and live health discovery.",
      "item": [
        {
          "name": "List providers and open corridors",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/providers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "providers"
              ]
            },
            "description": "Every enabled and configured provider with its open corridors: countries, currencies, methods, operators, per-corridor amount limits (decimal strings) and live health status. Providers with no open corridors are omitted."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "pricing",
      "description": "Fee transparency before creating anything.",
      "item": [
        {
          "name": "Price a payment before creating it",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/pricing?direction=collection&country=CM&currency=XAF&method=mobile_money&amount=5000",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "pricing"
              ],
              "query": [
                {
                  "key": "direction",
                  "value": "collection",
                  "description": "Whether money is being collected or paid out.",
                  "disabled": false
                },
                {
                  "key": "country",
                  "value": "CM",
                  "description": "ISO 3166-1 alpha-2 country code, e.g. CM.",
                  "disabled": false
                },
                {
                  "key": "currency",
                  "value": "XAF",
                  "description": "ISO 4217 currency code, e.g. XAF.",
                  "disabled": false
                },
                {
                  "key": "method",
                  "value": "mobile_money",
                  "description": "Payment method, e.g. mobile_money, bank, card.",
                  "disabled": false
                },
                {
                  "key": "amount",
                  "value": "5000",
                  "description": "Amount as a decimal string in the given currency (zero-decimal currencies take no fractional part).",
                  "disabled": false
                }
              ]
            },
            "description": "Resolves the exact fee breakdown (provider fee, platform fee, total and who bears them) for a prospective collection or payout. Backed by the same fee resolution the money path uses, so the quote and the charge can never disagree."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "resolve",
      "description": "Confirm who owns a mobile money number or bank account before you pay it, and list bank codes. Read-only: nothing here moves money. Names come only from the provider on the corridor, never guessed.",
      "item": [
        {
          "name": "Resolve an account holder name",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/resolve",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "resolve"
              ]
            },
            "description": "Confirms who owns a mobile money number or bank account before a payout. Names come only from the provider on the corridor (never guessed); a corridor with no name source answers 200 with is_supported=false and reason_code resolve_unsupported. Results are cached per merchant and environment (found names 7 days, not-found answers 60 minutes by default) and served with cached=true. On top of the app quota the endpoint is rate limited per API key (30/min, 2000/day by default; 429 rate_limited with Retry-After). Not a money endpoint: no Idempotency-Key. Requires the payouts:read or payouts:write scope. Sandbox keys answer deterministic fixtures by the last four digits (see the Sandbox guide). bank_transfer is accepted as an alias of bank and normalised.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"country\": \"CM\",\n  \"method\": \"mobile_money\",\n  \"currency\": \"XAF\",\n  \"msisdn\": \"+237670000001\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List bank codes for a country",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/banks?country=NG",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banks"
              ],
              "query": [
                {
                  "key": "country",
                  "value": "NG",
                  "description": "",
                  "disabled": false
                },
                {
                  "key": "type",
                  "value": "bank",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "The bank codes to send as beneficiary.bank_code. Reference data (no scope beyond a valid key). Production keys see only lists a provider synced; sandbox keys also see the simulator's seed banks. An empty list is answered honestly with a note. Served with Cache-Control: private, max-age=3600."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "fx",
      "description": "Approved FX rates and time-boxed quotes. A quote locks a rate for the manual settlement queue if the settlement payout is created before it expires; it does not convert money, does not create a payout, and cross-currency payouts are not offered. Every object carries conversion: manual_settlement_only. Rates need no scope; quotes need fx:read / fx:write.",
      "item": [
        {
          "name": "List approved FX rates",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/fx/rates?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "fx",
                "rates"
              ],
              "query": [
                {
                  "key": "base",
                  "value": "XAF",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "quote",
                  "value": "USD",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Every pair Capay Global treasury has approved for the environment of your key (not filtered to your currencies), each with the mid, the spread applied against you and the resulting rate. Pass both base and quote to read one pair in that orientation (422 fx_pair_unsupported when the pair is not offered, 503 fx_rate_unavailable when it is offered but no approved, fresh rate exists right now); pass neither to list every active pair as stored. A rate older than the freshness window (fx.rate_max_age_hours, 72 h by default) is not listed and cannot be quoted until treasury re-approves it, so what this endpoint lists is exactly what POST /fx/quotes will lock. Sandbox keys see deterministic, deliberately non-market fixture rates (fixture: true) that never go stale. No scope beyond a valid key. Rates inform the manual settlement queue only; nothing here converts money."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List FX quotes",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/fx/quotes?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "fx",
                "quotes"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Your quotes in this environment, newest first, keyset paged. `expired` is derived at read time (an open quote past expires_at). Requires fx:read."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create an FX quote",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Optional on this endpoint. When present, the same reserve-and-replay contract as the authenticated money endpoints applies.",
                "disabled": true
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/fx/quotes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "fx",
                "quotes"
              ]
            },
            "description": "Locks the approved rate for `amount` of `currency` into `to_currency` for the quote TTL (15 minutes by default). The quote applies only if a manual settlement payout is CREATED before expires_at: share the quote id with your account manager when you request settlement. No money moves, no journal is posted and no payout is created; the signature is an HMAC verifiable by Capay Global when the quote is used (verify the terms yourself with GET /fx/quotes/{id}). Amounts round down on the way out (you are paid out). Requires fx:write. Idempotency-Key is optional: a replay returns the stored quote.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"1000000\",\n  \"currency\": \"XAF\",\n  \"to_currency\": \"USD\",\n  \"reference\": \"settle-sep-w1\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve an FX quote",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/fx/quotes/{{fxQuoteId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "fx",
                "quotes",
                "{{fxQuoteId}}"
              ]
            },
            "description": "One quote by fxq_ id (or uuid). After use, used_by names the settlement payout (spo_ id) that consumed it. Requires fx:read."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "collections",
      "description": "Collect payments (mobile money, bank). Card data is never accepted on this surface: any request carrying a PAN, CVV or expiry is rejected with card_data_rejected and nothing is stored.",
      "item": [
        {
          "name": "List collections",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/collections?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "collections"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Newest first. Filter by status; page with limit + starting_after (an unknown cursor answers 404)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a collection",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              },
              {
                "key": "X-Scenario-Key",
                "value": "{{scenarioKey}}",
                "description": "Sandbox only: selects the simulator scenario that scripts this resource's lifecycle. Resolution order: this header wins, then the magic MSISDN suffix (last 4 digits), then the 'success' default. The header is strict: an unknown key or a direction mismatch answers 422 with a validation_errors entry for X-Scenario-Key. Ignored on sk_live_ keys.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/collections",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "collections"
              ]
            },
            "description": "Charge a customer over mobile money or bank rails. The pipeline is validate -> PCI ingress guard -> corridor routing -> limits -> fee -> intent -> risk -> dispatch. Requires an Idempotency-Key; replaying the same key with the same body returns the original resource. Prohibited or unsupported corridors, out-of-bounds amounts and card data are rejected BEFORE anything is stored. A provider dispatch that exceeds 20 seconds answers 201 with status pending and requires_requery true; poll GET /collections/{id} or consume webhooks for the final state.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"5000\",\n  \"currency\": \"XAF\",\n  \"country\": \"CM\",\n  \"method\": \"mobile_money\",\n  \"msisdn\": \"+237650000001\",\n  \"merchant_reference\": \"order-10482\",\n  \"customer\": {\n    \"name\": \"Ada Nkemelu\",\n    \"email\": \"ada@example.com\"\n  },\n  \"metadata\": {\n    \"cart_id\": \"cart_88213\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a collection",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/collections/{{collectionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "collections",
                "{{collectionId}}"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List a collection's status timeline",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/collections/{{collectionId}}/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "collections",
                "{{collectionId}}",
                "events"
              ]
            },
            "description": "Every recorded status transition for the collection, oldest first, with actor, reason and context. The whole timeline comes back in one page (has_more is always false)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Cancel a collection",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/collections/{{collectionId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "collections",
                "{{collectionId}}",
                "cancel"
              ]
            },
            "description": "Only initiated or pending collections can be cancelled; any other status answers 422. No request body is required."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List the stablecoins and networks a payer can use",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/crypto/networks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "crypto",
                "networks"
              ]
            },
            "description": "Reference data for crypto collections (no scope beyond a valid key). Each pair is one {asset, network} a collection can be created with: 16 coins over 12 networks (stablecoins at the USD amount, other coins at a locked price). Any other pair is refused with crypto.network."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "payouts",
      "description": "Send money to mobile money wallets and bank accounts. Payouts at or above the configured approval threshold are created on hold (approval_status PENDING_APPROVAL, hold_reasons AMOUNT_THRESHOLD) and dispatch only after a second person approves in the dashboard. Batch payouts (POST /payouts/batch) create up to 500 payouts in one currency with one reserve and one approval decision on the sum; every item is a regular payout carrying `batch` and `batch_index`.",
      "item": [
        {
          "name": "List your crypto withdrawal wallets",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/crypto/wallets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "crypto",
                "wallets"
              ]
            },
            "description": "The USDT / USDC wallets this business registered in the portal (Settings, Settlement accounts). A wallet is added in the portal only, never through the API, and must be verified by Capay Global (status verified) before it can receive a withdrawal. automatic_from is when withdrawals to it stop needing an approval (48 hours after verification). Needs payouts:read."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List crypto withdrawals",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/crypto/withdrawals?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "crypto",
                "withdrawals"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "awaiting_approval",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Newest first. status filters by awaiting_approval, pending, processing, completed or failed; page with limit + starting_after. Needs payouts:read."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Withdraw your USD balance as USDT or USDC",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/crypto/withdrawals",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "crypto",
                "withdrawals"
              ]
            },
            "description": "Sends part of your available USD balance (the money your crypto collections credited) to one of your verified wallets, 1 USD = 1 USDT / USDC; the network fee is taken from the coin sent. The balance is debited at once. Within your daily automatic limit, to a wallet verified more than 48 hours ago, the withdrawal is sent automatically within about a minute; otherwise it is created with status awaiting_approval and is sent as soon as one Capay Global admin approves it (a rejection returns the money to your balance). The outcome arrives as crypto_withdrawal.completed or crypto_withdrawal.failed. Needs payouts:write and an Idempotency-Key.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"wallet_id\": \"52cb91bc-ddcd-412f-b376-c2dc18042baa\",\n  \"amount\": \"25.00\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a crypto withdrawal",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/crypto/withdrawals/{{payoutId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "crypto",
                "withdrawals",
                "{{payoutId}}"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List payouts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "batch",
                  "value": "",
                  "description": "Only the payouts of this batch (public id pob_... or uuid). An unknown batch answers 404 resource_not_found.",
                  "disabled": true
                },
                {
                  "key": "merchant_reference",
                  "value": "",
                  "description": "Only the payout carrying this merchant_reference (exact match). Unique per app, so the list holds at most one item; an unknown reference answers an empty list.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Newest first. Filter by status, by batch (the items of one batch; an unknown batch answers 404) or by your own merchant_reference (exact: at most one payout, since the reference is unique per app); page with limit + starting_after (an unknown cursor answers 404)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a payout",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              },
              {
                "key": "X-Scenario-Key",
                "value": "{{scenarioKey}}",
                "description": "Sandbox only: selects the simulator scenario that scripts this resource's lifecycle. Resolution order: this header wins, then the magic MSISDN suffix (last 4 digits), then the 'success' default. The header is strict: an unknown key or a direction mismatch answers 422 with a validation_errors entry for X-Scenario-Key. Ignored on sk_live_ keys.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts"
              ]
            },
            "description": "Send money to a mobile money wallet or bank account. The available balance is reserved atomically BEFORE any provider dispatch (insufficient balance answers 402 and nothing moves). At or above the approval threshold the payout is created on hold with approval_status PENDING_APPROVAL and hold_reasons [\"AMOUNT_THRESHOLD\"]; a second person approves it in the dashboard before it is dispatched. Risk and compliance screening run on every payout; a screening hold keeps the reserve locked and dispatches nothing.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"30000\",\n  \"currency\": \"XAF\",\n  \"country\": \"CM\",\n  \"method\": \"mobile_money\",\n  \"msisdn\": \"+237650000001\",\n  \"recipient_name\": \"Awa Ngono\",\n  \"merchant_reference\": \"payroll-2026-08-0142\",\n  \"metadata\": {\n    \"run\": \"august-payroll\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a batch of payouts",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              },
              {
                "key": "X-Scenario-Key",
                "value": "{{scenarioKey}}",
                "description": "Sandbox only: selects the simulator scenario that scripts this resource's lifecycle. Resolution order: this header wins, then the magic MSISDN suffix (last 4 digits), then the 'success' default. The header is strict: an unknown key or a direction mismatch answers 422 with a validation_errors entry for X-Scenario-Key. Ignored on sk_live_ keys.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/batch",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "batch"
              ]
            },
            "description": "Create 1 to 500 payouts in one currency as one batch: one Idempotency-Key, one atomic reserve of the whole total (amount plus merchant-borne fees over every item) and one approval decision on the SUM of the amounts. Validation is all-or-nothing: any invalid item rejects the whole batch with 422, every issue is listed in validation_errors (field paths such as items[7].msisdn, each with a machine-readable code) and no payout is written. Routing, limits and fees resolve per item before anything is inserted; a route failure answers the HTTP status of the first failing code with every failing item listed. If the available balance cannot cover the reserve the batch and its items are recorded as failed (reason_code insufficient_balance), nothing is dispatched and the call answers 402. On 201 every item is initiated and the intake (risk, sanctions screening, holds) runs after the response: poll GET /payouts/batches/{id} or subscribe to the payout.* and payout_batch.* events. In the sandbox an X-Scenario-Key applies to every item.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"currency\": \"XAF\",\n  \"title\": \"March payroll\",\n  \"merchant_reference\": \"payroll-2026-03\",\n  \"allow_duplicate_recipients\": false,\n  \"metadata\": {\n    \"department\": \"ops\"\n  },\n  \"items\": [\n    {\n      \"amount\": \"150000\",\n      \"country\": \"CM\",\n      \"method\": \"mobile_money\",\n      \"msisdn\": \"+237670000001\",\n      \"recipient_name\": \"Amina Njoya\",\n      \"merchant_reference\": \"emp-0017\",\n      \"metadata\": {\n        \"employee_id\": \"17\"\n      }\n    },\n    {\n      \"amount\": \"80000\",\n      \"country\": \"CM\",\n      \"method\": \"mobile_money\",\n      \"msisdn\": \"+237670000002\",\n      \"recipient_name\": \"Paul Etoa\",\n      \"merchant_reference\": \"emp-0018\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List payout batches",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/batches?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "batches"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Batches newest first, without their items. Filter by batch status; page with limit + starting_after (a batch id; an unknown cursor answers 404)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a payout batch",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/batches/{{payoutBatchId}}?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "batches",
                "{{payoutBatchId}}"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "The batch with one page of its items in batch_index order. The items page takes limit (default 25), status (an item status) and starting_after (an item id belonging to THIS batch; any other id answers 404)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Cancel a payout batch",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/batches/{{payoutBatchId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "batches",
                "{{payoutBatchId}}",
                "cancel"
              ]
            },
            "description": "Cancels every item a provider has not accepted yet (initiated, pending or enqueued, never dispatched) with reason_code batch_cancelled and releases their reserves; items already with a provider are left untouched and finish on their own. A batch still awaiting approval is stamped approval_status REJECTED. Only a created or processing batch can be cancelled; a terminal batch answers 422 validation_error on the status field. The JSON body is optional. No Idempotency-Key is read.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Duplicate payroll run\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a payout",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/{{payoutId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "{{payoutId}}"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List a payout's status timeline",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/{{payoutId}}/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "{{payoutId}}",
                "events"
              ]
            },
            "description": "Every recorded status transition for the payout, oldest first, with actor, reason and context. The whole timeline comes back in one page (has_more is always false)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Cancel a payout",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payouts/{{payoutId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payouts",
                "{{payoutId}}",
                "cancel"
              ]
            },
            "description": "Only enqueued or pending payouts can be cancelled (a dispatched payout is with the provider and cannot be recalled); any other status answers 422. Cancelling releases the reserved balance. No request body is required."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "refunds",
      "description": "Refund completed collections, up to the collected net amount.",
      "item": [
        {
          "name": "Create a refund",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              },
              {
                "key": "X-Scenario-Key",
                "value": "{{scenarioKey}}",
                "description": "Sandbox only: selects the simulator scenario that scripts this resource's lifecycle. Resolution order: this header wins, then the magic MSISDN suffix (last 4 digits), then the 'success' default. The header is strict: an unknown key or a direction mismatch answers 422 with a validation_errors entry for X-Scenario-Key. Ignored on sk_live_ keys.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/refunds",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "refunds"
              ]
            },
            "description": "Refund a completed collection, fully (omit amount) or partially. The sum of live refunds can never exceed the collection's net amount. Requires an Idempotency-Key.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"collection_id\": \"col_7Qm3k1Xa9Zr4\",\n  \"amount\": \"3000\",\n  \"reason\": \"Customer returned item\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a refund",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/refunds/{{refundId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "refunds",
                "{{refundId}}"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "balances",
      "description": "Balances per currency and the ledger entries behind them.",
      "item": [
        {
          "name": "List balances",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/balances",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "balances"
              ]
            },
            "description": "One row per currency in the key's environment: available, pending and locked amounts as decimal strings. Always a single page (has_more is always false)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List balance transactions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/balance_transactions?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "balance_transactions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "currency",
                  "value": "",
                  "description": "Only entries in this ISO 4217 currency.",
                  "disabled": true
                },
                {
                  "key": "journal_kind",
                  "value": "",
                  "description": "Exact match on journal_kind, e.g. settlement_payout to list only the manual settlement payouts (spo: journals). Unknown kinds of the documented shape return an empty list; a value outside the pattern (a hyphen, a space, an upper-case letter) is refused with 400 validation_error rather than ignored, so a filter is never silently widened.",
                  "disabled": true
                }
              ]
            },
            "description": "The double-entry ledger entries behind the balances, newest first. Every entry belongs to a balanced journal; quote journal_reference when reconciling. There is no cursor parameter on this endpoint yet: narrow with the currency filter, the journal_kind filter and limit. Entries posted by a manual settlement payout (journal reference spo:...) carry a settlement_payout reference and, when the payout was paid in a stablecoin, an fx line with the executed rate: Capay Global settles by hand and converts nothing automatically, so the line reports what was done, never a promise."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "reports",
      "description": "Reconciliation report: one row per customer money movement (collection, payout, refund) with gross, fee, net, currency, FX rate, status, your own reference and the checkout session behind it, as JSON pages or one streamed CSV. Built for CRM, ERP, shop and billing platforms that reconcile their books against Capay Global.",
      "item": [
        {
          "name": "Reconciliation report",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/reports/transactions?from=&to=",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "reports",
                "transactions"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "",
                  "description": "Start of the window (inclusive), ISO-8601 instant or date.",
                  "disabled": false
                },
                {
                  "key": "to",
                  "value": "",
                  "description": "End of the window (exclusive), ISO-8601 instant or date; after `from` and at most 31 days later.",
                  "disabled": false
                },
                {
                  "key": "kind",
                  "value": "collection",
                  "description": "Restrict to one kind of movement; absent = all three.",
                  "disabled": true
                },
                {
                  "key": "format",
                  "value": "json",
                  "description": "json (paged envelope, the default) or csv (streamed file).",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "100",
                  "description": "Page size for format=json (digits only); ignored for csv.",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "The `id` of the last row of the previous JSON page (col_, po_ or rf_); the page continues strictly after it. Unknown ids answer 404 resource_not_found. Ignored for csv.",
                  "disabled": true
                }
              ]
            },
            "description": "One row per customer money movement in this environment: every collection, payout and refund created in the window, newest first. Each row carries the gross amount, the fee, the net, the currency, the FX rate, the status and your own reference (`reference`: the checkout session's reference for a session-created collection, otherwise the merchant_reference you sent). Refunds have no reference of their own and report the parent collection's reference and session. Settlement payouts (your own withdrawals) are not rows of this report, so `fx_rate` is null today; a payout that is the rail leg of a settlement carries `settlement_payout` (spo_ id) so it can be told apart. The window is at most 31 days (422 report_window_invalid otherwise): page by month for longer spans. JSON pages by `starting_after` = the `id` of the last row received; `format=csv` streams the whole window as one RFC 4180 file (UTF-8, no BOM, CRLF) with the same column names as its header row and ignores `limit` and `starting_after`. The CSV is unbounded for the window and runs in one invocation: page by day for very large windows. `fx_rate` is null today: settlement payouts, the only cross-currency movement, are not rows of this report. Requires balances:read."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "events",
      "description": "The outbound event stream and delivery replay.",
      "item": [
        {
          "name": "List events",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "",
                  "description": "Only events of this exact type.",
                  "disabled": true
                }
              ]
            },
            "description": "The outbound event stream for this merchant and environment, newest first: the same events webhooks deliver, retained 90 days."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Replay an event's webhook deliveries",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/events/{{eventId}}/replay",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "events",
                "{{eventId}}",
                "replay"
              ]
            },
            "description": "Fans the event out to every currently active endpoint whose filter matches (endpoints created after the event included) and re-arms every existing delivery row for it, delivered and failed ones too. No request body is required."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "webhook_endpoints",
      "description": "Manage webhook endpoints and inspect their deliveries.",
      "item": [
        {
          "name": "List webhook endpoints",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhook_endpoints",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhook_endpoints"
              ]
            },
            "description": "Every endpoint in the key's environment, newest first, in one page (has_more is always false). Signing secrets are never returned here."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a webhook endpoint",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhook_endpoints",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhook_endpoints"
              ]
            },
            "description": "Registers an https URL for signed event deliveries. The response is the ONLY place the signing secret (whsec_...) ever appears; store it before responding. Up to 16 active endpoints per environment.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/capay/webhooks\",\n  \"event_types\": [\n    \"collection.completed\",\n    \"collection.failed\",\n    \"payout.completed\"\n  ],\n  \"description\": \"Order fulfilment listener\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List an endpoint's deliveries",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhook_endpoints/{{endpointId}}/deliveries?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhook_endpoints",
                "{{endpointId}}",
                "deliveries"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Delivery attempts to one endpoint, newest first: status, attempt counts, the last HTTP status and error, and timing."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "checkout",
      "description": "Public endpoints behind the hosted payment page (pay.capayglobal.com). No API key is sent or accepted: the payment link id (POST /checkout/initiate) or the unguessable collection public id (GET /checkout/status/{id}) is the credential, and every call is IP rate limited.",
      "item": [
        {
          "name": "Start a payment from a payment link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Optional on this endpoint. When present, the same reserve-and-replay contract as the authenticated money endpoints applies.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/initiate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "initiate"
              ]
            },
            "description": "Public and link-authenticated: the payer's browser on pay.capayglobal.com posts this. No API key is sent; the payment link id in the body is the only credential, and calls are IP rate limited (10 per minute per address). Creates a collection through the exact same pipeline as POST /collections (card data is rejected identically). An Idempotency-Key is optional: the hosted page sends one per form attempt so a double submit replays the same collection.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"link\": \"pl_9Xw2mB4qR7Ta\",\n  \"name\": \"Ada Nkemelu\",\n  \"email\": \"ada@example.com\",\n  \"phone\": \"+237650000001\",\n  \"amount\": \"5000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Poll a checkout's status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/status/{{collectionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "status",
                "{{collectionId}}"
              ]
            },
            "description": "Public: the hosted payment page's live poll. No API key is sent; the unguessable collection public id is the credential, and calls are IP rate limited (90 per minute per address). For a recent in-flight production collection this poll re-fetches the provider's own status before answering, so the payer sees completion within seconds of paying. An in-flight collection past its expiry reads expired (held in_reconciliation payments excepted: money parked at the provider never masquerades as expired).",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "checkout_sessions",
      "description": "Hosted checkout sessions: an API-created deposit the payer completes later on pay.capayglobal.com/s/{id}, built for platforms (broker CRMs, ERPs, billing systems) that know the amount and their own reference before the payer's phone is known. The session mirrors the collection it produces; the browser redirect back to your platform is signed with the one-time return_token.",
      "item": [
        {
          "name": "List checkout sessions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/sessions?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "sessions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "reference",
                  "value": "",
                  "description": "Only sessions created with this reference (exact match).",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                }
              ]
            },
            "description": "Newest first. Filter by your reference (exact) or by status; an open session past its expires_at reads and filters as expired. Page with limit + starting_after (an unknown cursor answers 404)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a hosted checkout session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Correlation-Id",
                "value": "{{$guid}}",
                "description": "Optional caller-supplied trace id (printable ASCII, max 128 chars). Echoed back on the response and recorded in the API usage log.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/sessions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "sessions"
              ]
            },
            "description": "Opens a deposit the payer completes on the returned url. The reference is your own deposit id: while a session for it is still open or processing, repeating the call answers 200 with that session (without the return_token, which is shown once at creation). Return URLs must be https and, when your business has a return-URL allow-list, on a listed domain or a subdomain of one. Refused with 403 checkout_sessions_disabled when the feature is off for your business.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"5000\",\n  \"currency\": \"XAF\",\n  \"reference\": \"DEP-10482\",\n  \"customer\": {\n    \"name\": \"Ada Nkemelu\",\n    \"email\": \"ada@example.com\",\n    \"phone\": \"+237670000001\"\n  },\n  \"return_urls\": {\n    \"success\": \"https://platform.example.com/deposits/DEP-10482/success\",\n    \"failure\": \"https://platform.example.com/deposits/DEP-10482/failure\",\n    \"cancel\": \"https://platform.example.com/deposits/DEP-10482/cancel\"\n  },\n  \"prefill\": {\n    \"country\": \"CM\",\n    \"method\": \"mobile_money\",\n    \"msisdn\": \"+237670000001\"\n  },\n  \"metadata\": {\n    \"client_id\": \"C-88213\"\n  },\n  \"expires_in_seconds\": 1800\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Start the payment for a hosted checkout session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Optional on this endpoint. When present, the same reserve-and-replay contract as the authenticated money endpoints applies.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/sessions/initiate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "sessions",
                "initiate"
              ]
            },
            "description": "Public and session-authenticated: the payer's browser on pay.capayglobal.com/s/{id} posts this with the payer's phone. No API key is sent; the session id in the body is the only credential, and calls share the payment-link initiate IP budget (10 per minute per address). Creates a collection through the exact same pipeline as POST /collections (card data is rejected identically) and moves the session to processing; from then on the session mirrors the collection (completed, failed, cancelled, expired) and emits checkout.session.completed or checkout.session.failed. country and method fall back to the session's prefill, name and email to its customer. A collection refusal (corridor closed, provider unavailable, ...) comes back unchanged and leaves the session open for another attempt. An Idempotency-Key is optional: the hosted page sends one per form attempt so a double submit replays the same collection.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"session\": \"cs_7Qm3k1Xa9Zr4Tb\",\n  \"phone\": \"+237670000001\",\n  \"country\": \"CM\",\n  \"method\": \"mobile_money\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a checkout session",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/sessions/{{sessionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "sessions",
                "{{sessionId}}"
              ]
            },
            "description": "The session with its collection id once the payer has started paying. Reading an open session past its expires_at marks it expired and emits checkout.session.expired once."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Close an open checkout session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/checkout/sessions/{{sessionId}}/expire",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "sessions",
                "{{sessionId}}",
                "expire"
              ]
            },
            "description": "Moves an open session to cancelled so the hosted page stops accepting payment for it. Repeating the call on a cancelled session answers 200 with the same row (safe to retry). An open session already past its expires_at is marked expired instead (checkout.session.expired fires once) and the call answers 409; processing, completed, failed and expired sessions answer 409 session_not_open. No cancel event is emitted: you closed it yourself.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "sandbox",
      "description": "Sandbox-only test helpers. They exist only for sk_test_ keys; a production key answers 403 sandbox_only. Production money is never minted or fast-forwarded.",
      "item": [
        {
          "name": "Fund the sandbox balance",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/test/fund",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test",
                "fund"
              ]
            },
            "description": "Sandbox only: an sk_live_ key answers 403 sandbox_only. Credits the app's available balance so payouts and refunds can be exercised. Each call is capped; an over-cap amount answers 422.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"100000\",\n  \"currency\": \"XAF\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Fast-forward a sandbox resource",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/test/advance",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "test",
                "advance"
              ]
            },
            "description": "Sandbox only: an sk_live_ key answers 403 sandbox_only. Makes every pending simulator step for the resource due immediately, so the next simulator tick applies it: a pending collection completes (or fails, per its scenario) now instead of after the scripted delay.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"resource_type\": \"collection\",\n  \"resource_id\": \"col_7Qm3k1Xa9Zr4\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "plans",
      "description": "The price and schedule a subscription follows. Only mobile money plans can be billed on a schedule today: no live rail offers a pull mandate or a card token, so every renewal is a prompt the customer approves.",
      "item": [
        {
          "name": "List plans",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/plans?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "plans"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "active",
                  "description": "",
                  "disabled": true
                }
              ]
            },
            "description": "Keyset list, newest first; `status` filters active or archived plans."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a plan",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Optional on this endpoint. When present, the same reserve-and-replay contract as the authenticated money endpoints applies.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/plans",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "plans"
              ]
            },
            "description": "The price and the schedule a subscription follows. Only mobile_money plans can be billed today. The collection corridor for country, currency and method must be open in this environment, otherwise the routing error (unsupported_country, corridor_closed, ...) is answered and nothing is written. Idempotency-Key is optional: no money moves.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Pro monthly\",\n  \"description\": \"Full access, billed monthly\",\n  \"amount\": \"5000\",\n  \"currency\": \"XAF\",\n  \"country\": \"CM\",\n  \"method\": \"mobile_money\",\n  \"interval\": \"month\",\n  \"interval_count\": 1,\n  \"trial_days\": 0,\n  \"max_cycles\": null,\n  \"retry_policy\": \"standard\",\n  \"metadata\": {\n    \"tier\": \"pro\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a plan",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/plans/{{planId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "plans",
                "{{planId}}"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Archive a plan",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/plans/{{planId}}/archive",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "plans",
                "{{planId}}",
                "archive"
              ]
            },
            "description": "An archived plan takes no new subscribers; existing subscriptions keep billing. Idempotent: archiving twice answers the same plan."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "subscriptions",
      "description": "Recurring billing on mobile money. Each period Capay opens an ordinary collection and sends the customer a payment prompt; nothing is charged until they approve it. Unapproved prompts follow the plan's retry policy, then the subscription is past due. Events: subscription.*.",
      "item": [
        {
          "name": "List subscriptions",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions?",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "10",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "starting_after",
                  "value": "",
                  "description": "Cursor: return results after this resource id.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "pending",
                  "description": "",
                  "disabled": true
                },
                {
                  "key": "plan",
                  "value": "",
                  "description": "Only subscriptions of this plan (plan_... id).",
                  "disabled": true
                }
              ]
            },
            "description": "Keyset list, newest first. `status` and `plan` (plan_... id) filter."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Create a subscription",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              },
              {
                "key": "X-Scenario-Key",
                "value": "{{scenarioKey}}",
                "description": "Sandbox only: selects the simulator scenario that scripts this resource's lifecycle. Resolution order: this header wins, then the magic MSISDN suffix (last 4 digits), then the 'success' default. The header is strict: an unknown key or a direction mismatch answers 422 with a validation_errors entry for X-Scenario-Key. Ignored on sk_live_ keys.",
                "disabled": true
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions"
              ]
            },
            "description": "Subscribes a customer's mobile money number to a plan. Without a trial the first payment prompt goes out at once and the response carries it as `latest_charge` (an ordinary collection: poll it or receive its webhooks). The customer must approve the prompt on their phone within its expiry; nothing is charged until they do. With a trial the first prompt goes out when the trial ends. In the sandbox, X-Scenario-Key is stored on the subscription so every prompt follows the same simulator path (otherwise the number's suffix rule applies).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"plan\": \"plan_4Kq9xZ2mR7tB\",\n  \"customer\": {\n    \"name\": \"Amina Nkeng\",\n    \"email\": \"amina@example.com\"\n  },\n  \"payment_method\": {\n    \"type\": \"mobile_money\",\n    \"msisdn\": \"+237650000001\"\n  },\n  \"merchant_reference\": \"cust-42-pro\",\n  \"metadata\": {\n    \"crm_id\": \"42\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Retrieve a subscription",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}"
              ]
            },
            "description": "The full subscription plus its last 20 payment prompts under `charges`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "List a subscription's payment prompts",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/charges",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "charges"
              ]
            },
            "description": "Every prompt (one row per period and attempt), newest first, up to 100."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Pause a subscription",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "pause"
              ]
            },
            "description": "Stops the schedule and withdraws an open prompt the customer has not approved yet. Allowed from pending, trialing, active and past_due; pausing a paused subscription answers it unchanged."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Resume a paused subscription",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "resume"
              ]
            },
            "description": "Re-anchors the schedule to now: the next prompt goes out at once and the period numbering continues. Only a paused subscription can be resumed."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Cancel a subscription",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "cancel"
              ]
            },
            "description": "Cancels now (an open prompt is withdrawn; a prompt the customer already approved cannot be, that payment stays refundable) or, with `at_period_end` on a paid subscription, at the end of the paid period. Idempotent: cancelling twice answers the same object.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"at_period_end\": false,\n  \"note\": \"Customer asked\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Send a payment prompt now",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Unique key per logical request (1-255 chars). Retrying with the same key and body replays the stored response (Idempotency-Replayed: true); the same key with a different body answers 422.",
                "disabled": false
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/retry",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "retry"
              ]
            },
            "description": "Opens the next attempt for the unpaid period immediately: allowed for a past_due subscription, or a pending one whose first prompt was not approved, and only when no prompt is open (422 on field `charge`)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Sandbox test clock",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/subscriptions/{{subscriptionId}}/advance",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "subscriptions",
                "{{subscriptionId}}",
                "advance"
              ]
            },
            "description": "Sandbox only: makes the next prompt due now (end of trial, next period or next retry) and sends it, so a monthly plan can be walked through in minutes. Refused while a prompt is open."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "pm.test('response is a Capay envelope', () => {",
                  "  pm.expect(body).to.have.property('status');",
                  "  pm.expect(['success', 'error']).to.include(body.status);",
                  "});",
                  "pm.test('response carries a request_id', () => {",
                  "  const requestId = body.request_id ?? body.error?.request_id;",
                  "  pm.expect(requestId, 'request_id is in the envelope or in error.request_id').to.be.a('string');",
                  "});"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    }
  ]
}
