{
  "info": {
    "_postman_id": "116c8e36-953a-4644-b656-6f06e7d336f7",
    "name": "OnLink Partner API (USDT/KES)",
    "description": "A self-testing collection for the OnLink partner API.\n\n**Set two environment variables — `keyId` and `secret` — and press Run.** Signing is handled by a collection-level pre-request script; you never touch it while using the collection.\n\nThe folders are in integration order, so running top to bottom *is* the tutorial: prove signing works, read where money goes, register a withdrawal address, execute a buy leg and a sell leg, check your balances, then send a bank transfer. Every request asserts its own contract, so `newman run` is a real integration check you can put in your CI.\n\n## Two products, one credential plane\n\nEvery credential is issued for exactly one product. `health`, `admins`, `funding`, `balances`, `transfers` (including batches and confirmations) are common to both; `quotes`, `orders`, `wallets` and `usdt/sends` belong to **`usdt_rails`** only, and `collections` belongs to **`collections`** only. A credential calling a route outside its product gets `403 product_not_supported` — see each folder's own description for which product it belongs to.\n\n## When you port the signing to your own client\n\nRead `docs/partner-api/signing/sign-request.js`. Three rules decide whether the port works, and each of them produces a **valid-looking signature that 401s every time**:\n\n1. **The signed path includes the query string, exactly as sent.** The server signs the request line’s full target, not the routed path.\n2. **The body hash covers the exact bytes you send.** Serialise once, sign that value, send that value. An absent body hashes as `sha256(\"\")`.\n3. **The timestamp is unix milliseconds and must be within ±5 minutes of ours.** A skewed clock 401s a correct implementation.\n\n## Limitations, stated as interface facts\n\n- **This collection targets the sandbox environment** (`baseUrl`). Sandbox mirrors the production contract — the same routes, the same signing scheme, the same response shapes — so an integration built here moves across by changing one variable. Ask your OnLink contact when you are ready for production credentials.\n- **Settlement is asynchronous.** A create returns as soon as the order or transfer exists. Subscribe to webhooks; do not poll to completion.\n- **Your USDT deposit address is permanent**, and a deposit credits your USDT balance rather than any one order — a sell order on the default wallet kind is paid straight from that balance.\n- **A withdrawal address needs two approvals**: your administrator’s code, then OnLink’s. A correct code alone does not make an address usable.\n- **Quotes are short-lived and single-use**, and a degraded rate source returns 503 rather than a rate we could not honour.\n- **A bank transfer is instructed, then confirmed by a one-time code sent to your registered contact** — by default never to your integration. If OnLink has switched your account to webhook delivery, the code arrives on your webhook as `confirmation.code_issued` instead (see `/docs/guides/auto-confirm-instructions`). Nothing reaches the bank until that code comes back on `POST /v1/confirmations/:id`.\n- **120 requests per minute per partner.** Two credentials do not buy two budgets.\n- **Every authentication failure returns the same body**, `{\"error\":\"unauthorized\"}`, so the endpoint cannot be used to probe whether a key exists. Debug against the three rules above.\n- **Your payout account is your own OnLink KES account.** `GET /v1/payout-accounts` lists it and you pass its `payoutAccountId` on a sell; the proceeds land there. There is no `POST /v1/payout-accounts`, and account numbers are returned as the last four digits only.\n- Some steps cannot run unattended: an address confirmation needs a code from a mailbox, and each transfer, batch or USDT-send confirmation needs its own code sent to your registered contact. Those requests skip themselves and say why.\n\nExamples saved on each request are **illustrative**, generated from the published response schemas. They are not recordings of live traffic.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "00 Setup",
      "description": "One authenticated request that touches nothing. Run it first: a 200 here proves your key id, your secret, your clock and your signing are all correct, and every later failure is then about the endpoint rather than about authentication. `health:read` is common to both products (`usdt_rails` and `collections`).\n\nA 401 on this route has several possible causes and the response does not say which of them it was — it says `unauthorized` and nothing else, deliberately, so that the endpoint cannot be used to probe whether a key exists. Work through the four most likely causes in order: an unset secret, a clock more than five minutes out, a key id that is not yet active, and a signature computed over the wrong path.",
      "item": [
        {
          "name": "GET /v1/health — prove signing works",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 — signing is correct end to end', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "pm.test('body is exactly { status: \"ok\" }', function () {",
                  "  const body = pm.response.json();",
                  "  pm.expect(body).to.have.property('status', 'ok');",
                  "  pm.expect(Object.keys(body)).to.eql(['status']);",
                  "});",
                  "",
                  "// On correlating a request with OnLink support:",
                  "//",
                  "// Every ERROR body carries a `requestId`, repeated as the `X-Request-Id`",
                  "// response header. That is the only handle that resolves to your request in",
                  "// OnLink's logs — quote it when you ask us about a failure.",
                  "//",
                  "// A SUCCESSFUL response carries NEITHER, so there is deliberately no",
                  "// assertion for one here. The header is written on the error path only, so a",
                  "// test demanding it on a 200 is red against a healthy API — which is exactly",
                  "// what this collection shipped with, and what this comment exists to stop",
                  "// somebody re-adding. To correlate a successful call, send your own",
                  "// `X-Request-Id`: an inbound one is echoed into the `requestId` of any error",
                  "// on that request.",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/health",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "health"]
            },
            "description": "A liveness signal, and nothing more. It deliberately reports nothing about our dependencies.\n\nRequires the `health:read` scope. A body-less GET, so the signed body hash is `sha256(\"\")` = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. Do not send an explicit `{}` to avoid that: `fetch` forbids a body on a GET, and the server treats an absent body as an empty one."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 reachable",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"status\": \"ok\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 429 over 120 requests per minute",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Too Many Requests",
              "code": 429,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                },
                {
                  "key": "Retry-After-partner",
                  "value": "30"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 429,\n  \"message\": \"ThrottlerException: Too Many Requests\",\n  \"error\": \"TOO_MANY_REQUESTS\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "01 Funding",
      "description": "Where money goes, both ways, and one way to pull KES in. Common to both products.\n\n`GET /v1/funding` and `GET /v1/payout-accounts` are read-only. There is no `POST /v1/payout-accounts`: the destinations are your own OnLink KES accounts, so a leaked credential can list them and choose among them but cannot add one. That is what makes `payoutAccountId` on a sell order a selection rather than an instruction.\n\n`POST /v1/funding/mpesa-push` is the one write here: it raises an M-PESA prompt on a phone you name. It can only ever pull money INTO your own KES account — the destination is resolved server-side, never a request field — and it moves nothing until the payer enters their PIN.",
      "item": [
        {
          "name": "GET /v1/funding — deposit instructions for both legs",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('carries a KES rail list and one USDT rail', function () {",
                  "  pm.expect(body.kes).to.be.an('array');",
                  "  pm.expect(body.usdt).to.be.an('object');",
                  "});",
                  "",
                  "pm.test('the USDT deposit address is rendered in full', function () {",
                  "  // A truncated chain address cannot be distinguished from a lookalike that",
                  "  // differs in the middle, and sending to a lookalike is an irrecoverable",
                  "  // burn. Compare the whole string, never a prefix.",
                  "  pm.expect(body.usdt.address).to.be.a('string');",
                  "  pm.expect(body.usdt.address).to.not.include('…');",
                  "  pm.expect(body.usdt.address).to.not.include('...');",
                  "  pm.expect(body.usdt.chain).to.eql('tron');",
                  "  pm.expect(body.usdt.asset).to.eql('USDT');",
                  "});",
                  "",
                  "pm.test('memo is present and explicitly null on Tron', function () {",
                  "  // Present-and-null, not absent: a client that cannot see the field cannot",
                  "  // tell whether we forgot it or the chain has none, and a deposit sent",
                  "  // without a REQUIRED memo is lost.",
                  "  pm.expect(body.usdt).to.have.property('memo');",
                  "  pm.expect(body.usdt.memo).to.be.null;",
                  "});",
                  "",
                  "pm.test('every KES rail states its own reference explicitly', function () {",
                  "  body.kes.forEach(function (rail) {",
                  "    pm.expect(rail.accountReference).to.be.a('string');",
                  "    pm.expect(rail.currency).to.eql('KES');",
                  "    pm.expect(rail.instructions).to.be.a('string');",
                  "  });",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/funding",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "funding"]
            },
            "description": "Requires `funding:read`.\n\nThe KES rails you send us on and your USDT deposit address. Every reference is an explicit field — integrate against the fields, and show `instructions` to a person.\n\n**Your USDT deposit address is permanent** — a deposit credits your USDT balance, not any one order, and a sell order is paid from that balance.\n\nA 503 here is our side not being ready rather than your request being wrong, which is why it is a 503 and not a 404. It is not self-serve."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 both rails",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"kes\": [\n    {\n      \"rail\": \"mpesa_paybill\",\n      \"paybill\": \"000000\",\n      \"accountReference\": \"00000000123456\",\n      \"beneficiaryName\": \"EXAMPLE PAYMENTS LTD\",\n      \"currency\": \"KES\",\n      \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 000000 > Account number 00000000123456. This tops up your KES balance — it is not tied to any single order, and there is no reference to quote per order.\"\n    },\n    {\n      \"rail\": \"bank_transfer\",\n      \"accountReference\": \"00000000123456\",\n      \"beneficiaryName\": \"EXAMPLE PAYMENTS LTD\",\n      \"currency\": \"KES\",\n      \"instructions\": \"Send KES to account 00000000123456 by PesaLink, RTGS or EFT. This tops up your KES balance — it is not tied to any single order, and there is no reference the narration needs to carry.\"\n    }\n  ],\n  \"usdt\": {\n    \"chain\": \"tron\",\n    \"asset\": \"USDT\",\n    \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n    \"memo\": null,\n    \"instructions\": \"Send USDT on the Tron (TRC-20) network to this address to top up your USDT balance. It is your permanent deposit address: a deposit is credited to your balance, not to any one order, and a sell order is paid from that balance. No memo or tag is used.\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the funding:read scope",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: funding:read.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"funding:read\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no USDT deposit address allocated yet",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"A USDT deposit address has not been allocated for your account yet. Contact OnLink — this is not something you can provision yourself.\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/payout-accounts — your KES destinations",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const list = pm.response.json();",
                  "",
                  "pm.test('an array of destinations', function () {",
                  "  pm.expect(list).to.be.an('array');",
                  "});",
                  "",
                  "pm.test('account numbers are last-4 only, never whole', function () {",
                  "  list.forEach(function (destination) {",
                  "    pm.expect(destination.accountNumberLast4).to.match(/^\\d{4}$/);",
                  "    // The whole number is never returned on this API and is never logged.",
                  "    pm.expect(destination).to.not.have.property('accountNumber');",
                  "  });",
                  "});",
                  "",
                  "pm.test('every destination has an id and a name', function () {",
                  "  list.forEach(function (destination) {",
                  "    pm.expect(destination.payoutAccountId).to.be.a('string');",
                  "    pm.expect(destination.accountName).to.be.a('string');",
                  "  });",
                  "});",
                  "",
                  "// Chain it: the sell leg selects a destination by id.",
                  "if (list.length > 0) {",
                  "  pm.collectionVariables.set('payoutAccountId', list[0].payoutAccountId);",
                  "  console.log('payoutAccountId set to ' + list[0].payoutAccountId);",
                  "} else {",
                  "  console.log(",
                  "    'No active OnLink KES account is listed for this partner — the sell leg ' +",
                  "      'cannot run until one exists.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payout-accounts",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "payout-accounts"]
            },
            "description": "Requires `payout_accounts:read`.\n\nYour OnLink KES account(s): the ids a sell order may name in `payoutAccountId`. The KES is paid into the account you name. Read-only, and only active, named KES accounts are listed.\n\nAccount numbers come back as **the last four digits only**. You select an account by its id and never type its number, which is what stops a credential from redirecting a payout.\n\nThe test below stores the first account in `{{payoutAccountId}}` so the sell leg can run without you copying anything."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 one active destination",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "[\n  {\n    \"payoutAccountId\": \"dd9f1e6a-0000-4000-8000-000000000003\",\n    \"accountNumberLast4\": \"6789\",\n    \"accountName\": \"YOURCOMPANY LIMITED\",\n    \"bankCode\": null,\n    \"label\": \"OnLink KES account\"\n  }\n]"
            },
            {
              "name": "EXAMPLE — 200 no OnLink KES account yet",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "[]"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/funding/mpesa-push — ask a payer to send you KES",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the prompt was sent, nothing has moved yet', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const push = pm.response.json();",
                  "",
                  "pm.test('carries an opaque handle and a pending status', function () {",
                  "  pm.expect(push.pushId).to.be.a('string');",
                  "  pm.expect(push.status).to.eql('pending');",
                  "  pm.expect(Object.keys(push).sort()).to.eql(['pushId', 'status']);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/funding/mpesa-push",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "funding", "mpesa-push"]
            },
            "description": "Requires `funding:write`.\n\nSends an STK Push prompt to the number you name. The payer approves it on their own handset with their PIN — there is no OTP for you to handle and no way to complete it on their behalf. The deposit lands in your own KES account, which is resolved from your verified business and is not something you can specify.\n\nThis returns as soon as the prompt is out, so treat the response as pending: the completion signal is the `credit.received` webhook, and a prompt the payer declines or ignores produces no event at all.\n\n**There is no idempotency key.** A repeated call is a second prompt, and if the payer approves both then both deposits land and each raises its own `credit.received`.\n\nRate-limited well below the plane's 120/min, because every request raises a real prompt on a real phone.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone\": \"254700000000\",\n  \"amount\": \"1000\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 the prompt was sent",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"pushId\": \"dd9f1e6a-0000-4000-8000-000000000004\",\n  \"status\": \"pending\"\n}"
            },
            {
              "name": "EXAMPLE — 400 not a Kenyan mobile number, or a malformed amount",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"phone must be a Kenyan mobile number (+254712345678, 254712345678, 0712345678 or 0112345678).\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the funding:write scope",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: funding:write.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"funding:write\"\n}"
            },
            {
              "name": "EXAMPLE — 429 over the (lower) rate limit for this route",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Too Many Requests",
              "code": 429,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                },
                {
                  "key": "Retry-After-partner",
                  "value": "30"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 429,\n  \"message\": \"ThrottlerException: Too Many Requests\",\n  \"error\": \"TOO_MANY_REQUESTS\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 the bank refused the request (do not retry unchanged)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"MPESA_PUSH_REJECTED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no answer from the bank, or your KES account is not ready",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"MPESA_PUSH_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "02 Addresses",
      "description": "The withdrawal-address registry, and the administrators who approve it. `admins` is common to both products; `wallets` (the withdrawal-address registry itself) belongs to **`usdt_rails`** only — a `collections` credential gets `403 product_not_supported` on every `/v1/wallets` route.\n\nRegister an administrator FIRST. A withdrawal address is approved by a 6-digit code emailed to a registered administrator, so registering an address with no administrator on file is refused (409) rather than creating one nobody can ever confirm.\n\n**Two approvals, not one.** Your administrator’s code moves an address to `pending_onlink_approval`; OnLink reviews it as well. An address is usable for delivery only at `active`. A correct code alone does not make it usable, and this is the part of the flow a first integration most often models wrongly.\n\nThe confirm step needs a code from a mailbox, so an unattended `newman run` cannot complete it. Those requests skip themselves rather than fail; supply `{{walletOtp}}` to run them.",
      "item": [
        {
          "name": "POST /v1/admins — register an approver",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('201 registered', function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "const admin = pm.response.json();",
                  "",
                  "pm.test('carries an id, the email, the name and a status', function () {",
                  "  pm.expect(admin.adminId).to.be.a('string');",
                  "  pm.expect(admin.email).to.be.a('string');",
                  "  pm.expect(admin.name).to.be.a('string');",
                  "  pm.expect(admin.status).to.eql('active');",
                  "});",
                  "",
                  "pm.collectionVariables.set('adminId', admin.adminId);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/admins",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "admins"]
            },
            "description": "Requires `admins:write`.\n\nThe email domain must match the one configured for your partner account, checked as an exact match — subdomains and lookalikes are refused. Change `{{adminEmail}}` and `{{adminName}}` in your environment to a real mailbox you can read, or this flow stops at the confirm step.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{adminEmail}}\",\n  \"name\": \"{{adminName}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 201 registered",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"adminId\": \"c9f2a3b4-5d6e-4708-9a1b-2c3d4e5f6071\",\n  \"email\": \"ops@yourcompany.com\",\n  \"name\": \"Amina Otieno\",\n  \"status\": \"active\"\n}"
            },
            {
              "name": "EXAMPLE — 400 email is not on your configured domain",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Administrator email must be on the yourcompany.com domain.\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/admins — list your approvers",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const list = pm.response.json();",
                  "",
                  "pm.test('an array of active administrators', function () {",
                  "  pm.expect(list).to.be.an('array');",
                  "  list.forEach(function (admin) {",
                  "    pm.expect(admin.status).to.eql('active');",
                  "  });",
                  "});",
                  "",
                  "pm.test('at least one administrator exists to approve an address', function () {",
                  "  // Registering a withdrawal address with none on file is refused (409),",
                  "  // because the approval code has nowhere to go.",
                  "  pm.expect(list.length).to.be.above(0);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/admins",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "admins"]
            },
            "description": "Requires `admins:read`. Active administrators only — a removed one is not listed and cannot approve."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 one active administrator",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "[\n  {\n    \"adminId\": \"c9f2a3b4-5d6e-4708-9a1b-2c3d4e5f6071\",\n    \"email\": \"ops@yourcompany.com\",\n    \"name\": \"Amina Otieno\",\n    \"status\": \"active\"\n  }\n]"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/wallets — register a withdrawal address",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// walletAddress ships EMPTY, and deliberately: the address you register is the",
                  "// address we deliver USDT to, so a value committed into a template is the one",
                  "// thing this file must never contain. Skipped with a named reason rather than",
                  "// sent, because \"address\": \"\" gets a bare 400 that says nothing about the",
                  "// template being what is short.",
                  "const address = (pm.environment.get('walletAddress') || '').trim();",
                  "if (!address) {",
                  "  skip(",
                  "    'SKIPPED: walletAddress is empty. Set it to a Tron address YOU control — ' +",
                  "      'this is where OnLink delivers USDT on the buy leg, so no address is ' +",
                  "      'shipped in the committed template. The address is checksum-validated at ' +",
                  "      'registration, and registering one is not reversible from this run.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('201 registered', function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "const w = pm.response.json();",
                  "",
                  "pm.test('reports a status this API publishes', function () {",
                  "  // A FIRST registration is always pending_partner_approval: two approvals",
                  "  // stand between registration and delivery, and a first integration that",
                  "  // treats registration as completion sends to an address the API will refuse.",
                  "  // Registering an address that is already on file is idempotent, though — the",
                  "  // existing registration comes back at whatever status it has reached — so a",
                  "  // re-run of this collection legitimately reads a later one.",
                  "  pm.expect([",
                  "    'pending_partner_approval',",
                  "    'pending_onlink_approval',",
                  "    'active',",
                  "    'revoked',",
                  "  ]).to.include(w.status);",
                  "});",
                  "",
                  "pm.test('the address is echoed back in full', function () {",
                  "  pm.expect(w.address).to.eql(pm.variables.replaceIn('{{walletAddress}}'));",
                  "});",
                  "",
                  "pm.test('screening status is reported', function () {",
                  "  pm.expect(['pending', 'clear']).to.include(w.screeningStatus);",
                  "});",
                  "",
                  "pm.test('the approval recipient is masked, not disclosed', function () {",
                  "  // approvalSentTo tells you a code went out without republishing a mailbox.",
                  "  if (w.approvalSentTo !== null) {",
                  "    pm.expect(w.approvalSentTo).to.include('•');",
                  "  }",
                  "});",
                  "",
                  "pm.test('nextStep names the endpoint that continues the flow', function () {",
                  "  pm.expect(w.nextStep).to.be.a('string');",
                  "});",
                  "",
                  "pm.collectionVariables.set('walletId', w.walletId);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/wallets",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "wallets"]
            },
            "description": "Requires `wallets:write`.\n\nCreates the address in `pending_partner_approval` and emails a 6-digit code to a registered administrator. The address is **not** usable until your administrator confirms the code AND OnLink approves it.\n\nThe address is validated for format **and base58check checksum** at registration. Failing here is far better than failing mid-order: a checksum-broken address is an irrecoverable burn.\n\nRe-registering the same address is idempotent — the existing registration is returned, at whatever status it has reached, and no second row is created. If that registration is still awaiting your administrator and its code is no longer usable, re-registering **re-issues** a fresh code. That is the recovery path. A code that is still live is never replaced, and no code is issued once the address has moved past your administrator.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"chain\": \"tron\",\n  \"asset\": \"USDT\",\n  \"address\": \"{{walletAddress}}\",\n  \"label\": \"Settlement wallet — Tron\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 201 registered, awaiting your administrator",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"walletId\": \"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60\",\n  \"chain\": \"tron\",\n  \"asset\": \"USDT\",\n  \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"label\": \"Settlement wallet — Tron\",\n  \"status\": \"pending_partner_approval\",\n  \"screeningStatus\": \"pending\",\n  \"createdAt\": \"2026-09-04T09:30:00.000Z\",\n  \"approvalSentTo\": \"o•••@yourcompany.com\",\n  \"nextStep\": \"A 6-digit approval code was emailed to a registered administrator. Confirm with POST /v1/wallets/{id}/confirm. The address is NOT usable until OnLink also approves it.\"\n}"
            },
            {
              "name": "EXAMPLE — 400 the address failed its checksum",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Address is not a valid tron address (base58check, checksum verified).\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 no administrator to send the code to",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"Register an administrator (POST /v1/admins) before adding a withdrawal address — approval codes are sent to a registered administrator.\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/wallets/:id/confirm — your administrator’s code",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{walletId}}, which an earlier request sets. POST /v1/wallets sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('walletId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: walletId is not set. POST /v1/wallets sets it.');",
                  "}",
                  "",
                  "const otp = (pm.environment.get('walletOtp') || '').trim();",
                  "if (!otp) {",
                  "  skip(",
                  "    'SKIPPED: walletOtp is empty. The 6-digit code is emailed to a registered ' +",
                  "      'administrator, so this step cannot run unattended. Read it from that ' +",
                  "      'mailbox and set walletOtp.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 confirmed', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const w = pm.response.json();",
                  "",
                  "pm.test('advances to pending_onlink_approval, NOT to active', function () {",
                  "  // The single most common wrong assumption on this endpoint. A correct code",
                  "  // does not make an address usable; OnLink review does.",
                  "  pm.expect(w.status).to.eql('pending_onlink_approval');",
                  "});",
                  "",
                  "pm.test('the address is unchanged and rendered in full', function () {",
                  "  pm.expect(w.address).to.be.a('string');",
                  "  pm.expect(w.address).to.not.include('…');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/wallets/{{walletId}}/confirm",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "wallets", "{{walletId}}", "confirm"]
            },
            "description": "Requires `wallets:write`.\n\nMoves the address to `pending_onlink_approval` — **not** to `active`. OnLink approves every withdrawal address as well.\n\nAn incorrect code is a 400; an expired or cancelled code, or one past its attempt limit, is a 403. Those are different remedies, which is why they are different statuses: retype the code, versus re-POST the address to `/v1/wallets` for a fresh one.\n\nConfirming an address of yours that has already moved past this step is a 409, not a 403: `WALLET_ALREADY_CONFIRMED` (`details.status` is `pending_onlink_approval` or `active`: your administrator's approval is already recorded, so do not register the address again) or `WALLET_REVOKED`. `details.nextStep` says what to do.\n\nNeeds a code from a mailbox, so it skips itself in an unattended run. Set `{{walletOtp}}` to run it.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"otp\": \"{{walletOtp}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 confirmed, awaiting OnLink review",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"walletId\": \"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60\",\n  \"chain\": \"tron\",\n  \"asset\": \"USDT\",\n  \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"label\": \"Settlement wallet — Tron\",\n  \"status\": \"pending_onlink_approval\",\n  \"screeningStatus\": \"clear\",\n  \"createdAt\": \"2026-09-04T09:30:00.000Z\",\n  \"nextStep\": \"Approved by your administrator. The address is now awaiting OnLink review and is NOT yet usable for delivery.\"\n}"
            },
            {
              "name": "EXAMPLE — 400 incorrect code",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Incorrect approval code.\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 403 the code is no longer usable",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This approval code is no longer usable.\",\n  \"error\": \"Forbidden\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/wallets — list your addresses",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const list = pm.response.json();",
                  "",
                  "pm.test('an array of addresses', function () {",
                  "  pm.expect(list).to.be.an('array');",
                  "});",
                  "",
                  "pm.test('every address carries a known status', function () {",
                  "  const known = [",
                  "    'pending_partner_approval',",
                  "    'pending_onlink_approval',",
                  "    'active',",
                  "    'revoked',",
                  "  ];",
                  "  list.forEach(function (w) {",
                  "    pm.expect(known).to.include(w.status);",
                  "  });",
                  "});",
                  "",
                  "pm.test('addresses are rendered in full, never truncated', function () {",
                  "  list.forEach(function (w) {",
                  "    pm.expect(w.address).to.not.include('…');",
                  "    pm.expect(w.address).to.not.include('...');",
                  "  });",
                  "});",
                  "",
                  "const active = list.filter(function (w) {",
                  "  return w.status === 'active';",
                  "});",
                  "if (active.length > 0) {",
                  "  pm.collectionVariables.set('activeWalletId', active[0].walletId);",
                  "  console.log('activeWalletId set to ' + active[0].walletId);",
                  "} else {",
                  "  pm.collectionVariables.set('activeWalletId', '');",
                  "  console.log(",
                  "    'No active address. Delivery requires BOTH your administrator’s ' +",
                  "      'confirmation and OnLink approval.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallets",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "wallets"]
            },
            "description": "Requires `wallets:read`.\n\nThe test picks the first `active` address into `{{activeWalletId}}`. The buy leg does not need one — it is debit-first and delivers by your wallet KIND rather than a delivery-address id — so nothing downstream in this collection reads `{{activeWalletId}}` any more; it is kept here for your own reference to a confirmed, OnLink-approved address."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 one active, one still pending",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "[\n  {\n    \"walletId\": \"b8e4d1c7-2f39-4a05-8d6b-1c2e3f4a5b60\",\n    \"chain\": \"tron\",\n    \"asset\": \"USDT\",\n    \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n    \"label\": \"Settlement wallet — Tron\",\n    \"status\": \"active\",\n    \"screeningStatus\": \"clear\",\n    \"createdAt\": \"2026-09-04T09:30:00.000Z\"\n  },\n  {\n    \"walletId\": \"e5d4c3b2-a190-4877-8665-544332211000\",\n    \"chain\": \"tron\",\n    \"asset\": \"USDT\",\n    \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n    \"label\": \"Treasury wallet — Tron\",\n    \"status\": \"pending_onlink_approval\",\n    \"screeningStatus\": \"clear\",\n    \"createdAt\": \"2026-09-04T09:30:00.000Z\"\n  }\n]"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the wallets:read scope",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: wallets:read.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"wallets:read\"\n}"
            }
          ]
        },
        {
          "name": "DELETE /v1/wallets/:id — revoke an address",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{walletId}}, which an earlier request sets. POST /v1/wallets sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('walletId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: walletId is not set. POST /v1/wallets sets it.');",
                  "}",
                  "",
                  "if ((pm.environment.get('runRevoke') || '').trim() !== 'true') {",
                  "  skip(",
                  "    'SKIPPED: revocation is irreversible and would destroy the address the ' +",
                  "      'buy leg delivers to. Set runRevoke=true to include it.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('204 revoked', function () {",
                  "  pm.response.to.have.status(204);",
                  "});",
                  "",
                  "pm.test('204 carries no body', function () {",
                  "  pm.expect(pm.response.text()).to.be.oneOf(['', undefined, null]);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallets/{{walletId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "wallets", "{{walletId}}"]
            },
            "description": "Requires `wallets:write`.\n\nImmediate and irreversible. An order whose address is revoked before release will not deliver to it.\n\n204 carries no body. A second DELETE of the same id is a 404, so revocation is not idempotent in its status code even though its effect is.\n\nLeft out of an unattended run on purpose: it destroys the address the buy leg needs. Set `{{runRevoke}}` to `true` to include it."
          },
          "response": [
            {
              "name": "EXAMPLE — 204 revoked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "json",
              "header": [],
              "cookie": [],
              "body": ""
            },
            {
              "name": "EXAMPLE — 404 not found, or already revoked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Wallet not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "DELETE /v1/admins/:id — remove an approver",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{adminId}}, which an earlier request sets. POST /v1/admins sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('adminId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: adminId is not set. POST /v1/admins sets it.');",
                  "}",
                  "",
                  "if ((pm.environment.get('runRevoke') || '').trim() !== 'true') {",
                  "  skip(",
                  "    'SKIPPED: removing your only administrator leaves nobody able to approve ' +",
                  "      'a withdrawal address. Set runRevoke=true to include it.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('204 removed', function () {",
                  "  pm.response.to.have.status(204);",
                  "});",
                  "",
                  "pm.test('204 carries no body', function () {",
                  "  pm.expect(pm.response.text()).to.be.oneOf(['', undefined, null]);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/admins/{{adminId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "admins", "{{adminId}}"]
            },
            "description": "Requires `admins:write`.\n\nAlso lapses any approval code already sent to them: a removed administrator’s mailbox must not still hold a live second factor.\n\nSkipped unless `{{runRevoke}}` is `true`, for the same reason as the address revocation above — removing your only administrator leaves no one able to approve an address."
          },
          "response": [
            {
              "name": "EXAMPLE — 204 removed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "json",
              "header": [],
              "cookie": [],
              "body": ""
            },
            {
              "name": "EXAMPLE — 404 not found, or already removed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Administrator not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "03 Buy leg — KES in, USDT out",
      "description": "Quote, order, poll. Belongs to **`usdt_rails`** only — a `collections` credential gets `403 product_not_supported` on every request in this folder.\n\n**Debit-first.** Creating a buy order debits the KES principal plus fee directly from your own KES balance — there is nothing to pay and no payment instructions to read. `kesDebit` on the order tells you where that debit stands (`reserved`, `confirmed`, `failed`, `reversal_pending` or `reversed`), and `order.funds_confirmed` is sent once when the bank confirms the debit.\n\n**Delivery is asynchronous.** The 202 means the order exists, not that the trade is done. Subscribe to webhooks rather than polling to completion.",
      "item": [
        {
          "name": "POST /v1/quotes (buy) — lock a rate",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('201 quote locked', function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "const quote = pm.response.json();",
                  "",
                  "pm.test('echoes the side asked for', function () {",
                  "  // A sell order will not consume a buy quote (409). The side is not",
                  "  // interchangeable.",
                  "  pm.expect(quote.side).to.eql('buy');",
                  "});",
                  "",
                  "pm.test('has an id, an expiry and a rate', function () {",
                  "  pm.expect(quote.quoteId).to.be.a('string');",
                  "  pm.expect(quote.expiresAt).to.be.a('string');",
                  "  pm.expect(new Date(quote.expiresAt).getTime()).to.be.a('number');",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(quote.kesAmount).to.be.a('string');",
                  "  pm.expect(quote.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(quote.usdtAmount).to.be.a('string');",
                  "  pm.expect(quote.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(quote.rate).to.be.a('string');",
                  "  pm.expect(quote.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "",
                  "pm.test('kesAmount carries exactly 2 decimal places', function () {",
                  "  pm.expect(quote.kesAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('usdtAmount carries exactly 6 decimal places', function () {",
                  "  // Tron USDT precision. Six, always — not \"up to six\".",
                  "  pm.expect(quote.usdtAmount).to.match(/^\\d+\\.\\d{6}$/);",
                  "});",
                  "",
                  "pm.test('kesTotalAmount is a decimal string: principal + fee on a buy', function () {",
                  "  pm.expect(quote.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(quote.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('the disclosed fee is a named component beside the rate', function () {",
                  "  pm.expect(quote.fee.kesAmount).to.be.a('string');",
                  "  pm.expect(quote.fee.currency).to.eql('KES');",
                  "  pm.expect(quote.fee.percentBps).to.be.a('number');",
                  "  pm.expect(quote.fee.flatKesAmount).to.be.a('string');",
                  "});",
                  "",
                  "pm.test('settlementEstimateSeconds is an integer band', function () {",
                  "  // The one non-string field on the quote, deliberately: a duration is not",
                  "  // money, cannot approach 2^53, and is not exchanged for anything.",
                  "  pm.expect(quote.settlementEstimateSeconds).to.be.a('number');",
                  "  pm.expect(quote.settlementEstimateSeconds % 1).to.eql(0);",
                  "});",
                  "",
                  "pm.test('the quote expires in the future', function () {",
                  "  pm.expect(new Date(quote.expiresAt).getTime()).to.be.above(Date.now());",
                  "});",
                  "",
                  "pm.collectionVariables.set('buyQuoteId', quote.quoteId);",
                  "pm.collectionVariables.set('buyQuoteUsdtAmount', quote.usdtAmount);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/quotes",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "quotes"]
            },
            "description": "Requires `quotes:write`.\n\nSingle-use and time-limited. Supply **exactly one** of `kesAmount` or `usdtAmount`; the other is derived, and it rounds in OnLink’s favour. Both or neither is a 400.\n\nQuotes are only ever issued off a live rate. A degraded rate source returns 503 rather than a locked rate we could not honour.\n\n`settlementEstimateSeconds` is a **band**, not a computed duration, and it is a different clock from `expiresAt`: `expiresAt` is how long you have to send, the estimate is how long settlement then takes.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"side\": \"buy\",\n  \"kesAmount\": \"{{buyKesAmount}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 201 buy quote locked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"quoteId\": \"a2d1b0c9-8e7f-4a65-b543-21fedcba9876\",\n  \"side\": \"buy\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.10\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-04T09:31:30.000Z\",\n  \"settlementEstimateSeconds\": 21600\n}"
            },
            {
              "name": "EXAMPLE — 400 both amounts supplied",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Provide exactly one of kesAmount or usdtAmount.\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 400 a malformed amount",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Validation failed on: kesAmount\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no live rate, so no quote is issued",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"A live rate is not available. Quotes are not issued on a degraded rate source.\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/orders/buy — create the order",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{buyQuoteId}}, which an earlier request sets. POST /v1/quotes (buy) sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('buyQuoteId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: buyQuoteId is not set. POST /v1/quotes (buy) sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the order exists, the trade is not done', function () {",
                  "  // Not 201. A 201 would suggest completion; delivery is asynchronous.",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const order = pm.response.json();",
                  "",
                  "pm.test('a new buy order debits your balance and starts at funds_reserved', function () {",
                  "  // Debit-first: the KES leaves your VA balance the instant this order is",
                  "  // created. There is no external payment to wait for and nothing to send.",
                  "  pm.expect(order.status).to.eql('funds_reserved');",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesAmount).to.be.a('string');",
                  "  pm.expect(order.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.usdtAmount).to.be.a('string');",
                  "  pm.expect(order.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(order.rate).to.be.a('string');",
                  "  pm.expect(order.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "",
                  "pm.test('kesTotalAmount is what was actually debited: principal + fee', function () {",
                  "  pm.expect(order.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(order.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('the disclosed fee is snapshotted from the quote', function () {",
                  "  pm.expect(order.fee.kesAmount).to.be.a('string');",
                  "  pm.expect(order.fee.currency).to.eql('KES');",
                  "});",
                  "",
                  "pm.test('kesDebit names where your debit stands, never a payment to make', function () {",
                  "  // No paymentInstructions, no payTo, no paymentReference: there is nowhere",
                  "  // to send KES to on this leg any more.",
                  "  pm.expect(['reserved', 'confirmed', 'failed', 'reversal_pending', 'reversed']).to.include(",
                  "    order.kesDebit.status,",
                  "  );",
                  "  pm.expect(order).to.not.have.property('paymentInstructions');",
                  "  pm.expect(order).to.not.have.property('paymentReference');",
                  "});",
                  "",
                  "pm.collectionVariables.set('orderId', order.orderId);",
                  "pm.collectionVariables.set('buyOrderId', order.orderId);",
                  "console.log('orderId=' + order.orderId + ' kesDebit.status=' + order.kesDebit.status);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/orders/buy",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "orders", "buy"]
            },
            "description": "Requires `orders:write`.\n\nConsumes the buy quote and debits the KES **directly from your own VA balance** the instant the order is created — there is nothing to send and no payment instructions to read. Delivery is asynchronous: nothing is sent until the debit is confirmed and the order reaches a terminal state. Subscribe to webhooks rather than assuming a create means the trade is done.\n\n**202, not 201.** The order exists; nothing has happened to the money.\n\nRetry-safe: creating twice with the same `partnerReference` returns the original order rather than a second one.\n\n`expectedUsdtAmount` is optional and is a guard, not an instruction — supply the amount your own system committed to and the order is refused (409 `EXPECTED_AMOUNT_MISMATCH`) if it does not match the quote. That turns a mispriced order into a refusal instead of a trade.\n\nThere is no `walletId` and no `paymentRail` here: the USDT delivery destination is decided by your wallet KIND, not a delivery-address id supplied on this call.\n\nA 503 names its cause in `message`: `SWAPS_DISABLED`, `MONEY_MOVEMENT_PAUSED`, `ORDER_DAILY_CAP_RATE_UNAVAILABLE`, `TREASURY_ACCOUNT_NOT_REGISTERED`, `TREASURY_ACCOUNT_UNAVAILABLE`, `PARTNER_VA_NOT_ALLOCATED`, `PARTNER_VA_ACTIVATING`, `PARTNER_VA_SUSPENDED`, `KES_BALANCE_UNREADABLE`, `KES_DEBIT_FAILED` (the quote is consumed — a retry needs a new quote and a new `partnerReference`) or `KES_DEBIT_AMBIGUOUS` (we could not confirm the debit's outcome — do not create a new order; re-send with the same `partnerReference` to get the existing order, never a second debit, or wait for the webhook). See /docs/errors.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"quoteId\": \"{{buyQuoteId}}\",\n  \"partnerReference\": \"{{$guid}}\",\n  \"expectedUsdtAmount\": \"{{buyQuoteUsdtAmount}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 created, KES debited and reserved",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n  \"status\": \"funds_reserved\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"kesDebit\": {\n    \"status\": \"reserved\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 404 unknown quoteId",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"quoteId not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the quote expired or was already consumed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"QUOTE_EXPIRED_OR_CONSUMED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the quote is for the other side",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"QUOTE_SIDE_MISMATCH\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 expectedUsdtAmount does not match the quote",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"EXPECTED_AMOUNT_MISMATCH\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 your partner account is suspended",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"PARTNER_SUSPENDED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 over your per-order cap",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"ORDER_EXCEEDS_PER_ORDER_CAP\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 over your rolling 24-hour cap",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"ORDER_EXCEEDS_DAILY_CAP\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 your KES balance cannot cover this quote",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"INSUFFICIENT_KES_BALANCE\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 a platform control, your KES account or the settlement account is not ready (the request description lists every code)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"PARTNER_VA_NOT_ALLOCATED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 the debit did not complete (the quote is consumed — retry needs a new one)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"KES_DEBIT_FAILED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 the debit outcome is unknown (do not retry — wait for the webhook or read the order)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"KES_DEBIT_AMBIGUOUS\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/orders/:id — poll the buy order",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{buyOrderId}}, which an earlier request sets. POST /v1/orders/buy sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('buyOrderId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: buyOrderId is not set. POST /v1/orders/buy sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const order = pm.response.json();",
                  "",
                  "pm.test('it is the order we asked for', function () {",
                  "  pm.expect(order.orderId).to.eql(",
                  "    pm.collectionVariables.get('buyOrderId'),",
                  "  );",
                  "});",
                  "",
                  "pm.test('the status is one this API publishes', function () {",
                  "  pm.expect([",
                  "    'funds_reserved',",
                  "    'awaiting_approval',",
                  "    'rejected',",
                  "    'sending',",
                  "    'settled',",
                  "    'expired',",
                  "    'review',",
                  "  ]).to.include(order.status);",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesAmount).to.be.a('string');",
                  "  pm.expect(order.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.usdtAmount).to.be.a('string');",
                  "  pm.expect(order.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(order.rate).to.be.a('string');",
                  "  pm.expect(order.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('kesTotalAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(order.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('a buy order carries kesDebit, not a deposit address', function () {",
                  "  pm.expect(order.kesDebit).to.be.an('object');",
                  "  pm.expect(order).to.not.have.property('depositAddress');",
                  "  pm.expect(order).to.not.have.property('paymentInstructions');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/orders/{{buyOrderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "orders", "{{buyOrderId}}"]
            },
            "description": "Requires `orders:read`.\n\nOne route for both sides; the shape follows the order’s side. Branch on the presence of `kesDebit` versus `depositAddress`, or keep the side you created it with.\n\nA buy order carries `kesDebit` — where your KES debit stands (`reserved`, `confirmed`, `failed`, `reversal_pending` or `reversed`; treat a value you do not recognise as not final yet) — never payment instructions: the debit is against your own balance, so there is nothing to send.\n\nAnother partner’s order id returns **404, not 403** — a cross-tenant id is indistinguishable from one that does not exist.\n\n**This is not how you learn an order settled.** Polling to completion is what webhooks exist to replace; the terminal-state examples below are here so you can code every branch before one occurs."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 funds_reserved (your KES debit is being confirmed)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n  \"status\": \"funds_reserved\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"kesDebit\": {\n    \"status\": \"reserved\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 awaiting_approval (your debit was confirmed by the bank)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n  \"status\": \"awaiting_approval\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"kesDebit\": {\n    \"status\": \"confirmed\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 settled (terminal: USDT delivered)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n  \"status\": \"settled\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"kesDebit\": {\n    \"status\": \"confirmed\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 rejected (terminal: debit failed or refused, contact OnLink)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n  \"status\": \"rejected\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1293820.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"kesDebit\": {\n    \"status\": \"failed\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 404 not found, or not yours",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Order not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the orders:read scope",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: orders:read.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"orders:read\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "04 Sell leg — USDT in, KES out",
      "description": "Quote, order, poll. Belongs to **`usdt_rails`** only — a `collections` credential gets `403 product_not_supported` on every request in this folder.\n\n**The sell is paid from your USDT balance.** On the default wallet kind (`onlink_managed`) the order debits that balance the instant it is created and goes straight to `usdt_received` — there is no counterparty deposit to wait for and nothing to attach. Top up the balance itself with the address from `GET /v1/funding`.\n\nThe payout destination is **selected**, never described: you name a `payoutAccountId` from `GET /v1/payout-accounts`. A leaked credential cannot redirect a KES payout, because the destination is never a free-text input.\n\n**Payout is asynchronous.** The 202 means the order exists.\n\n`PATCH /v1/orders/{id}` (attaching a transaction hash) still exists on the API for a wallet provisioned as `partner_customer`, but is not part of this documented flow — see the \"Other\" folder.",
      "item": [
        {
          "name": "POST /v1/quotes (sell) — lock a rate",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('201 quote locked', function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "const quote = pm.response.json();",
                  "",
                  "pm.test('echoes side: sell', function () {",
                  "  pm.expect(quote.side).to.eql('sell');",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(quote.kesAmount).to.be.a('string');",
                  "  pm.expect(quote.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(quote.usdtAmount).to.be.a('string');",
                  "  pm.expect(quote.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(quote.rate).to.be.a('string');",
                  "  pm.expect(quote.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "",
                  "pm.test('the USDT leg is exactly what we asked for', function () {",
                  "  // We named the USDT leg, so it comes back unchanged and the KES leg is the",
                  "  // derived one.",
                  "  pm.expect(quote.usdtAmount).to.eql(",
                  "    pm.variables.replaceIn('{{sellUsdtAmount}}'),",
                  "  );",
                  "});",
                  "",
                  "pm.test('kesTotalAmount is a decimal string: principal minus fee on a sell', function () {",
                  "  pm.expect(quote.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(quote.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('the disclosed fee is a named component beside the rate', function () {",
                  "  pm.expect(quote.fee.kesAmount).to.be.a('string');",
                  "  pm.expect(quote.fee.currency).to.eql('KES');",
                  "  pm.expect(quote.fee.percentBps).to.be.a('number');",
                  "  pm.expect(quote.fee.flatKesAmount).to.be.a('string');",
                  "});",
                  "",
                  "pm.test('the quote expires in the future', function () {",
                  "  pm.expect(new Date(quote.expiresAt).getTime()).to.be.above(Date.now());",
                  "});",
                  "",
                  "pm.collectionVariables.set('sellQuoteId', quote.quoteId);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/quotes",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "quotes"]
            },
            "description": "Requires `quotes:write`.\n\nSame endpoint as the buy quote, with `side: \"sell\"`. Here we deliver the KES, so the derived leg rounds the other way — and a sell order will not consume a buy quote (409 `QUOTE_SIDE_MISMATCH`).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"side\": \"sell\",\n  \"usdtAmount\": \"{{sellUsdtAmount}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 201 sell quote locked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"quoteId\": \"f1c0a5d2-3b4e-4a71-9c8d-0e1f2a3b4c5d\",\n  \"side\": \"sell\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.10\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-04T09:31:30.000Z\",\n  \"settlementEstimateSeconds\": 21600\n}"
            },
            {
              "name": "EXAMPLE — 400 neither amount supplied",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Provide exactly one of kesAmount or usdtAmount.\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no live rate, so no quote is issued",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"A live rate is not available. Quotes are not issued on a degraded rate source.\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/orders/sell — create the order",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{sellQuoteId}}, which an earlier request sets. POST /v1/quotes (sell) sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('sellQuoteId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: sellQuoteId is not set. POST /v1/quotes (sell) sets it.');",
                  "}",
                  "",
                  "const destination = (pm.collectionVariables.get('payoutAccountId') || '').trim();",
                  "if (!destination) {",
                  "  skip(",
                  "    'SKIPPED: no payoutAccountId. GET /v1/payout-accounts sets it, and it lists ' +",
                  "      'none for this partner — there is nowhere for the KES to go.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the order exists, the trade is not done', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const order = pm.response.json();",
                  "",
                  "pm.test('a new sell order on the default wallet kind goes straight to usdt_received', function () {",
                  "  // onlink_managed (the default) debits your USDT balance at CREATE — there",
                  "  // is no counterparty deposit to wait for, so this is not awaiting_usdt.",
                  "  pm.expect(order.status).to.eql('usdt_received');",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesAmount).to.be.a('string');",
                  "  pm.expect(order.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.usdtAmount).to.be.a('string');",
                  "  pm.expect(order.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(order.rate).to.be.a('string');",
                  "  pm.expect(order.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "",
                  "pm.test('kesTotalAmount is what we pay you: principal minus fee', function () {",
                  "  pm.expect(order.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(order.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('the disclosed fee is snapshotted from the quote', function () {",
                  "  pm.expect(order.fee.kesAmount).to.be.a('string');",
                  "  pm.expect(order.fee.currency).to.eql('KES');",
                  "});",
                  "",
                  "pm.test('depositAddress is still rendered in full, even though the default flow does not use it', function () {",
                  "  // Present on every response regardless of wallet kind. Never compare a",
                  "  // prefix: a lookalike address differing only in the middle is an",
                  "  // irrecoverable burn.",
                  "  pm.expect(order.depositAddress).to.be.a('string');",
                  "  pm.expect(order.depositAddress).to.not.include('…');",
                  "  pm.expect(order.depositAddress).to.not.include('...');",
                  "  pm.expect(order.chain).to.eql('tron');",
                  "});",
                  "",
                  "pm.test('sendBefore is the same instant as expiresAt', function () {",
                  "  // Two names for one field, both present so you need not know that.",
                  "  pm.expect(new Date(order.sendBefore).getTime()).to.eql(",
                  "    new Date(order.expiresAt).getTime(),",
                  "  );",
                  "});",
                  "",
                  "pm.test('the response says nothing about our position', function () {",
                  "  ['balance', 'float', 'poolDepth', 'settlementEstimateSeconds'].forEach(",
                  "    function (field) {",
                  "      pm.expect(order).to.not.have.property(field);",
                  "    },",
                  "  );",
                  "});",
                  "",
                  "pm.collectionVariables.set('orderId', order.orderId);",
                  "pm.collectionVariables.set('sellOrderId', order.orderId);",
                  "console.log('orderId=' + order.orderId + ' status=' + order.status);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/orders/sell",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "orders", "sell"]
            },
            "description": "Requires `orders:write`.\n\nConsumes the sell quote and, on the default wallet kind, debits your USDT balance directly — the order goes straight to `usdt_received`. **202**, because the order exists and payout has not happened yet.\n\nRetry-safe on `partnerReference`: creating twice with the same value returns the original order.\n\n`payoutAccountId` must be one of your OnLink KES accounts, from `GET /v1/payout-accounts`. One that cannot take a credit right now (frozen, closed or not yet active) is 409 `PAYOUT_ACCOUNT_NOT_PAYABLE`; an unknown one is 404.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"quoteId\": \"{{sellQuoteId}}\",\n  \"partnerReference\": \"{{$guid}}\",\n  \"payoutAccountId\": \"{{payoutAccountId}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 created, USDT debited from your balance",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"usdt_received\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 unknown quoteId or payoutAccountId",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"payoutAccountId not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the quote expired or was already consumed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"QUOTE_EXPIRED_OR_CONSUMED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the quote is for the other side",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"QUOTE_SIDE_MISMATCH\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the payout account cannot take a credit",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"PAYOUT_ACCOUNT_NOT_PAYABLE\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 your partner account is suspended",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"PARTNER_SUSPENDED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 over your rolling 24-hour cap",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"ORDER_EXCEEDS_DAILY_CAP\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 your USDT balance cannot cover this quote",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"INSUFFICIENT_USDT_BALANCE\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no USDT deposit address allocated yet",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"A USDT deposit address has not been allocated for your account yet.\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/orders/:id — poll the sell order",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{sellOrderId}}, which an earlier request sets. POST /v1/orders/sell sets it.",
                  "// Skipped rather than sent, so a partial run reports \"not reached\" instead of a",
                  "// misleading 400 or 404.",
                  "const chained = (pm.collectionVariables.get('sellOrderId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: sellOrderId is not set. POST /v1/orders/sell sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const order = pm.response.json();",
                  "",
                  "pm.test('a sell order carries a deposit address, not payment instructions', function () {",
                  "  pm.expect(order.depositAddress).to.be.a('string');",
                  "  pm.expect(order).to.not.have.property('paymentInstructions');",
                  "});",
                  "",
                  "// Amounts are decimal STRINGS on this API, never JSON numbers: KES cents and",
                  "// USDT micro-units cannot cross 2^53 safely, and JSON.parse would round before",
                  "// any of your code ran. A client that reads them as numbers is wrong even while",
                  "// the values happen to be small.",
                  "pm.test('kesAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesAmount).to.be.a('string');",
                  "  pm.expect(order.kesAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.usdtAmount).to.be.a('string');",
                  "  pm.expect(order.usdtAmount).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('rate is a decimal string, not a number', function () {",
                  "  pm.expect(order.rate).to.be.a('string');",
                  "  pm.expect(order.rate).to.match(/^\\d+\\.\\d+$/);",
                  "});",
                  "pm.test('kesTotalAmount is a decimal string, not a number', function () {",
                  "  pm.expect(order.kesTotalAmount).to.be.a('string');",
                  "  pm.expect(order.kesTotalAmount).to.match(/^\\d+\\.\\d{2}$/);",
                  "});",
                  "",
                  "pm.test('txHash is present, and null unless a hash was attached', function () {",
                  "  pm.expect(order).to.have.property('txHash');",
                  "  if (order.txHash !== null) {",
                  "    pm.expect(order.txHash).to.match(/^[0-9a-f]{64}$/);",
                  "  }",
                  "});",
                  "",
                  "console.log('order ' + order.orderId + ' is ' + order.status);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/orders/{{sellOrderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "orders", "{{sellOrderId}}"]
            },
            "description": "Requires `orders:read`.\n\nThe sell shape: the deposit address, `txHash` (`null` unless one was attached), and — on the default wallet kind — a `status` that reaches `usdt_received` at create, since the balance debit already happened.\n\n`settled`, `rejected` and `expired` are terminal. An order sitting in `review` is not terminal and generates no webhook until it leaves — that is intended rather than a gap, and the remedy is to contact OnLink with the order id.\n\n**Do not poll to completion.** Use webhooks; the examples below are so you can code every terminal branch first."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 awaiting_usdt (partner_customer wallet kind only — not the documented flow)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"awaiting_usdt\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 usdt_received (your balance was debited — the default flow)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"usdt_received\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 settled (terminal: KES paid out)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"settled\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 rejected (terminal: refused, contact OnLink)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"rejected\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 expired (partner_customer wallet kind only — nothing arrived in the window)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"expired\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 review (NOT terminal; no webhook until it leaves)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"review\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"kesTotalAmount\": \"1268180.00\",\n  \"fee\": {\n    \"kesAmount\": \"12820.00\",\n    \"currency\": \"KES\",\n    \"percentBps\": 100,\n    \"flatKesAmount\": \"10.00\",\n    \"capKesAmount\": null\n  },\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 404 not found, or not yours",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Order not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "05 Balances",
      "description": "Your balances. Common to both products (`usdt_rails` and `collections`) — `kes` is the KES account every partner holds. A `collections` credential also gets `usd`, the USD account collections are paid into and that conversions and USD transfers are paid from. A `usdt_rails` credential gets `kes` only.",
      "item": [
        {
          "name": "GET /v1/balances — your balances",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('kes carries a decimal-string available balance and an as-of instant', function () {",
                  "  pm.expect(body.kes.available).to.be.a('string');",
                  "  pm.expect(body.kes.available).to.match(/^\\d+\\.\\d{2}$/);",
                  "  pm.expect(body.kes.currency).to.eql('KES');",
                  "  pm.expect(body.kes.accountNumber).to.be.a('string');",
                  "  pm.expect(new Date(body.kes.asOf).getTime()).to.be.a('number');",
                  "});",
                  "",
                  "pm.test('usd, when present (Collections), is a figure or absent with a named cause — never 0 for a missing account', function () {",
                  "  pm.expect(body).to.not.have.property('usdt');",
                  "  if (body.usd === undefined) return;",
                  "  pm.expect(body.usd.currency).to.eql('USD');",
                  "  if (body.usd.unavailableReason === null) {",
                  "    pm.expect(body.usd.available).to.match(/^\\d+\\.\\d{2}$/);",
                  "    pm.expect(body.usd.accountNumber).to.be.a('string');",
                  "  } else {",
                  "    pm.expect(body.usd.available).to.eql(null);",
                  "    pm.expect(['PARTNER_VA_NOT_ALLOCATED', 'PARTNER_VA_ACTIVATING', 'PARTNER_VA_SUSPENDED']).to.include(body.usd.unavailableReason);",
                  "  }",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/balances",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "balances"]
            },
            "description": "Requires `balances:read`.\n\nThe KES account you hold with us and its available balance, cached for up to 30 seconds — `asOf` tells you how fresh the figure is. A Collections credential also gets `usd`, your USD account; when it cannot be read, `usd.available` is `null` and `usd.unavailableReason` says why."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your balances",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"kes\": {\n    \"available\": \"150000.50\",\n    \"accountNumber\": \"00000000123456\",\n    \"currency\": \"KES\",\n    \"asOf\": \"2026-09-08T12:00:00.000Z\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 your balances (Collections: KES and USD)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"kes\": {\n    \"available\": \"150000.50\",\n    \"accountNumber\": \"00000000123456\",\n    \"currency\": \"KES\",\n    \"asOf\": \"2026-09-08T12:00:00.000Z\"\n  },\n  \"usd\": {\n    \"available\": \"98.50\",\n    \"accountNumber\": \"00000000000000\",\n    \"currency\": \"USD\",\n    \"asOf\": \"2026-09-08T12:00:00.000Z\",\n    \"unavailableReason\": null\n  }\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no KES account allocated yet, or it is not active",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"PARTNER_VA_NOT_ALLOCATED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "06 Transfers",
      "description": "Send money from your KES account: `POST /v1/transfers` writes an instruction and sends a one-time code to your **registered confirmation contact** — by default never to your integration; an account OnLink has switched to webhook delivery receives it on its webhook as `confirmation.code_issued`. Nothing reaches the bank until that code comes back on `POST /v1/confirmations/:id`. Common to both products.\n\nSix rails: `mpesa`, `pesalink`, `rtgs`, `eft`, `swift`, `internal`. Each validates its own destination shape — see `/docs/concepts/rails`.\n\n`amount.currency` is `KES` (paid from your KES account) or, on a `collections` account only, `USD` (paid from your USD account, on `rtgs`, `eft` or `swift` only, to a USD account; nothing is converted). USD on another rail is `422 TRANSFER_RAIL_CURRENCY_UNSUPPORTED`; USD from a `usdt_rails` account is `422 TRANSFER_CURRENCY_NOT_ENABLED`. A USD transfer counts against your KES caps at its KES equivalent, and with no usable rate for that is `503 TRANSFER_DAILY_CAP_RATE_UNAVAILABLE`. See `/docs/guides/send-money-from-your-kes-account#paying-out-usd`.\n\nThe test on the create request stores `{{transferId}}` and `{{confirmationId}}` the same way the buy/sell folders chain their own ids, so the confirm request can run without you copying anything.",
      "item": [
        {
          "name": "POST /v1/transfers — send money from your KES account",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the instruction was created, a confirmation code was sent', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const transfer = pm.response.json();",
                  "",
                  "pm.test('starts at awaiting_confirmation', function () {",
                  "  pm.expect(transfer.status).to.eql('awaiting_confirmation');",
                  "});",
                  "",
                  "pm.test('amount and fee are decimal strings, never numbers', function () {",
                  "  pm.expect(transfer.amount.value).to.be.a('string');",
                  "  pm.expect(transfer.amount.currency).to.eql('KES');",
                  "  pm.expect(transfer.fee.value).to.be.a('string');",
                  "});",
                  "",
                  "pm.test('the confirmation names where the code went, never the code itself', function () {",
                  "  pm.expect(transfer.confirmation.id).to.be.a('string');",
                  "  pm.expect(transfer.confirmation.channel).to.be.oneOf(['email', 'sms']);",
                  "  // Masked with a bullet or an asterisk. chai cannot chain an either-or, so test the pair as one.",
                  "  pm.expect(/[•*]/.test(transfer.confirmation.deliveredTo)).to.be.true;",
                  "});",
                  "",
                  "pm.collectionVariables.set('transferId', transfer.id);",
                  "pm.collectionVariables.set('confirmationId', transfer.confirmation.id);",
                  "console.log('transferId=' + transfer.id + ' confirmationId=' + transfer.confirmation.id);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transfers",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "transfers"]
            },
            "description": "Requires `transfers:write`. Moves value once confirmed — see the folder description.\n\nValidates the destination, verifies the beneficiary name where the rail supports it, checks your caps and balance, then writes the instruction and sends a confirmation code to your registered confirmation contact, or to your webhook if your account uses webhook delivery.\n\n`idempotencyKey` is required: a retried create with the same key and the same body returns the original transfer; the same key with a different body is refused.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rail\": \"mpesa\",\n  \"destination\": {\n    \"accountNumber\": \"254700000000\",\n    \"accountName\": \"ACME Example Payee\"\n  },\n  \"amount\": {\n    \"currency\": \"KES\",\n    \"value\": \"1500.00\"\n  },\n  \"partnerReference\": \"ACME-TRF-000123\",\n  \"idempotencyKey\": \"{{$guid}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 created, a confirmation code was sent",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n  \"status\": \"awaiting_confirmation\",\n  \"rail\": \"mpesa\",\n  \"amount\": {\n    \"currency\": \"KES\",\n    \"value\": \"1500.00\"\n  },\n  \"fee\": {\n    \"currency\": \"KES\",\n    \"value\": \"10.00\"\n  },\n  \"destination\": {\n    \"accountNumber\": \"254700000000\",\n    \"accountName\": \"ACME Example Payee\"\n  },\n  \"nameVerification\": \"verified\",\n  \"verifiedAccountName\": \"ACME EXAMPLE PAYEE\",\n  \"partnerReference\": \"ACME-TRF-000123\",\n  \"createdAt\": \"2026-09-11T00:00:00.000Z\",\n  \"confirmation\": {\n    \"id\": \"df9f1e6a-0000-4000-8000-000000000030\",\n    \"channel\": \"email\",\n    \"deliveredTo\": \"o•••@partner.example\",\n    \"expiresAt\": \"2026-09-11T00:05:00.000Z\",\n    \"resendAvailableAt\": \"2026-09-11T00:00:15.000Z\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 202 created, a USD payout on rtgs (collections account)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"bd9f1e6a-0000-4000-8000-000000000011\",\n  \"status\": \"awaiting_confirmation\",\n  \"rail\": \"rtgs\",\n  \"amount\": {\n    \"currency\": \"USD\",\n    \"value\": \"250.00\"\n  },\n  \"fee\": {\n    \"currency\": \"USD\",\n    \"value\": \"5.00\"\n  },\n  \"destination\": {\n    \"accountNumber\": \"00000000123456\",\n    \"accountName\": \"ACME Example Payee\",\n    \"bankCode\": \"11\"\n  },\n  \"nameVerification\": \"unavailable\",\n  \"partnerReference\": \"ACME-TRF-000124\",\n  \"createdAt\": \"2026-09-11T00:00:00.000Z\",\n  \"confirmation\": {\n    \"id\": \"df9f1e6a-0000-4000-8000-000000000031\",\n    \"channel\": \"email\",\n    \"deliveredTo\": \"o•••@partner.example\",\n    \"expiresAt\": \"2026-09-11T00:05:00.000Z\",\n    \"resendAvailableAt\": \"2026-09-11T00:00:15.000Z\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 404 PARTNER_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"PARTNER_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the same idempotencyKey was used for a different request",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"TRANSFER_IDEMPOTENCY_CONFLICT\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 the beneficiary name did not match",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"TRANSFER_NAME_MISMATCH\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"TRANSFER_NAME_MISMATCH\"\n}"
            },
            {
              "name": "EXAMPLE — 422 this rail does not pay USD (USD on mpesa, pesalink or internal)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"TRANSFER_RAIL_CURRENCY_UNSUPPORTED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"details\": {\n    \"rail\": \"mpesa\",\n    \"currency\": \"USD\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 422 this account cannot send USD (a usdt_rails account)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"TRANSFER_CURRENCY_NOT_ENABLED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"details\": {\n    \"currency\": \"USD\",\n    \"enabledCurrencies\": [\n      \"KES\"\n    ]\n  }\n}"
            },
            {
              "name": "EXAMPLE — 503 no usable rate to count a USD transfer against your caps",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"TRANSFER_DAILY_CAP_RATE_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 transfers are disabled, or your KES account is not ready",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"CONFIRMATION_CONTACT_MISSING\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/confirmations/:id — confirm the transfer",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{confirmationId}}, which an earlier request sets. POST /v1/transfers sets it.",
                  "const chained = (pm.collectionVariables.get('confirmationId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: confirmationId is not set. POST /v1/transfers sets it.');",
                  "}",
                  "",
                  "const code = (pm.environment.get('confirmationCode') || '').trim();",
                  "if (!code) {",
                  "  skip(",
                  "    'SKIPPED: confirmationCode is empty. The code goes to your registered ' +",
                  "      'confirmation contact (or, if OnLink switched your account to webhook ' +",
                  "      'delivery, to your webhook as confirmation.code_issued), so this step ' +",
                  "      'cannot run unattended. Read it off that channel and set confirmationCode.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 confirmed — execution has been handed off', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const result = pm.response.json();",
                  "",
                  "pm.test('names the transfer this confirmation authorises', function () {",
                  "  pm.expect(result.subjectType).to.eql('transfer');",
                  "  pm.expect(result.subjectId).to.eql(pm.collectionVariables.get('transferId'));",
                  "  pm.expect(result.status).to.eql('confirmed');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/confirmations/{{confirmationId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "confirmations", "{{confirmationId}}"]
            },
            "description": "Requires `confirmations:write`. Moves value: verifies the code sent to your registered confirmation contact (or pushed to your webhook, for an account on webhook delivery) and hands the instruction off for execution.\n\n`status: \"confirmed\"` means the code was right; it is not a settlement signal. Poll `GET /v1/transfers/{id}` or subscribe to `transfer.settled` / `transfer.failed` for that.\n\nSet `{{confirmationCode}}` to the code your registered contact read out to run this — on contact delivery it never reaches your integration on its own.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"{{confirmationCode}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 confirmed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"confirmationId\": \"df9f1e6a-0000-4000-8000-000000000030\",\n  \"subjectType\": \"transfer\",\n  \"subjectId\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n  \"status\": \"confirmed\",\n  \"verifiedAt\": \"2026-09-11T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 403 too many wrong codes — the confirmation is locked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"CONFIRMATION_LOCKED\",\n  \"error\": \"Forbidden\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 CONFIRMATION_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"CONFIRMATION_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 CONFIRMATION_CODE_INVALID",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONFIRMATION_CODE_INVALID\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/confirmations/:id/resend — re-issue the code",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{confirmationId}}, which an earlier request sets. POST /v1/transfers sets it.",
                  "const chained = (pm.collectionVariables.get('confirmationId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: confirmationId is not set. POST /v1/transfers sets it.');",
                  "}",
                  "",
                  "if ((pm.environment.get('runResend') || '').trim() !== 'true') {",
                  "  skip(",
                  "    'SKIPPED: a resend invalidates the previous code, and once the confirm ' +",
                  "      'request above has succeeded it is refused (409 CONFIRMATION_ALREADY_VERIFIED). ' +",
                  "      'Set runResend=true to include it, and run it before the code is confirmed.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — a fresh code was sent', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const result = pm.response.json();",
                  "",
                  "pm.test('a new expiry and a new resend window', function () {",
                  "  pm.expect(result.confirmationId).to.be.a('string');",
                  "  pm.expect(new Date(result.expiresAt).getTime()).to.be.above(Date.now());",
                  "  pm.expect(new Date(result.resendAvailableAt).getTime()).to.be.a('number');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/confirmations/{{confirmationId}}/resend",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "confirmations", "{{confirmationId}}", "resend"]
            },
            "description": "Requires `confirmations:write`.\n\nRe-issues the code on the shared escalating resend ladder — see /docs/concepts/caps-and-limits. Refused while the previous code is still within its cooldown window (429 `CONFIRMATION_RESEND_TOO_SOON`), and refused once the code has been confirmed (409 `CONFIRMATION_ALREADY_VERIFIED`). Resending invalidates the previous code, so this is skipped by default; set `{{runResend}}` to `true` to include it, and run it before the confirm request.",
            "body": {
              "mode": "raw",
              "raw": ""
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 a fresh code was sent",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"confirmationId\": \"df9f1e6a-0000-4000-8000-000000000030\",\n  \"expiresAt\": \"2026-09-11T00:15:00.000Z\",\n  \"resendAvailableAt\": \"2026-09-11T00:00:30.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 429 CONFIRMATION_RESEND_TOO_SOON",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Too Many Requests",
              "code": 429,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                },
                {
                  "key": "Retry-After-partner",
                  "value": "30"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 429,\n  \"message\": \"CONFIRMATION_RESEND_TOO_SOON\",\n  \"error\": \"Too Many Requests\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"retryAfterSeconds\": 30\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/transfers/:id — read a transfer",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{transferId}}, which an earlier request sets. POST /v1/transfers sets it.",
                  "const chained = (pm.collectionVariables.get('transferId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: transferId is not set. POST /v1/transfers sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const transfer = pm.response.json();",
                  "",
                  "pm.test('it is the transfer we asked for', function () {",
                  "  pm.expect(transfer.id).to.eql(pm.collectionVariables.get('transferId'));",
                  "});",
                  "",
                  "pm.test('the status is one this API publishes', function () {",
                  "  pm.expect([",
                  "    'awaiting_confirmation',",
                  "    'confirmed',",
                  "    'executing',",
                  "    'processing',",
                  "    'settled',",
                  "    'failed',",
                  "    'expired',",
                  "    'cancelled',",
                  "  ]).to.include(transfer.status);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transfers/{{transferId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "transfers", "{{transferId}}"]
            },
            "description": "Requires `transfers:read`.\n\n**Do not poll to completion.** Subscribe to `transfer.settled` / `transfer.failed` / `confirmation.expired` instead; use this to check on an instruction, not to learn the outcome first."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 settled",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n  \"status\": \"settled\",\n  \"rail\": \"mpesa\",\n  \"amount\": {\n    \"currency\": \"KES\",\n    \"value\": \"1500.00\"\n  },\n  \"fee\": {\n    \"currency\": \"KES\",\n    \"value\": \"10.00\"\n  },\n  \"destination\": {\n    \"accountNumber\": \"254700000000\",\n    \"accountName\": \"ACME Example Payee\"\n  },\n  \"nameVerification\": \"verified\",\n  \"verifiedAccountName\": \"ACME EXAMPLE PAYEE\",\n  \"partnerReference\": \"ACME-TRF-000123\",\n  \"createdAt\": \"2026-09-11T00:00:00.000Z\",\n  \"confirmedAt\": \"2026-09-11T00:02:00.000Z\",\n  \"settledAt\": \"2026-09-11T00:05:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 200 failed (terminal: the bank answered and refused it)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n  \"status\": \"failed\",\n  \"rail\": \"mpesa\",\n  \"amount\": {\n    \"currency\": \"KES\",\n    \"value\": \"1500.00\"\n  },\n  \"fee\": {\n    \"currency\": \"KES\",\n    \"value\": \"10.00\"\n  },\n  \"destination\": {\n    \"accountNumber\": \"254700000000\",\n    \"accountName\": \"ACME Example Payee\"\n  },\n  \"nameVerification\": \"verified\",\n  \"verifiedAccountName\": \"ACME EXAMPLE PAYEE\",\n  \"partnerReference\": \"ACME-TRF-000123\",\n  \"failureCode\": \"PROVIDER_REJECTED\",\n  \"createdAt\": \"2026-09-11T00:00:00.000Z\",\n  \"confirmedAt\": \"2026-09-11T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 TRANSFER_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"TRANSFER_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "07 Batch transfers",
      "description": "`POST /v1/transfers/batch` is a loop over `POST /v1/transfers` behind ONE confirmation code — each item is validated exactly like a single transfer, a failed item is reported with its refusal reason, and the rest proceed. Common to both products. Needs `transfers:write`/`transfers:read`, same as the single-transfer folder.\n\nCaps and your live balance are checked CUMULATIVELY across the items, in submission order — not independently per item.\n\nEvery item must send the same currency: a batch is paid from one account under one code, so a batch mixing `KES` and `USD` items is refused whole with `422 BATCH_CURRENCY_MIXED` (`details.currencies`) and nothing is written. A USD batch (a `collections` account only) is paid from your USD account, and a USD item on a rail other than `rtgs`, `eft` or `swift` fails that item with `TRANSFER_RAIL_CURRENCY_UNSUPPORTED`.",
      "item": [
        {
          "name": "POST /v1/transfers/batch — send N transfers under one confirmation",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the batch was created, a confirmation code was sent', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const batch = pm.response.json();",
                  "",
                  "pm.test('starts at awaiting_confirmation, with a per-item outcome', function () {",
                  "  pm.expect(batch.status).to.eql('awaiting_confirmation');",
                  "  pm.expect(batch.itemCount).to.eql(2);",
                  "  pm.expect(batch.items).to.have.lengthOf(2);",
                  "  batch.items.forEach(function (item) {",
                  "    pm.expect(item).to.have.property('accepted');",
                  "    pm.expect(item).to.have.property('idempotencyKey');",
                  "  });",
                  "});",
                  "",
                  "pm.test('one confirmation covers the whole batch', function () {",
                  "  pm.expect(batch.confirmation.id).to.be.a('string');",
                  "});",
                  "",
                  "pm.collectionVariables.set('batchId', batch.id);",
                  "pm.collectionVariables.set('batchConfirmationId', batch.confirmation.id);",
                  "console.log('batchId=' + batch.id + ' confirmationId=' + batch.confirmation.id);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/transfers/batch",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "transfers", "batch"]
            },
            "description": "Requires `transfers:write`.\n\nEach item in `items` is shaped exactly like a single transfer's create body, including its OWN `idempotencyKey`. The batch itself carries a separate `idempotencyKey` for the whole submission — two different jobs, two different keys. This collection sends a fresh `{{$guid}}` for every key, so each run opens a new batch: resubmitting a batch key replays the original batch, and reusing an item's key is refused for that item (`TRANSFER_IDEMPOTENCY_CONFLICT`).\n\n1 to 100 items. A batch with zero valid items is refused outright (422 `BATCH_NO_VALID_ITEMS`) rather than issuing a code for nothing.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"idempotencyKey\": \"{{$guid}}\",\n  \"items\": [\n    {\n      \"rail\": \"mpesa\",\n      \"destination\": {\n        \"accountNumber\": \"254700000000\",\n        \"accountName\": \"ACME Example Payee\"\n      },\n      \"amount\": {\n        \"currency\": \"KES\",\n        \"value\": \"1500.00\"\n      },\n      \"idempotencyKey\": \"{{$guid}}\"\n    },\n    {\n      \"rail\": \"pesalink\",\n      \"destination\": {\n        \"accountNumber\": \"00000000123456\",\n        \"accountName\": \"ACME Second Payee\",\n        \"bankCode\": \"11\"\n      },\n      \"amount\": {\n        \"currency\": \"KES\",\n        \"value\": \"2500.00\"\n      },\n      \"idempotencyKey\": \"{{$guid}}\"\n    }\n  ]\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 both items accepted",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"be9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"awaiting_confirmation\",\n  \"itemCount\": 2,\n  \"acceptedCount\": 2,\n  \"failedCount\": 0,\n  \"items\": [\n    {\n      \"transferId\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n      \"idempotencyKey\": \"trf-2026-09-16-000001\",\n      \"accepted\": true\n    },\n    {\n      \"transferId\": \"cd9f1e6a-0000-4000-8000-000000000011\",\n      \"idempotencyKey\": \"trf-2026-09-16-000002\",\n      \"accepted\": true\n    }\n  ],\n  \"createdAt\": \"2026-09-16T00:00:00.000Z\",\n  \"confirmation\": {\n    \"id\": \"df9f1e6a-0000-4000-8000-000000000030\",\n    \"channel\": \"email\",\n    \"deliveredTo\": \"o•••@partner.example\",\n    \"expiresAt\": \"2026-09-16T00:05:00.000Z\",\n    \"resendAvailableAt\": \"2026-09-16T00:00:15.000Z\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 422 the items send more than one currency",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"BATCH_CURRENCY_MIXED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"details\": {\n    \"currencies\": [\n      \"KES\",\n      \"USD\"\n    ]\n  }\n}"
            },
            {
              "name": "EXAMPLE — 422 every item failed validation, nothing was written",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"BATCH_NO_VALID_ITEMS\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"BATCH_NO_VALID_ITEMS\"\n}"
            },
            {
              "name": "EXAMPLE — 422 more items than the declared per-request limit",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"BATCH_TOO_LARGE\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"BATCH_TOO_LARGE\"\n}"
            },
            {
              "name": "EXAMPLE — 503 batch transfers are disabled",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"PARTNER_BATCH_TRANSFERS_DISABLED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/confirmations/:id — confirm the batch",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{batchConfirmationId}}, which an earlier request sets. POST /v1/transfers/batch sets it.",
                  "const chained = (pm.collectionVariables.get('batchConfirmationId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: batchConfirmationId is not set. POST /v1/transfers/batch sets it.');",
                  "}",
                  "",
                  "// Every confirmation has its own code, so this does not reuse confirmationCode:",
                  "// a wrong code is one of a limited number of attempts at the REAL confirmation.",
                  "// Set batchConfirmationCode as an environment or a collection variable.",
                  "const code = (pm.variables.get('batchConfirmationCode') || '').trim();",
                  "if (!code) {",
                  "  skip(",
                  "    'SKIPPED: batchConfirmationCode is empty. The code goes to your registered ' +",
                  "      'confirmation contact, never to your integration, so this step cannot ' +",
                  "      'run unattended. Read it off your own channel and set batchConfirmationCode.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 confirmed — execution of the batch has been handed off', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const result = pm.response.json();",
                  "",
                  "pm.test('names the batch this confirmation authorises', function () {",
                  "  pm.expect(result.subjectType).to.eql('batch');",
                  "  pm.expect(result.subjectId).to.eql(pm.collectionVariables.get('batchId'));",
                  "  pm.expect(result.status).to.eql('confirmed');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/confirmations/{{batchConfirmationId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "confirmations", "{{batchConfirmationId}}"]
            },
            "description": "Requires `confirmations:write`. Moves value: one code covers the whole batch.\n\nSame endpoint as the transfer confirmation in \"06 Transfers\", with the `confirmation.id` the batch create returned. `status: \"confirmed\"` means the code was right; it is not a settlement signal — each item posts the ordinary `transfer.settled` / `transfer.failed` event as it settles.\n\nSet `{{batchConfirmationCode}}` (an environment or collection variable) to the code your registered contact read out for THIS batch. It is not `{{confirmationCode}}`: each confirmation has its own code, and a wrong one counts against the attempts you have at the right one.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"{{batchConfirmationCode}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 confirmed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"confirmationId\": \"df9f1e6a-0000-4000-8000-000000000030\",\n  \"subjectType\": \"batch\",\n  \"subjectId\": \"be9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"confirmed\",\n  \"verifiedAt\": \"2026-09-16T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 409 CONFIRMATION_ALREADY_VERIFIED",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"CONFIRMATION_ALREADY_VERIFIED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 CONFIRMATION_CODE_INVALID",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONFIRMATION_CODE_INVALID\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/transfers/batch/:id — read a batch, with every item outcome",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{batchId}}, which an earlier request sets. POST /v1/transfers/batch sets it.",
                  "const chained = (pm.collectionVariables.get('batchId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: batchId is not set. POST /v1/transfers/batch sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const batch = pm.response.json();",
                  "",
                  "pm.test('it is the batch we asked for, with a rolled-up per-item outcome', function () {",
                  "  pm.expect(batch.id).to.eql(pm.collectionVariables.get('batchId'));",
                  "  pm.expect(batch.items).to.be.an('array');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transfers/batch/{{batchId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "transfers", "batch", "{{batchId}}"]
            },
            "description": "Requires `transfers:read`.\n\n`acceptedCount` / `failedCount` roll up as items settle; the batch's own `status` reaches `completed` only once every item has a terminal outcome. There is no separate batch-level webhook — each item posts the ordinary `transfer.settled` / `transfer.failed` event as it settles."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 processing, one item settled so far",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"be9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"processing\",\n  \"itemCount\": 2,\n  \"acceptedCount\": 1,\n  \"failedCount\": 0,\n  \"items\": [\n    {\n      \"transferId\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n      \"idempotencyKey\": \"trf-2026-09-16-000001\",\n      \"accepted\": true\n    },\n    {\n      \"transferId\": \"cd9f1e6a-0000-4000-8000-000000000011\",\n      \"idempotencyKey\": \"trf-2026-09-16-000002\",\n      \"accepted\": true\n    }\n  ],\n  \"createdAt\": \"2026-09-16T00:00:00.000Z\",\n  \"confirmedAt\": \"2026-09-16T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 BATCH_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"BATCH_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "08 USDT sends",
      "description": "`POST /v1/usdt/sends` moves USDT out of your wallet to a registered destination wallet — the outbound mirror of the sell leg's inbound address. Same one-time-code consent gate as a KES transfer (`POST /v1/confirmations/:id`, same endpoint, `subjectType: \"usdt_send\"`). Belongs to **`usdt_rails`** only — a `collections` credential gets `403 product_not_supported` here. Needs `usdt:write`/`usdt:read`. `GET /v1/usdt/balance` reads the USDT you can use now; `GET /v1/usdt/holding-wallet` reads where that USDT is held.\n\nThe destination must already be a `active`, registered wallet of yours — see \"02 Addresses\".",
      "item": [
        {
          "name": "POST /v1/usdt/sends — send USDT to a registered wallet",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{activeWalletId}}, which GET /v1/wallets sets to the",
                  "// first address at status active.",
                  "const active = (pm.collectionVariables.get('activeWalletId') || '').trim();",
                  "if (!active) {",
                  "  skip(",
                  "    'SKIPPED: no active destination wallet. GET /v1/wallets sets ' +",
                  "      'activeWalletId, and delivery requires BOTH your administrator’s ' +",
                  "      'confirmation and OnLink approval.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted — the send exists, a confirmation code was sent', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const send = pm.response.json();",
                  "",
                  "pm.test('starts at awaiting_confirmation', function () {",
                  "  pm.expect(send.status).to.eql('awaiting_confirmation');",
                  "});",
                  "",
                  "pm.test('usdtAmount is a decimal string, not a number', function () {",
                  "  pm.expect(send.usdtAmount).to.be.a('string');",
                  "  pm.expect(send.usdtAmount).to.match(/^\\d+\\.\\d{6}$/);",
                  "});",
                  "",
                  "pm.collectionVariables.set('usdtSendId', send.id);",
                  "pm.collectionVariables.set('usdtSendConfirmationId', send.confirmation.id);",
                  "console.log('sendId=' + send.id + ' confirmationId=' + send.confirmation.id);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/usdt/sends",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usdt", "sends"]
            },
            "description": "Requires `usdt:write`.\n\nChecked against your caps and, if your wallet kind is `onlink_managed`, your USDT ledger balance. Nothing is held aside until the code is confirmed. Retry-safe on `idempotencyKey`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destinationWalletId\": \"{{activeWalletId}}\",\n  \"usdtAmount\": \"25.500000\",\n  \"partnerReference\": \"ACME-SEND-000123\",\n  \"idempotencyKey\": \"{{$guid}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 created, a confirmation code was sent",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"ed9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"awaiting_confirmation\",\n  \"destinationWalletId\": \"fa9f1e6a-0000-4000-8000-000000000050\",\n  \"usdtAmount\": \"25.500000\",\n  \"partnerReference\": \"ACME-SEND-000123\",\n  \"createdAt\": \"2026-09-16T00:00:00.000Z\",\n  \"confirmation\": {\n    \"id\": \"df9f1e6a-0000-4000-8000-000000000030\",\n    \"channel\": \"email\",\n    \"deliveredTo\": \"o•••@partner.example\",\n    \"expiresAt\": \"2026-09-16T00:05:00.000Z\",\n    \"resendAvailableAt\": \"2026-09-16T00:00:15.000Z\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 422 the destination is not a registered, active wallet of yours",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"USDT_SEND_INVALID_DESTINATION\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"USDT_SEND_INVALID_DESTINATION\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/confirmations/:id — confirm the USDT send",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{usdtSendConfirmationId}}, which an earlier request sets. POST /v1/usdt/sends sets it.",
                  "const chained = (pm.collectionVariables.get('usdtSendConfirmationId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: usdtSendConfirmationId is not set. POST /v1/usdt/sends sets it.');",
                  "}",
                  "",
                  "// Every confirmation has its own code, so this does not reuse confirmationCode:",
                  "// a wrong code is one of a limited number of attempts at the REAL confirmation.",
                  "// Set usdtSendConfirmationCode as an environment or a collection variable.",
                  "const code = (pm.variables.get('usdtSendConfirmationCode') || '').trim();",
                  "if (!code) {",
                  "  skip(",
                  "    'SKIPPED: usdtSendConfirmationCode is empty. The code goes to your registered ' +",
                  "      'confirmation contact, never to your integration, so this step cannot ' +",
                  "      'run unattended. Read it off your own channel and set usdtSendConfirmationCode.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 confirmed — execution of the send has been handed off', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const result = pm.response.json();",
                  "",
                  "pm.test('names the USDT send this confirmation authorises', function () {",
                  "  pm.expect(result.subjectType).to.eql('usdt_send');",
                  "  pm.expect(result.subjectId).to.eql(pm.collectionVariables.get('usdtSendId'));",
                  "  pm.expect(result.status).to.eql('confirmed');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/confirmations/{{usdtSendConfirmationId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "confirmations", "{{usdtSendConfirmationId}}"]
            },
            "description": "Requires `confirmations:write`. Moves value: verifies the code and hands the send off for execution.\n\nSame endpoint as the transfer confirmation in \"06 Transfers\", with the `confirmation.id` the send create returned. `status: \"confirmed\"` means the code was right; it is not a settlement signal — subscribe to `usdt_send.settled` / `usdt_send.failed`, or poll `GET /v1/usdt/sends/{id}`.\n\nSet `{{usdtSendConfirmationCode}}` (an environment or collection variable) to the code your registered contact read out for THIS send. It is not `{{confirmationCode}}`: each confirmation has its own code, and a wrong one counts against the attempts you have at the right one.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"{{usdtSendConfirmationCode}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 confirmed",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"confirmationId\": \"df9f1e6a-0000-4000-8000-000000000030\",\n  \"subjectType\": \"usdt_send\",\n  \"subjectId\": \"ed9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"confirmed\",\n  \"verifiedAt\": \"2026-09-16T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 409 CONFIRMATION_ALREADY_VERIFIED",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"CONFIRMATION_ALREADY_VERIFIED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 CONFIRMATION_CODE_INVALID",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONFIRMATION_CODE_INVALID\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/usdt/sends/:id — read a send",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{usdtSendId}}, which an earlier request sets. POST /v1/usdt/sends sets it.",
                  "const chained = (pm.collectionVariables.get('usdtSendId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: usdtSendId is not set. POST /v1/usdt/sends sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const send = pm.response.json();",
                  "",
                  "pm.test('it is the send we asked for', function () {",
                  "  pm.expect(send.id).to.eql(pm.collectionVariables.get('usdtSendId'));",
                  "});",
                  "",
                  "pm.test('the status is one this API publishes', function () {",
                  "  pm.expect([",
                  "    'awaiting_confirmation',",
                  "    'confirmed',",
                  "    'processing',",
                  "    'settled',",
                  "    'failed',",
                  "    'expired',",
                  "  ]).to.include(send.status);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/usdt/sends/{{usdtSendId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usdt", "sends", "{{usdtSendId}}"]
            },
            "description": "Requires `usdt:read`.\n\n**Do not poll to completion.** Subscribe to `usdt_send.settled` / `usdt_send.failed` / `confirmation.expired`. If confirmed but neither settled nor failed, read this rather than sending a second instruction — silence is not failure."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 settled (terminal: USDT left your wallet)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"ed9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"settled\",\n  \"destinationWalletId\": \"fa9f1e6a-0000-4000-8000-000000000050\",\n  \"usdtAmount\": \"25.500000\",\n  \"partnerReference\": \"ACME-SEND-000123\",\n  \"createdAt\": \"2026-09-16T00:00:00.000Z\",\n  \"confirmedAt\": \"2026-09-16T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 200 failed (it did not go out; any reservation was reversed)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"ed9f1e6a-0000-4000-8000-000000000040\",\n  \"status\": \"failed\",\n  \"destinationWalletId\": \"fa9f1e6a-0000-4000-8000-000000000050\",\n  \"usdtAmount\": \"25.500000\",\n  \"partnerReference\": \"ACME-SEND-000123\",\n  \"createdAt\": \"2026-09-16T00:00:00.000Z\",\n  \"confirmedAt\": \"2026-09-16T00:02:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 not found, or not yours",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"USDT_SEND_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/usdt/balance — your USDT balance",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('custody, source and asset agree, and asOf is an instant', function () {",
                  "  pm.expect(['onlink_managed', 'partner_customer']).to.include(body.custody);",
                  "  pm.expect(body.asset).to.eql('USDT');",
                  "  pm.expect(body.chain).to.eql('tron');",
                  "  pm.expect(body.source).to.eql(body.custody === 'onlink_managed' ? 'ledger' : 'chain');",
                  "  pm.expect(new Date(body.asOf).getTime()).to.be.a('number');",
                  "});",
                  "",
                  "pm.test('available is a 6 dp decimal string, or null with a named cause — never 0 for a wallet that could not be read', function () {",
                  "  if (body.unavailableReason === null) {",
                  "    pm.expect(body.available).to.be.a('string');",
                  "    pm.expect(body.available).to.match(/^\\d+\\.\\d{6}$/);",
                  "  } else {",
                  "    pm.expect(body.available).to.eql(null);",
                  "    pm.expect(body.custody).to.eql('partner_customer');",
                  "    pm.expect(['holding_wallet_not_allocated', 'provider_unavailable']).to.include(body.unavailableReason);",
                  "  }",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/usdt/balance",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usdt", "balance"]
            },
            "description": "Requires `usdt:read`.\n\nThe USDT you can use now. If OnLink holds your USDT for you (`custody: onlink_managed`) it is your USDT balance with us (`source: ledger`): it already leaves out USDT committed to a sell order or to a send that has started, and a buy order counts once it has been delivered. If your USDT is in your own wallet (`custody: partner_customer`) it is read from that wallet (`source: chain`) and may be up to 30 seconds old. When your wallet cannot be read, `available` is `null` and `unavailableReason` says why; it is never `0`. A reading, not a reservation."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your USDT balance (OnLink holds your USDT)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"custody\": \"onlink_managed\",\n  \"asset\": \"USDT\",\n  \"chain\": \"tron\",\n  \"available\": \"125.500000\",\n  \"source\": \"ledger\",\n  \"asOf\": \"2026-10-30T14:00:00.000Z\",\n  \"unavailableReason\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 your USDT balance (your own wallet)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"custody\": \"partner_customer\",\n  \"asset\": \"USDT\",\n  \"chain\": \"tron\",\n  \"available\": \"42.000001\",\n  \"source\": \"chain\",\n  \"asOf\": \"2026-10-30T14:00:00.000Z\",\n  \"unavailableReason\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 your wallet could not be read (available is null, never 0)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"custody\": \"partner_customer\",\n  \"asset\": \"USDT\",\n  \"chain\": \"tron\",\n  \"available\": null,\n  \"source\": \"chain\",\n  \"asOf\": \"2026-10-30T14:00:00.000Z\",\n  \"unavailableReason\": \"provider_unavailable\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the usdt:read scope, or is not for this product",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: usdt:read.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"usdt:read\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/usdt/holding-wallet — where your USDT is held",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('custody, asset and chain are the named values, and the address is present in full', function () {",
                  "  pm.expect(['onlink_managed', 'partner_customer']).to.include(body.custody);",
                  "  pm.expect(body.asset).to.eql('USDT');",
                  "  pm.expect(body.chain).to.eql('tron');",
                  "  pm.expect(body.address).to.be.a('string').and.not.empty;",
                  "});",
                  "",
                  "pm.test('receives says what a deposit to this address does for this custody', function () {",
                  "  pm.expect(body.receives).to.be.a('string').and.not.empty;",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/usdt/holding-wallet",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usdt", "holding-wallet"]
            },
            "description": "Requires `usdt:read`.\n\nThe wallet that holds your USDT, and what USDT sent to it does. If OnLink holds your USDT for you (`custody: onlink_managed`) it is your deposit address with us, and USDT sent to it is credited to your USDT balance. If your USDT is in your own wallet (`custody: partner_customer`) it is that wallet: your sells and sends draw from it. It is not one of the withdrawal wallets you register under `/v1/wallets`. Compare the whole address when you verify it.\n\nWith no address allocated for your account yet the answer is `503` with `errorCode: HOLDING_WALLET_NOT_ALLOCATED`: contact us, you cannot set it up yourself."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your holding wallet (OnLink holds your USDT)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"custody\": \"onlink_managed\",\n  \"asset\": \"USDT\",\n  \"chain\": \"tron\",\n  \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"receives\": \"USDT sent to this address is credited to your USDT balance.\"\n}"
            },
            {
              "name": "EXAMPLE — 200 your holding wallet (your own wallet)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"custody\": \"partner_customer\",\n  \"asset\": \"USDT\",\n  \"chain\": \"tron\",\n  \"address\": \"T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb\",\n  \"receives\": \"This is your own wallet: your sells and sends draw from it.\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no holding wallet is allocated for your account yet",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"HOLDING_WALLET_NOT_ALLOCATED: A holding wallet has not been allocated for your account yet. Contact OnLink — this is not something you can set up yourself.\",\n  \"error\": \"SERVICE_UNAVAILABLE\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"HOLDING_WALLET_NOT_ALLOCATED\"\n}"
            },
            {
              "name": "EXAMPLE — 403 this credential lacks the usdt:read scope, or is not for this product",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Forbidden",
              "code": 403,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 403,\n  \"message\": \"This credential is not authorised for this endpoint. Required scope: usdt:read.\",\n  \"error\": \"insufficient_scope\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"usdt:read\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "09 Collections",
      "description": "Open a collection and read it back. Belongs to **`collections`** only — a `usdt_rails` credential gets `403 product_not_supported` on every request in this folder. Needs `collections:write`/`collections:read`.\n\nA contributor pays USD into the returned deposit instructions. `expectedAmount` is advisory only — never used to refuse a deposit. Fee terms are set per partner, and the `fee` object on the response states yours. The saved examples show one set — 1.00% + USD 0.50 flat, uncapped (`percentBps: 100`, `flatMinor: 50`, `capMinor: null`) — under which a gross of USD 100.00 carries a fee of 1.50 and a net of 98.50.",
      "item": [
        {
          "name": "POST /v1/collections — open a collection",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('201 opened', function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "const collection = pm.response.json();",
                  "",
                  "pm.test('starts open, with a minted reference and deposit instructions', function () {",
                  "  pm.expect(collection.status).to.eql('open');",
                  "  pm.expect(collection.reference).to.be.a('string');",
                  "  pm.expect(collection.depositInstructions).to.be.an('object');",
                  "});",
                  "",
                  "pm.test('the fee terms are stated, not a computed amount yet', function () {",
                  "  pm.expect(collection.fee.percentBps).to.be.a('number');",
                  "  pm.expect(collection.fee.currency).to.eql('USD');",
                  "});",
                  "",
                  "pm.collectionVariables.set('collectionId', collection.id);",
                  "console.log('collectionId=' + collection.id + ' reference=' + collection.reference);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/collections",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "collections"]
            },
            "description": "Requires `collections:write`.\n\nMints a reference and returns the USD deposit instructions a contributor pays to. `depositMessage` is present only when this collection shares a deposit account with others — quote it in the transfer or the deposit risks a significant delay or being returned. Absent when the collection has a dedicated account.\n\nSend an optional `Idempotency-Key` header (8 to 64 characters of `A-Z`, `a-z`, `0-9`, `_`, `-`) to make a retry safe: the same key and body returns the same collection; the same key with a different body, or after the collection was edited, is `422 IDEMPOTENCY_KEY_REUSED`; a malformed key is `400 INVALID_IDEMPOTENCY_KEY`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"currency\": \"USD\",\n  \"partnerReference\": \"campaign-2026-09\",\n  \"payerLabel\": \"September community appeal\",\n  \"expectedAmount\": \"100.00\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 201 opened, shared deposit account (depositMessage required)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n  \"reference\": \"DEMO-7K3Q2M\",\n  \"status\": \"open\",\n  \"currency\": \"USD\",\n  \"partnerReference\": \"campaign-2026-09\",\n  \"label\": \"September community appeal\",\n  \"depositInstructions\": {\n    \"beneficiaryName\": \"EXAMPLE PAYMENTS LTD\",\n    \"bankName\": \"Bank of Nowhere\",\n    \"routingNumber\": \"000000000\",\n    \"accountNumber\": \"00000000123456\",\n    \"rails\": [\"ach_push\", \"fednow\", \"wire\"],\n    \"depositMessage\": \"SBX0DEPOSITMESSAGE\"\n  },\n  \"fee\": {\n    \"percentBps\": 100,\n    \"flatMinor\": 50,\n    \"capMinor\": null,\n    \"currency\": \"USD\"\n  },\n  \"expectedAmount\": \"100.00\",\n  \"createdAt\": \"2026-09-22T17:38:46.915Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 PARTNER_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"PARTNER_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 your partner account is suspended",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"PARTNER_SUSPENDED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 currency not supported",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"COLLECTION_CURRENCY_UNSUPPORTED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 collections are disabled, or the deposit wallet is not provisioned",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"PARTNER_COLLECTIONS_DISABLED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/collections — list your collections",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const list = pm.response.json();",
                  "",
                  "pm.test('an array of your own collections, newest first', function () {",
                  "  pm.expect(list).to.be.an('array');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/collections",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "collections"]
            },
            "description": "Requires `collections:read`.\n\nEvery filter is optional: `reference` (exact match on the reference OnLink minted, never parsed for shape), `partnerReference` (exact match on your own reference, 1 to 128 characters), `status`, `from`/`to` (creation time, ISO 8601, `from` inclusive and `to` exclusive)."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 one open collection",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "[\n  {\n    \"id\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n    \"reference\": \"DEMO-7K3Q2M\",\n    \"status\": \"open\",\n    \"currency\": \"USD\",\n    \"partnerReference\": \"campaign-2026-09\",\n    \"label\": \"September community appeal\",\n    \"depositInstructions\": {\n      \"beneficiaryName\": \"EXAMPLE PAYMENTS LTD\",\n      \"bankName\": \"Bank of Nowhere\",\n      \"routingNumber\": \"000000000\",\n      \"accountNumber\": \"00000000123456\",\n      \"rails\": [\"ach_push\", \"fednow\", \"wire\"],\n      \"depositMessage\": \"SBX0DEPOSITMESSAGE\"\n    },\n    \"fee\": {\n      \"percentBps\": 100,\n      \"flatMinor\": 50,\n      \"capMinor\": null,\n      \"currency\": \"USD\"\n    },\n    \"expectedAmount\": \"100.00\",\n    \"createdAt\": \"2026-09-22T17:38:46.915Z\"\n  }\n]"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/collections/:id — read a collection",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{collectionId}}, which an earlier request sets. POST /v1/collections sets it.",
                  "const chained = (pm.collectionVariables.get('collectionId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: collectionId is not set. POST /v1/collections sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const collection = pm.response.json();",
                  "",
                  "pm.test('it is the collection we asked for', function () {",
                  "  pm.expect(collection.id).to.eql(pm.collectionVariables.get('collectionId'));",
                  "});",
                  "",
                  "pm.test('the status is one this API publishes', function () {",
                  "  // `crediting` is the payout in flight (received -> crediting -> credited).",
                  "  // A read in that window is healthy, so it must not fail the run.",
                  "  pm.expect([",
                  "    'open',",
                  "    'received',",
                  "    'crediting',",
                  "    'credited',",
                  "    'refunded',",
                  "    'expired',",
                  "    'cancelled',",
                  "  ]).to.include(collection.status);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/collections/{{collectionId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "collections", "{{collectionId}}"]
            },
            "description": "Requires `collections:read`.\n\nOnce a deposit lands and clears, `status` moves to `received`, then `crediting` while we pay the net amount into your USD account, then `credited` (`collection.received` fires on credit, not on the deposit landing) — `grossAmount`/`feeAmount`/`netAmount` are populated together once the deposit is attributed, never individually. A `credited` collection can later read `refunded`, and no webhook is sent when it does."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 credited (terminal: net amount paid to your USD account)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n  \"reference\": \"DEMO-7K3Q2M\",\n  \"status\": \"credited\",\n  \"currency\": \"USD\",\n  \"partnerReference\": \"campaign-2026-09\",\n  \"label\": \"September community appeal\",\n  \"fee\": {\n    \"percentBps\": 100,\n    \"flatMinor\": 50,\n    \"capMinor\": null,\n    \"currency\": \"USD\"\n  },\n  \"expectedAmount\": \"100.00\",\n  \"grossAmount\": \"100.00\",\n  \"feeAmount\": \"1.50\",\n  \"netAmount\": \"98.50\",\n  \"createdAt\": \"2026-09-22T17:38:46.915Z\"\n}"
            },
            {
              "name": "EXAMPLE — 404 COLLECTION_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"COLLECTION_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/conversions/quote — an indicative USD to KES rate",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const quote = pm.response.json();",
                  "",
                  "pm.test('an indicative rate and the KES it would pay, as decimal strings', function () {",
                  "  pm.expect(quote.indicative).to.eql(true);",
                  "  pm.expect(quote.rate).to.be.a('string');",
                  "  pm.expect(quote.target).to.be.a('string');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/conversions/quote?from=USD&to=KES&amount=10.00",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "conversions", "quote"],
              "query": [
                { "key": "from", "value": "USD" },
                { "key": "to", "value": "KES" },
                { "key": "amount", "value": "10.00" }
              ]
            },
            "description": "Requires `conversions:read`.\n\nThe KES you would be paid per 1 USD now (`rate`, 2 decimal places) and the KES `amount` would become (`target` = `source × rate`, rounded down to the cent). Indicative only: nothing is held or reserved, and the rate is fixed when a conversion executes. Only `from=USD` and `to=KES` are supported."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 indicative quote",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"rate\": \"128.35\",\n  \"source\": \"10.00\",\n  \"target\": \"1283.50\",\n  \"indicative\": true,\n  \"asOf\": \"2026-10-24T10:00:00.000Z\"\n}"
            },
            {
              "name": "EXAMPLE — 422 only USD to KES is converted",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONVERSION_PAIR_UNSUPPORTED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 amount is not a decimal string with at most 2 places",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONVERSION_AMOUNT_INVALID\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 no usable rate right now",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"CONVERSION_RATE_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 conversions are not enabled",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"PARTNER_CONVERSIONS_DISABLED\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/conversions — convert USD to KES",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('202 accepted', function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "",
                  "const conversion = pm.response.json();",
                  "",
                  "pm.test('accepted and running', function () {",
                  "  pm.expect(conversion.id).to.be.a('string');",
                  "  pm.expect(['processing', 'settled', 'failed']).to.include(conversion.status);",
                  "});",
                  "",
                  "pm.collectionVariables.set('conversionId', conversion.id);",
                  "console.log('conversionId=' + conversion.id);",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/conversions",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "conversions"]
            },
            "description": "Requires `conversions:write`.\n\nTakes `amount` USD from your USD account and pays KES into your KES account. No confirmation code. Answers `202` with the conversion `id`; the result arrives as `conversion.settled` or `conversion.failed`, or read it back with `GET /v1/conversions/:id`. `idempotencyKey` (8 to 64 characters of `A-Z`, `a-z`, `0-9`, `_`, `-`) makes a retry safe: the same key and body returns the same conversion; the same key with a different body is `422 IDEMPOTENCY_KEY_REUSED`. The minimum is USD 1.00. This request sends a fresh `{{$guid}}` each run, so every run is a new conversion.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"from\": \"USD\",\n  \"to\": \"KES\",\n  \"amount\": \"10.00\",\n  \"partnerReference\": \"postman-conversion\",\n  \"idempotencyKey\": \"{{$guid}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 accepted",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"c0a8012e-7b1a-4c39-9d3e-2f5c8a1b6d40\",\n  \"status\": \"processing\"\n}"
            },
            {
              "name": "EXAMPLE — 422 not enough USD",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONVERSION_INSUFFICIENT_BALANCE\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 below the USD 1.00 minimum",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONVERSION_AMOUNT_BELOW_MINIMUM\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 above the per-conversion maximum",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"CONVERSION_AMOUNT_ABOVE_MAXIMUM\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 422 the same idempotencyKey was used for a different conversion",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 422,\n  \"message\": \"IDEMPOTENCY_KEY_REUSED\",\n  \"error\": \"Unprocessable Entity\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\",\n  \"errorCode\": \"IDEMPOTENCY_KEY_REUSED\"\n}"
            },
            {
              "name": "EXAMPLE — 503 your USD balance could not be read, nothing moved",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"CONVERSION_BALANCE_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 conversions are temporarily unavailable, retry with the same key",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"CONVERSIONS_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "GET /v1/conversions/:id — read a conversion",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{conversionId}}, which an earlier request sets. POST /v1/conversions sets it.",
                  "const chained = (pm.collectionVariables.get('conversionId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: conversionId is not set. POST /v1/conversions sets it.');",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const conversion = pm.response.json();",
                  "",
                  "pm.test('it is the conversion we asked for', function () {",
                  "  pm.expect(conversion.id).to.eql(pm.collectionVariables.get('conversionId'));",
                  "});",
                  "",
                  "pm.test('the status is one this API publishes', function () {",
                  "  pm.expect(['processing', 'settled', 'failed']).to.include(conversion.status);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/conversions/{{conversionId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "conversions", "{{conversionId}}"]
            },
            "description": "Requires `conversions:read`.\n\n`status` is `processing` until the conversion is `settled` (the KES is in your KES account) or `failed` (no KES paid; any USD taken has been returned). `rate` and `target` are `null` until the rate is fixed, then never change. `failureReason` is set only on a `failed` conversion."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 settled",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"id\": \"c0a8012e-7b1a-4c39-9d3e-2f5c8a1b6d40\",\n  \"status\": \"settled\",\n  \"from\": \"USD\",\n  \"to\": \"KES\",\n  \"source\": \"10.00\",\n  \"target\": \"1283.50\",\n  \"rate\": \"128.35\",\n  \"partnerReference\": \"postman-conversion\",\n  \"failureReason\": null,\n  \"createdAt\": \"2026-10-24T10:00:00.000Z\",\n  \"settledAt\": \"2026-10-24T10:00:41.000Z\",\n  \"failedAt\": null\n}"
            },
            {
              "name": "EXAMPLE — 404 CONVERSION_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"CONVERSION_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "10 Webhooks",
      "description": "How OnLink tells you an order, transfer, send or credit moved, and how you verify that it was us. Applies to both products — the `order.*` and `usdt_send.*`/`usdt_credit.*` families fire only for `usdt_rails`, and the KES ledger family (`credit.received`, `transfer.*`, `confirmation.code_issued`, `confirmation.expired`) fires for both. `collection.received` belongs to the `collections` product.\n\n## Verifying a delivery\n\nThe string to sign is five newline-joined fields with no trailing newline:\n\n```text\nPOST\n<path + query of your webhook URL>\n<unix milliseconds>\n<delivery id>\n<lowercase hex sha256 of the exact request body bytes>\n```\n\nHMAC-SHA256 that with your webhook signing secret and compare, in constant time, against `X-OnLink-Signature: v1=<hex>`. A runnable implementation is `verifyWebhook` in `docs/partner-api/signing/sign-request.js`.\n\nFour things decide whether your verifier works:\n\n1. **Capture the RAW body before any JSON parsing.** A body that has been through `JSON.parse` and back is a different byte string, and the hash is byte-exact.\n2. **Sign the path and query, never the absolute URL.** Use `req.originalUrl` (Express) or `req.url` (Node http). Behind a proxy you cannot know whether we saw your host as `you.example` or `you.example:443`.\n3. **The delivery id occupies the nonce slot** and is stable across our retries. Store the ids you have processed: one value gives you replay protection and idempotency, and a retry reads as the same event.\n4. **Reject a delivery whose timestamp is far from your clock.** We recommend the same ±5 minutes we enforce on requests inbound to us.\n\n`X-OnLink-Event` is a routing hint and is **not signed**. The body’s own `type` is authoritative.\n\n## Delivery semantics\n\nAt-least-once. Retries run on `min(2^n × 15s, 15min)` for 7 attempts and then dead-letter. A 4xx is retried as well, because a 404 from a partner endpoint is far more often a deploy in progress than a considered refusal. **Dedupe on the delivery id.** The URL must be HTTPS — the signature is integrity and authenticity, not confidentiality.\n\n## The order events\n\n| event | fires when | what it is for |\n| --- | --- | --- |\n| `order.funds_confirmed` | your money was attributed to this order | The only signal that distinguishes \"not arrived\" from \"arrived, unattributed\". Both legs map onto this one event, so you write one handler. On the buy leg it is sent once, when the bank confirms the KES debit taken from your balance, and carries `status: \"funds_reserved\"`. On the sell leg's default wallet kind it fires at CREATE, because the debit is immediate. |\n| `order.settled` | the order completed | The completion signal. This is the event the contract exists to deliver. |\n| `order.rejected` | terminal refusal | Without it a rejected order is indistinguishable from a slow one, forever. |\n| `order.expired` | terminal non-settlement | Only reachable on a sell order provisioned `partner_customer` — a debited order is already funded and cannot expire. Distinct from `rejected` because your remedy differs: send again within a new window. |\n\nThere is deliberately **no order-created event** — creation is synchronous and you already have the 202 — and **no event for a status between funds-confirmed and terminal**. An order parked in `review` generates no webhook until it reaches a terminal state. That is the intended behaviour: `GET /v1/orders/{id}` reports the status, and the remedy for a parked order is to contact OnLink with the order id.\n\nThe order payload is a fixed NINE-field `data` object (phase 9 added `feeKesAmount` and `kesTotalAmount` to the original seven). It carries no reason, no provider handle, no settlement estimate and no figure about our position — every field in it is one you can already read on `GET /v1/orders/{id}`.\n\n## The ledger events\n\nCommon to both products. Cover money arriving in your KES or USD account (on USD: a collection payout, and the USD a failed conversion returns), `POST /v1/transfers` and the confirmation code it issues.\n\n| event | fires when | terminal |\n| --- | --- | --- |\n| `credit.received` | money landed in one of your accounts and cleared — KES, or USD for a collection payout. A conversion's KES and the USD a failed conversion returns usually produce one too, but not always: count a conversion on `conversion.settled` | yes |\n| `transfer.settled` | the bank completed the transfer | yes |\n| `transfer.failed` | the bank **answered and refused** the transfer — value never moved | yes |\n| `confirmation.code_issued` | a confirmation code was issued for an instruction you created. Sent only if OnLink has set your account to receive codes by webhook | no |\n| `confirmation.expired` | the confirmation code's window passed before anyone confirmed it | yes |\n\n`confirmation.code_issued` carries a LIVE code in `data.code`: verify the signature before reading it and never log it. It is never stored and never redelivered later — up to three attempts, all over within about 35 seconds, each waiting at most 5 seconds for your answer, then we stop. If you miss it, call `POST /v1/confirmations/{id}/resend` for a new code (under a new delivery id).\n\n`transfer.settled` / `transfer.failed` carry a different `data` shape from the order family (`transferId`, `rail`, `amount`, `fee`, `destinationAccountNumber`, `failureCode`, `settledAt`). `confirmation.expired` is keyed on the CONFIRMATION, not the transfer (or the send) — its `subjectId` is the confirmation id, and `subjectType` is `transfer`, `batch` or `usdt_send`. `credit.received` has no prior identifier you could have polled: it is the first time you see the transaction.\n\n## The USDT send events (usdt_rails only)\n\n| event | fires when | terminal |\n| --- | --- | --- |\n| `usdt_send.settled` | the USDT left your wallet and reached the destination | yes |\n| `usdt_send.failed` | it did not go out — any ledger reservation is reversed first | yes |\n| `usdt_credit.received` | USDT landed at your deposit address | yes |\n\n`usdt_send.*` shares its `data` shape with the transfer family (`sendId`, `amount`, `destinationWalletId`, `failureReason`). `usdt_credit.received` carries no order, no tx hash and no prior identifier — `data.activityId` is the first handle you see for that credit and your idempotency anchor.\n\n## The collection event (collections only)\n\n| event | fires when | terminal |\n| --- | --- | --- |\n| `collection.received` | a contribution was paid out, net of fee, to your USD account — sent when the collection becomes `credited`, not when the deposit first arrives | no — nothing follows it, but the collection can later read `refunded`, and no webhook is sent when it does |\n\n`data` carries `collectionId`, `reference`, `gross`, `fee`, `net`, `currency`, `senderName` and `treasuryCreditTxId`; reconcile against `net`.\n\n## Running this folder\n\nEach request below **replays a sample signed delivery against your own endpoint**, so you can test your handler before a real order exists. Set `webhookUrl` and `webhookSecret` in your environment; with either unset the requests skip themselves, because an unsigned webhook is never sent.",
      "item": [
        {
          "name": "order.funds_confirmed (sell leg) → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"d41f9b7e-2c3a-4d5e-8f60-718293a4b5c6\",\n  \"type\": \"order.funds_confirmed\",\n  \"createdAt\": \"2026-09-04T09:45:12.000Z\",\n  \"data\": {\n    \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"sell\",\n    \"status\": \"usdt_received\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\",\n    \"feeKesAmount\": \"12820.00\",\n    \"kesTotalAmount\": \"1268180.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Your USDT was attributed to this order — on the default wallet kind this fires at create, since the balance debit is immediate. One event, one handler."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "order.funds_confirmed (buy leg) → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"e52a0c8f-3d4b-4e6f-9071-8293a4b5c6d7\",\n  \"type\": \"order.funds_confirmed\",\n  \"createdAt\": \"2026-09-04T09:47:03.000Z\",\n  \"data\": {\n    \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"buy\",\n    \"status\": \"funds_reserved\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\",\n    \"feeKesAmount\": \"12820.00\",\n    \"kesTotalAmount\": \"1293820.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The bank confirmed the KES debit taken from your balance for this buy order. Sent once, and carries `status: \"funds_reserved\"` — the order moves on right after it, so `GET /v1/orders/{id}` may already report a later status. The same event as the sell leg's: one event, one handler."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "order.settled → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"f63b1d90-4e5c-4f70-8182-93a4b5c6d7e8\",\n  \"type\": \"order.settled\",\n  \"createdAt\": \"2026-09-04T10:02:41.000Z\",\n  \"data\": {\n    \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"sell\",\n    \"status\": \"settled\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\",\n    \"feeKesAmount\": \"12820.00\",\n    \"kesTotalAmount\": \"1268180.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Terminal. The order completed. This is the event to act on rather than polling `GET /v1/orders/{id}` to completion."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "order.rejected → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"a74c2e01-5f6d-4a81-9293-a4b5c6d7e8f9\",\n  \"type\": \"order.rejected\",\n  \"createdAt\": \"2026-09-04T10:11:08.000Z\",\n  \"data\": {\n    \"orderId\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"buy\",\n    \"status\": \"rejected\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\",\n    \"feeKesAmount\": \"12820.00\",\n    \"kesTotalAmount\": \"1293820.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Terminal refusal. No reason is carried: the reasons name internal controls, and your remedy is to contact OnLink with the order id, which no payload field improves."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "order.expired → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"b85d3f12-6a7e-4b92-83a4-b5c6d7e8f901\",\n  \"type\": \"order.expired\",\n  \"createdAt\": \"2026-09-04T10:30:00.000Z\",\n  \"data\": {\n    \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"sell\",\n    \"status\": \"expired\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\",\n    \"feeKesAmount\": \"12820.00\",\n    \"kesTotalAmount\": \"1268180.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Terminal non-settlement — nothing arrived inside the window. Only reachable on a sell order provisioned `partner_customer`: the default wallet kind debits your balance at create and is already funded, so it cannot expire. Distinct from `rejected` because the remedy differs: quote again and send inside the new window."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "transfer.settled → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"5b2e9f14-0000-4000-8000-000000000040\",\n  \"type\": \"transfer.settled\",\n  \"createdAt\": \"2026-09-11T00:05:00.000Z\",\n  \"data\": {\n    \"transferId\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n    \"partnerReference\": \"ACME-TRF-000123\",\n    \"rail\": \"mpesa\",\n    \"status\": \"settled\",\n    \"amount\": {\n      \"currency\": \"KES\",\n      \"value\": \"1500.00\"\n    },\n    \"fee\": {\n      \"currency\": \"KES\",\n      \"value\": \"10.00\"\n    },\n    \"destinationAccountNumber\": \"254700000000\",\n    \"failureCode\": null,\n    \"settledAt\": \"2026-09-11T00:05:00.000Z\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The bank completed the transfer. Terminal — this is the event to act on rather than polling `GET /v1/transfers/{id}` to completion."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "transfer.failed → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"5b2e9f14-0000-4000-8000-000000000041\",\n  \"type\": \"transfer.failed\",\n  \"createdAt\": \"2026-09-11T00:05:00.000Z\",\n  \"data\": {\n    \"transferId\": \"bd9f1e6a-0000-4000-8000-000000000011\",\n    \"partnerReference\": \"ACME-TRF-000124\",\n    \"rail\": \"mpesa\",\n    \"status\": \"failed\",\n    \"amount\": {\n      \"currency\": \"KES\",\n      \"value\": \"1500.00\"\n    },\n    \"fee\": {\n      \"currency\": \"KES\",\n      \"value\": \"10.00\"\n    },\n    \"destinationAccountNumber\": \"254700000000\",\n    \"failureCode\": \"PROVIDER_REJECTED\",\n    \"settledAt\": null\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The bank **answered and refused** the transfer. Terminal — value never moved. `failureCode` names why; see /docs/errors. Do not re-instruct on silence: read `GET /v1/transfers/{id}` instead of guessing."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "confirmation.expired → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"6c3fa025-0000-4000-8000-000000000050\",\n  \"type\": \"confirmation.expired\",\n  \"createdAt\": \"2026-09-11T00:05:00.000Z\",\n  \"data\": {\n    \"confirmationId\": \"df9f1e6a-0000-4000-8000-000000000030\",\n    \"subjectType\": \"transfer\",\n    \"subjectId\": \"bd9f1e6a-0000-4000-8000-000000000010\",\n    \"expiredAt\": \"2026-09-11T00:05:00.000Z\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The confirmation code's window passed before anyone confirmed it. Keyed on the CONFIRMATION, not the transfer — `subjectId` is the confirmation id. Read it to find the transfer this expiry was about; it now reads `expired` on `GET /v1/transfers/{id}`."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "credit.received → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"7d4ab136-0000-4000-8000-000000000060\",\n  \"type\": \"credit.received\",\n  \"createdAt\": \"2026-09-14T10:32:04.118Z\",\n  \"data\": {\n    \"transactionId\": \"e1c2d3f4-0000-4000-8000-000000000020\",\n    \"accountId\": \"a7b8c9d0-0000-4000-8000-000000000001\",\n    \"accountNumber\": \"00000000123456\",\n    \"amount\": {\n      \"currency\": \"KES\",\n      \"value\": \"1500.00\"\n    },\n    \"rail\": \"mpesa\",\n    \"reference\": \"SBX0EXAMPLE\",\n    \"receivedAt\": \"2026-09-14T10:32:01.000Z\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Somebody paid you. Fires once per cleared inbound credit on a KES or USD account of yours, whoever sent it and whichever rail it came in on. On your USD account that includes a collection payout. A conversion's KES, and the USD a failed conversion returns, usually also produce one, but not always: do not rely on it, count a conversion once on `conversion.settled`'s `target`, and never count returned USD as income (it is your own money coming back). There is no payer name, phone number, bank account number or narration in `data` — `reference` is a rail-generated code, and it is `null` when the credit carried none. Match `accountNumber` (your own account's number) against the `accountNumber` on `GET /v1/balances`."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "collection.received → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"a07de469-0000-4000-8000-000000000090\",\n  \"type\": \"collection.received\",\n  \"createdAt\": \"2026-10-13T10:32:04.118Z\",\n  \"data\": {\n    \"collectionId\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n    \"reference\": \"DEMO-7K3Q2M\",\n    \"gross\": \"100.00\",\n    \"fee\": \"1.50\",\n    \"net\": \"98.50\",\n    \"currency\": \"USD\",\n    \"senderName\": \"Jane Contributor\",\n    \"treasuryCreditTxId\": \"UTRANS0000000000000000000000\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "A contribution to one of your collections was paid out, net of fee, to your USD account. Fires once per collection, when it becomes `credited` — not when the deposit first arrives. Reconcile against `net` (`gross` less `fee`); all three are decimal strings in `currency`. `reference` is the code we minted when you opened the collection, not your `partnerReference` and not the `depositMessage` the contributor quoted. `senderName` is `null` when the deposit carried none, and no sender bank details travel in this event. `treasuryCreditTxId` identifies the payout into your account — quote it if you ask us about this payout."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "conversion.settled → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"c41e2a90-0000-4000-8000-000000000100\",\n  \"type\": \"conversion.settled\",\n  \"createdAt\": \"2026-10-24T10:00:41.512Z\",\n  \"data\": {\n    \"conversionId\": \"c0a8012e-7b1a-4c39-9d3e-2f5c8a1b6d40\",\n    \"rate\": \"128.35\",\n    \"source\": \"100.00\",\n    \"target\": \"12835.00\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "A conversion you created with `POST /v1/conversions` completed: the USD left your USD account and the KES landed in your KES account. Fires once and is terminal. `rate` is the KES you were paid per 1 USD, `source` the USD debited and `target` the KES credited (`source × rate`, rounded down to the cent); all three are decimal strings. `conversionId` matches `id` on `GET /v1/conversions/:id`."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "conversion.failed → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"c41e2a90-0000-4000-8000-000000000101\",\n  \"type\": \"conversion.failed\",\n  \"createdAt\": \"2026-10-24T10:03:12.004Z\",\n  \"data\": {\n    \"conversionId\": \"c0a8012e-7b1a-4c39-9d3e-2f5c8a1b6d41\",\n    \"reason\": \"fx_unavailable\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The conversion will not complete. Fires once and is terminal. No KES was paid. `reason` is `insufficient_balance` or `debit_refused` (your USD was never taken), or `fx_unavailable`, `provider_otp_not_received` or `rate_moved_beyond_tolerance` (your USD was taken and has been returned to your USD account before this event fires)."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "usdt_send.settled → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"8e5bc247-0000-4000-8000-000000000070\",\n  \"type\": \"usdt_send.settled\",\n  \"createdAt\": \"2026-09-16T00:05:00.000Z\",\n  \"data\": {\n    \"sendId\": \"ed9f1e6a-0000-4000-8000-000000000040\",\n    \"partnerReference\": \"ACME-SEND-000123\",\n    \"status\": \"settled\",\n    \"amount\": {\n      \"currency\": \"USDT\",\n      \"value\": \"25.500000\"\n    },\n    \"destinationWalletId\": \"fa9f1e6a-0000-4000-8000-000000000050\",\n    \"failureReason\": null\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The USDT left your wallet and reached the destination. Terminal — this is the event to act on rather than polling `GET /v1/usdt/sends/{id}` to completion."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "usdt_send.failed → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"8e5bc247-0000-4000-8000-000000000071\",\n  \"type\": \"usdt_send.failed\",\n  \"createdAt\": \"2026-09-16T00:05:00.000Z\",\n  \"data\": {\n    \"sendId\": \"ed9f1e6a-0000-4000-8000-000000000041\",\n    \"partnerReference\": \"ACME-SEND-000124\",\n    \"status\": \"failed\",\n    \"amount\": {\n      \"currency\": \"USDT\",\n      \"value\": \"25.500000\"\n    },\n    \"destinationWalletId\": \"fa9f1e6a-0000-4000-8000-000000000050\",\n    \"failureReason\": \"USDT_SEND_INSUFFICIENT_LEDGER_BALANCE\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "It did not go out. If we had already reserved the amount against your ledger balance, that reservation is reversed before this event fires. `failureReason` names why; see /docs/errors. Do not re-instruct on silence: read `GET /v1/usdt/sends/{id}` instead of guessing."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        },
        {
          "name": "usdt_credit.received → your endpoint",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Signs this sample delivery the way OnLink signs a real one, so you can",
                  "// replay it against your own endpoint and exercise your verifier.",
                  "//",
                  "// Same five fields, same order. Only two are derived differently:",
                  "//   - the method is always POST;",
                  "//   - the NONCE SLOT carries the DELIVERY ID, which is stable across our",
                  "//     retries. Store the delivery ids you have processed and you get replay",
                  "//     protection AND idempotency from one value — and a retry then reads as",
                  "//     the same event rather than as an attack.",
                  "// NB: the binding is CJS, not CryptoJS. Newman PRE-INJECTS a global named",
                  "// CryptoJS into the script sandbox, so `const CryptoJS = require(...)` is a",
                  "// redeclaration and throws SyntaxError before a single header is set — every",
                  "// request then goes out unsigned. require() is kept so this also runs in the",
                  "// Postman app.",
                  "const CJS = require('crypto-js');",
                  "",
                  "const url = (pm.environment.get('webhookUrl') || '').trim();",
                  "const secret = pm.environment.get('webhookSecret') || '';",
                  "",
                  "if (!url || !secret) {",
                  "  skip(",
                  "    'SKIPPED: set webhookUrl to your own HTTPS endpoint and webhookSecret to ' +",
                  "      'the signing secret OnLink configured with you. Nothing is sent without ' +",
                  "      'both — an unsigned webhook is never sent, by us or by this collection.',",
                  "  );",
                  "} else {",
                  "  const body = pm.variables.replaceIn(pm.request.body.raw);",
                  "  pm.request.body.raw = body;",
                  "",
                  "  const target = url",
                  "    .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
                  "    .replace(/#.*$/, '');",
                  "  // Path AND query of YOUR url, never the absolute URL: behind a proxy you",
                  "  // cannot know whether we saw your host as you.example or you.example:443, so",
                  "  // a signature over the absolute URL would never verify.",
                  "  const pathWithQuery = target === '' ? '/' : target;",
                  "",
                  "  const timestamp = String(Date.now());",
                  "  const deliveryId = JSON.parse(body).id;",
                  "  const eventType = JSON.parse(body).type;",
                  "",
                  "  const signingString = [",
                  "    'POST',",
                  "    pathWithQuery,",
                  "    timestamp,",
                  "    deliveryId,",
                  "    CJS.SHA256(body).toString(CJS.enc.Hex),",
                  "  ].join('\\n');",
                  "",
                  "  pm.request.headers.upsert({",
                  "    key: 'X-OnLink-Signature',",
                  "    value:",
                  "      'v1=' + CJS.HmacSHA256(signingString, secret).toString(CJS.enc.Hex),",
                  "  });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Timestamp', value: timestamp });",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Delivery', value: deliveryId });",
                  "  // A routing hint, and deliberately NOT signed — a header outside the",
                  "  // signature is mutable in transit. If you route on it, still verify against",
                  "  // the body's own `type`, which is authoritative.",
                  "  pm.request.headers.upsert({ key: 'X-OnLink-Event', value: eventType });",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('your endpoint accepted the delivery with a 2xx', function () {",
                  "  // Anything else is retried on min(2^n x 15s, 15min) for 7 attempts, then",
                  "  // dead-lettered. A 4xx is retried too: a 404 from a partner endpoint is far",
                  "  // more often a deploy in progress than a considered refusal.",
                  "  pm.expect(pm.response.code).to.be.within(200, 299);",
                  "});",
                  "",
                  "pm.test('your endpoint answered promptly', function () {",
                  "  pm.expect(pm.response.responseTime).to.be.below(10000);",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": \"9f6cd358-0000-4000-8000-000000000080\",\n  \"type\": \"usdt_credit.received\",\n  \"createdAt\": \"2026-09-16T00:05:00.000Z\",\n  \"data\": {\n    \"activityId\": \"af7de469-0000-4000-8000-000000000090\",\n    \"amount\": {\n      \"currency\": \"USDT\",\n      \"value\": \"50.000000\"\n    },\n    \"receivedAt\": \"2026-09-16T00:04:50.000Z\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "USDT landed at your deposit address — no order, no tx hash, and no prior identifier: `activityId` is the first handle you see for this credit and your idempotency anchor."
          },
          "response": [
            {
              "name": "EXAMPLE — 200 your endpoint acknowledged it",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"received\": true\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "11 Other (not part of the documented flow)",
      "description": "Requests that exist on the API but are not part of the flow this collection teaches. Kept here, and covered, so `newman run` still exercises every operation — not because you are expected to call them on the documented sell path.",
      "item": [
        {
          "name": "PATCH /v1/orders/:id — attach a transaction hash",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// This request needs {{sellOrderId}}, which the sell-leg folder sets. Not",
                  "// needed for the documented (onlink_managed) flow, so it is skipped by",
                  "// default even when a sell order exists.",
                  "const chained = (pm.collectionVariables.get('sellOrderId') || '').trim();",
                  "if (!chained) {",
                  "  skip('SKIPPED: sellOrderId is not set. POST /v1/orders/sell sets it.');",
                  "}",
                  "",
                  "const hash = (pm.environment.get('txHash') || '').trim();",
                  "if (!hash) {",
                  "  skip(",
                  "    'SKIPPED: txHash is empty. Not needed for the documented flow — the ' +",
                  "      'default wallet kind pays from your USDT balance and never reaches ' +",
                  "      'awaiting_usdt. This only applies to a sell order on a wallet ' +",
                  "      'provisioned partner_customer. Set txHash to a real send to run it.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 attached', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const result = pm.response.json();",
                  "",
                  "pm.test('three fields, and no re-quote of the amounts', function () {",
                  "  // The attach call answers \"is this hash on this order\". Re-quoting the",
                  "  // amounts would invite a client to read the response as a re-price.",
                  "  pm.expect(Object.keys(result).sort()).to.eql([",
                  "    'orderId',",
                  "    'status',",
                  "    'txHash',",
                  "  ]);",
                  "});",
                  "",
                  "pm.test('the hash comes back normalised: lower-case, no 0x', function () {",
                  "  pm.expect(result.txHash).to.match(/^[0-9a-f]{64}$/);",
                  "});",
                  "",
                  "pm.test('attaching is a claim, not a receipt', function () {",
                  "  // Still awaiting_usdt: recording a hash does not move the order.",
                  "  pm.expect(result.orderId).to.eql(pm.collectionVariables.get('sellOrderId'));",
                  "  pm.expect(result.status).to.be.a('string');",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/orders/{{sellOrderId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "orders", "{{sellOrderId}}"]
            },
            "description": "Requires `orders:write`.\n\n**Not needed for the documented flow.** We take your USDT at create — from your balance on the default wallet kind (`onlink_managed`), from your own wallet on `partner_customer` — so there is normally no hash of yours to attach. If you do hold the hash of an on-chain send for a sell order that is still `awaiting_usdt`, this records it against that order. That is all it does: it does not move the order, release a payout, or confirm the USDT arrived. Deposit matching later looks for a deposit carrying this hash and exactly the order’s USDT amount, and a deposit with this hash but a different amount is held for review, not paid. The deposit address is shared across all your orders, so we never infer the order from the amount.\n\nA buy order is refused (409 `ORDER_NOT_SELL_SIDE`), as is any order that has already left `awaiting_usdt` (409 `ORDER_NOT_AWAITING_USDT`) — an order paid from your balance leaves it at create.\n\nAttaching a **different** hash to an order that already has one is refused (409 `TX_HASH_ALREADY_ATTACHED`), never an overwrite. Re-sending the **same** hash is safe and returns 200, so a retry after a lost response is fine.\n\nThe hash is normalised — lower-cased, any `0x` prefix stripped — and returned in that form. Attaching a hash is a **claim, not a receipt**: it does not move the order, so the status stays `awaiting_usdt`. Only a deposit that carries this hash and exactly the order’s USDT amount moves it on.\n\nSet `{{txHash}}` to the hash of a real send to run this.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"txHash\": \"{{txHash}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 hash attached (status unchanged: it is a claim)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n  \"status\": \"awaiting_usdt\",\n  \"txHash\": \"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070\"\n}"
            },
            {
              "name": "EXAMPLE — 400 not a Tron transaction hash",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Validation failed on: txHash\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 order not found, or not yours",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Order not found.\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 a DIFFERENT hash is already attached (never overwritten)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"TX_HASH_ALREADY_ATTACHED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 that hash already funds another of your orders",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"TX_HASH_ALREADY_USED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the order is no longer awaiting USDT",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"ORDER_NOT_AWAITING_USDT\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 that is a buy order",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"ORDER_NOT_SELL_SIDE\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "12 Sandbox simulations",
      "description": "Acts as if money arrived, so you can test a collection or a sell order end to end without sending any. **Sandbox only**: outside the sandbox these two routes answer `404`, exactly as a path that does not exist would, so never build a production flow on them.\n\n`POST /v1/simulations/collections/{id}/deposits` needs **`collections:write`** and a `collections` partner. `POST /v1/simulations/orders/{id}/usdt-receipt` needs **`orders:write`** and a `usdt_rails` partner. Each request is skipped unless `{{runSimulations}}` is `true` (the sandbox environment sets it; leave it empty anywhere else, so a run sends nothing from this folder outside the sandbox), and until the id it needs has been chained by an earlier request (`collectionId` by `POST /v1/collections`, `sellOrderId` by `POST /v1/orders/sell`).\n\nA simulated payment into a collection marks it `received` and moves no real money: `crediting` is always `pending` in that response. While sandbox crediting is enabled, a collection created in sandbox is then credited at the bank simulator, so read it until its status is `credited`; otherwise it stays `received`. A simulated USDT deposit needs the order's transaction hash attached first (`PATCH /v1/orders/{id}`).",
      "item": [
        {
          "name": "POST /v1/simulations/collections/:id/deposits — simulate a payment into a collection (sandbox only)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Opt-in, like runRevoke and runResend: this route exists only in the sandbox (it",
                  "// answers 404 anywhere else), so nothing is sent unless the environment says so.",
                  "if ((pm.environment.get('runSimulations') || '').trim() !== 'true') {",
                  "  skip('SKIPPED: sandbox simulations exist only in the sandbox. Set runSimulations=true there to include them.');",
                  "} else {",
                  "  // This request needs {{collectionId}}, which an earlier request sets. POST /v1/collections sets it.",
                  "  const chained = (pm.collectionVariables.get('collectionId') || '').trim();",
                  "  if (!chained) {",
                  "    skip('SKIPPED: collectionId is not set. POST /v1/collections sets it.');",
                  "  }",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200 received, or 202 accepted and the collection still catching up', function () {",
                  "  pm.expect(pm.response.code).to.be.oneOf([200, 202]);",
                  "});",
                  "",
                  "const simulated = pm.response.json();",
                  "",
                  "pm.test('the amount is a decimal string and nothing is paid out', function () {",
                  "  pm.expect(simulated.deposit.amount).to.be.a('string');",
                  "  pm.expect(simulated.deposit.currency).to.eql('USD');",
                  "  pm.expect(['received', 'pending']).to.include(simulated.deposit.status);",
                  "  pm.expect(simulated.crediting).to.eql('pending');",
                  "});",
                  "",
                  "pm.test('it names the collection we simulated against', function () {",
                  "  pm.expect(simulated.collectionId).to.eql(pm.collectionVariables.get('collectionId'));",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/simulations/collections/{{collectionId}}/deposits",
              "host": ["{{baseUrl}}"],
              "path": [
                "v1",
                "simulations",
                "collections",
                "{{collectionId}}",
                "deposits"
              ]
            },
            "description": "Requires `collections:write`. Sandbox only: outside the sandbox this answers 404, so it is skipped unless `{{runSimulations}}` is `true`.\n\nBehaves as if a payer paid this collection. The collection must be open. `amount` is a decimal string in USD with at most two decimal places, greater than 0 and at most `10000.00`; `senderName` is optional. Answers `200` once the collection is marked received, or `202` when the payment was accepted and the collection has not caught up yet: read it with `GET /v1/collections/{id}` a little later.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"25.00\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 received, shared deposit account",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"collectionId\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n  \"reference\": \"DEMO-7K3Q2M\",\n  \"route\": \"memo\",\n  \"deposit\": {\n    \"id\": \"SIM-DEP-6f1c2d9e-3b7a-4c51-9d0e-2a8b7c6d5e4f\",\n    \"amount\": \"25.00\",\n    \"currency\": \"USD\",\n    \"status\": \"received\"\n  },\n  \"collectionStatus\": \"received\",\n  \"crediting\": \"pending\",\n  \"message\": \"Deposit received. The collection is credited to your account shortly; poll GET /v1/collections/{id} until it reads credited.\"\n}"
            },
            {
              "name": "EXAMPLE — 202 accepted, dedicated deposit account (read the collection shortly)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Accepted",
              "code": 202,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"collectionId\": \"a1b2c3d4-0000-4000-8000-000000000001\",\n  \"reference\": \"DEMO-7K3Q2M\",\n  \"route\": \"virtual_account\",\n  \"deposit\": {\n    \"id\": null,\n    \"amount\": \"25.00\",\n    \"currency\": \"USD\",\n    \"status\": \"pending\"\n  },\n  \"collectionStatus\": \"open\",\n  \"crediting\": \"pending\",\n  \"message\": \"Payment sent. It usually appears within two minutes; poll GET /v1/collections/{id}.\"\n}"
            },
            {
              "name": "EXAMPLE — 400 the amount is not a valid decimal string",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Validation failed on: amount\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 COLLECTION_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"COLLECTION_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 outside the sandbox: the route does not exist there",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Cannot POST /v1/simulations/collections/a1b2c3d4-0000-4000-8000-000000000001/deposits\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 the collection is not open",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"COLLECTION_NOT_OPEN\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 we could not complete the simulated payment (retry shortly)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"SIMULATION_UPSTREAM_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        },
        {
          "name": "POST /v1/simulations/orders/:id/usdt-receipt — simulate a USDT deposit for a sell order (sandbox only)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// A skip must be VISIBLE. pm.execution.skipRequest() sends nothing and produces",
                  "// no response, which in the Postman app is indistinguishable from a request that",
                  "// hung — and a console.log lands where nobody is looking. So the reason is also",
                  "// reported as an assertion, which appears in Test Results. Verified under newman:",
                  "// a pm.test() in a pre-request script is reported even when the request is then",
                  "// skipped (the request stays uncounted and the test script does not run).",
                  "function skip(reason) {",
                  "  pm.test(reason, function () {",
                  "    pm.expect(true).to.be.true;",
                  "  });",
                  "  console.log(reason);",
                  "  if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) {",
                  "    pm.execution.skipRequest();",
                  "  }",
                  "}",
                  "",
                  "",
                  "// Opt-in, like runRevoke and runResend: this route exists only in the sandbox (it",
                  "// answers 404 anywhere else), so nothing is sent unless the environment says so.",
                  "if ((pm.environment.get('runSimulations') || '').trim() !== 'true') {",
                  "  skip('SKIPPED: sandbox simulations exist only in the sandbox. Set runSimulations=true there to include them.');",
                  "} else {",
                  "  // This request needs {{sellOrderId}}, which an earlier request sets. POST /v1/orders/sell sets it.",
                  "  const chained = (pm.collectionVariables.get('sellOrderId') || '').trim();",
                  "  if (!chained) {",
                  "    skip('SKIPPED: sellOrderId is not set. POST /v1/orders/sell sets it.');",
                  "  }",
                  "}",
                  ""
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "",
                  "pm.test('200', function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "",
                  "const simulated = pm.response.json();",
                  "",
                  "pm.test('the receipt amount is a decimal string and the outcome is one this API publishes', function () {",
                  "  pm.expect(simulated.receipt.amount).to.be.a('string');",
                  "  pm.expect(['matched', 'review', 'not_matched']).to.include(simulated.receipt.outcome);",
                  "});",
                  "",
                  "pm.test('it names the order we simulated against', function () {",
                  "  pm.expect(simulated.orderId).to.eql(pm.collectionVariables.get('sellOrderId'));",
                  "});",
                  ""
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/simulations/orders/{{sellOrderId}}/usdt-receipt",
              "host": ["{{baseUrl}}"],
              "path": [
                "v1",
                "simulations",
                "orders",
                "{{sellOrderId}}",
                "usdt-receipt"
              ]
            },
            "description": "Requires `orders:write`. Sandbox only: outside the sandbox this answers 404, so it is skipped unless `{{runSimulations}}` is `true`.\n\nBehaves as if the USDT for this sell order arrived on chain. Attach the transaction hash to the order first with `PATCH /v1/orders/{id}`. The body may be empty: the order's own amount is used. A different `amount` (a decimal string, at most six decimal places) sends the order to review, which is the way to see what an amount mismatch looks like.",
            "body": {
              "mode": "raw",
              "raw": "{}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 200 matched",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"a1b2c3d4-0000-4000-8000-000000000002\",\n  \"receipt\": {\n    \"amount\": \"10.000000\",\n    \"txHash\": \"9f2b6c1d0a7e4f3b8c5d2e1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c\",\n    \"outcome\": \"matched\"\n  },\n  \"orderStatus\": \"awaiting_approval\",\n  \"walletFunded\": true,\n  \"message\": \"Receipt matched. The order has moved on from awaiting_usdt; poll GET /v1/orders/{id}.\"\n}"
            },
            {
              "name": "EXAMPLE — 200 review (the amount differs from the order's)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\n  \"orderId\": \"a1b2c3d4-0000-4000-8000-000000000002\",\n  \"receipt\": {\n    \"amount\": \"7.500000\",\n    \"txHash\": \"9f2b6c1d0a7e4f3b8c5d2e1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c\",\n    \"outcome\": \"review\"\n  },\n  \"orderStatus\": \"review\",\n  \"walletFunded\": true,\n  \"message\": \"The amount differs from the order's, so the order moved to review and will not settle without our help.\"\n}"
            },
            {
              "name": "EXAMPLE — 400 the amount is not a valid decimal string",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 400,\n  \"message\": \"Validation failed on: amount\",\n  \"error\": \"Bad Request\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 ORDER_NOT_FOUND",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"ORDER_NOT_FOUND\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 404 outside the sandbox: the route does not exist there",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 404,\n  \"message\": \"Cannot POST /v1/simulations/orders/a1b2c3d4-0000-4000-8000-000000000002/usdt-receipt\",\n  \"error\": \"Not Found\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 409 no transaction hash is attached to the order yet",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 409,\n  \"message\": \"ORDER_TX_HASH_NOT_ATTACHED\",\n  \"error\": \"Conflict\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 503 we could not complete the simulated deposit (retry shortly)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Service Unavailable",
              "code": 503,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 503,\n  \"message\": \"SIMULATION_UPSTREAM_UNAVAILABLE\",\n  \"error\": \"Service Unavailable\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            },
            {
              "name": "EXAMPLE — 401 the signature did not verify (or the clock, or the key)",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": ""
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Request-Id",
                  "value": "1647"
                }
              ],
              "cookie": [],
              "body": "{\n  \"statusCode\": 401,\n  \"message\": \"An error occurred\",\n  \"error\": \"unauthorized\",\n  \"requestId\": \"req-1756512000000-a1b2c3d\"\n}"
            }
          ]
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// =============================================================================",
          "// OnLink Partner API — request signing. You do not need to touch this.",
          "//",
          "// Set two environment variables (keyId, secret) and every request below signs",
          "// itself. What follows is the same five-field scheme as",
          "// docs/partner-api/signing/sign-request.js, transliterated into the Postman",
          "// sandbox's crypto-js. Read that file when you port this to your own backend.",
          "// =============================================================================",
          "",
          "// The binding is CJS, NOT CryptoJS. Newman pre-injects a global named CryptoJS",
          "// into every script sandbox, so `const CryptoJS = require('crypto-js')` is a",
          "// redeclaration and throws SyntaxError -- before any header is set, which sends",
          "// every /v1 request UNSIGNED and gets a uniform 401 back. require() is kept",
          "// (rather than reading the bare global) so this also runs in the Postman app.",
          "const CJS = require('crypto-js');",
          "",
          "// Only /v1 requests are signed. The \"10 Webhooks\" folder posts to YOUR endpoint",
          "// with OUR webhook secret, so it signs differently and is handled there.",
          "const rawUrl = pm.variables.replaceIn(pm.request.url.toString());",
          "const target = rawUrl",
          "  .replace(/^[a-z][a-z0-9+.\\-]*:\\/\\/[^/?#]*/i, '')",
          "  .replace(/#.*$/, '');",
          "const pathWithQuery = target === '' ? '/' : target;",
          "",
          "if (!/^\\/v1(\\/|$|\\?)/.test(pathWithQuery)) {",
          "  return;",
          "}",
          "",
          "const keyId = (pm.environment.get('keyId') || '').trim();",
          "const secret = pm.environment.get('secret') || '';",
          "",
          "// A clear failure here beats a 401. Every authentication failure on this API",
          "// returns the same opaque body, so an unset credential is indistinguishable",
          "// from a wrong signature once the request has left.",
          "if (!keyId || !secret) {",
          "  throw new Error(",
          "    'Set keyId and secret in your environment. Both are empty in the committed ' +",
          "      'template — OnLink issues them, and they are never checked into a repo.',",
          "  );",
          "}",
          "if (!/^[A-Za-z0-9_]{1,64}$/.test(keyId)) {",
          "  throw new Error(",
          "    'keyId must match ^[A-Za-z0-9_]{1,64}$ — no dashes, dots, colons or spaces.',",
          "  );",
          "}",
          "",
          "// ---------------------------------------------------------------------------",
          "// TRAP 2 — the body hash covers the EXACT BYTES SENT.",
          "//",
          "// Postman substitutes {{variables}} into the body at send time, so the raw",
          "// template is NOT what goes on the wire. Resolve it here and write the resolved",
          "// value straight back onto the request, so the string that is hashed and the",
          "// string that is sent are the same object. Hashing the template — or letting",
          "// anything re-serialise between here and the send — is a signature that always",
          "// 401s.",
          "// ---------------------------------------------------------------------------",
          "let body = '';",
          "if (pm.request.body && pm.request.body.mode === 'raw' && pm.request.body.raw) {",
          "  body = pm.variables.replaceIn(pm.request.body.raw);",
          "  pm.request.body.raw = body;",
          "}",
          "",
          "// An absent body hashes as sha256('') — e3b0c442...b855. Do not send an",
          "// explicit {} on a GET to avoid it: fetch forbids a body on GET, and the server",
          "// already treats an absent body as an empty one.",
          "const bodySha256Hex = CJS.SHA256(body).toString(CJS.enc.Hex);",
          "",
          "// ---------------------------------------------------------------------------",
          "// TRAP 3 — the clock. Unix MILLISECONDS, and the server accepts ±5 minutes of",
          "// its own. Seconds (a ten-digit number) read as ~56 years ago and are refused.",
          "// If every request starts failing at once, check this host's clock before you",
          "// rotate the secret.",
          "// ---------------------------------------------------------------------------",
          "const timestamp = String(Date.now());",
          "",
          "// Unique per request, at most 64 characters, and must not contain ':'.",
          "const nonce = pm.variables.replaceIn('{{$guid}}');",
          "",
          "// ---------------------------------------------------------------------------",
          "// TRAP 1 — the signed path INCLUDES THE QUERY STRING, exactly as sent.",
          "//",
          "// pathWithQuery above is derived from Postman's own resolved URL, which is what",
          "// makes this correct for free: the server signs the request line's full target",
          "// (Express req.originalUrl), so signing the bare path 401s every request that",
          "// carries a query.",
          "// ---------------------------------------------------------------------------",
          "const signingString = [",
          "  pm.request.method.toUpperCase(),",
          "  pathWithQuery,",
          "  timestamp,",
          "  nonce,",
          "  bodySha256Hex,",
          "].join('\\n');",
          "",
          "const signature = CJS.HmacSHA256(signingString, secret).toString(",
          "  CJS.enc.Hex,",
          ");",
          "",
          "// upsert, not add: a re-run must replace the previous request's headers rather",
          "// than send two of each.",
          "pm.request.headers.upsert({ key: 'x-onlink-key', value: keyId });",
          "pm.request.headers.upsert({ key: 'x-onlink-timestamp', value: timestamp });",
          "pm.request.headers.upsert({ key: 'x-onlink-nonce', value: nonce });",
          "pm.request.headers.upsert({",
          "  key: 'x-onlink-signature',",
          "  value: 'v1=' + signature,",
          "});",
          ""
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "buyQuoteId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/quotes (buy)."
    },
    {
      "key": "buyQuoteUsdtAmount",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/quotes (buy), then sent back as expectedUsdtAmount so a mispriced order is refused rather than traded."
    },
    {
      "key": "sellQuoteId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/quotes (sell)."
    },
    {
      "key": "orderId",
      "value": "",
      "type": "string",
      "description": "The most recently created order, either side."
    },
    {
      "key": "buyOrderId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/orders/buy."
    },
    {
      "key": "sellOrderId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/orders/sell."
    },
    {
      "key": "walletId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/wallets."
    },
    {
      "key": "activeWalletId",
      "value": "",
      "type": "string",
      "description": "Chained by GET /v1/wallets — the first address at status `active`. Sent by POST /v1/usdt/sends as `destinationWalletId`. The buy leg does not use it: it is debit-first and delivers by wallet KIND rather than by an address id."
    },
    {
      "key": "adminId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/admins."
    },
    {
      "key": "payoutAccountId",
      "value": "",
      "type": "string",
      "description": "Chained by GET /v1/payout-accounts — the destination the sell leg names."
    },
    {
      "key": "transferId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/transfers."
    },
    {
      "key": "confirmationId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/transfers — the confirmation.id the code you were read out belongs to."
    },
    {
      "key": "batchId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/transfers/batch."
    },
    {
      "key": "batchConfirmationId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/transfers/batch — the confirmation.id for the whole batch."
    },
    {
      "key": "usdtSendId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/usdt/sends."
    },
    {
      "key": "usdtSendConfirmationId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/usdt/sends — the confirmation.id the code you were read out belongs to."
    },
    {
      "key": "collectionId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/collections."
    },
    {
      "key": "conversionId",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/conversions."
    }
  ]
}
