{
  "info": {
    "_postman_id": "116c8e36-953a-4644-b656-6f06e7d336f7",
    "name": "OnLink Partner API (USDT/KES)",
    "description": "A self-testing collection for the OnLink USDT/KES 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 an address, then execute a buy leg and a sell leg. Every request asserts its own contract, so `newman run` is a real integration check you can put in your CI.\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 exists. Subscribe to webhooks; do not poll to completion.\n- **Your USDT deposit address is permanent and shared across all your orders**, so it cannot identify which order a deposit belongs to. The transaction hash does — attach it with `PATCH /v1/orders/{id}`.\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- **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- **KES payout destinations are registered by OnLink out of band.** 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 attaching a transaction hash needs a real send. 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.\n\nA 401 on this route means one of fourteen things and the response says which of them it is — 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. Both routes are read-only.\n\nThere is no `POST /v1/payout-accounts`: KES destinations are registered by OnLink out of band, so a leaked credential can list your destinations and choose among them but cannot add one. That is what makes `payoutAccountId` on a sell order a selection rather than an instruction.",
      "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 (buy leg) and your USDT deposit address (sell leg). Every reference is an explicit field — integrate against the fields, and show `instructions` to a person.\n\n**Your USDT deposit address is permanent and shared across all your sell orders**, so a deposit cannot be attributed by the address it landed on. The transaction hash you attach with `PATCH /v1/orders/{id}` is what attributes it.\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\": \"444174\",\n      \"accountReference\": \"46019900000001\",\n      \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n      \"currency\": \"KES\",\n      \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001. Then quote the paymentReference from your order in your own records — we match the payment by the reference we issued, not by the amount.\"\n    },\n    {\n      \"rail\": \"bank_transfer\",\n      \"accountReference\": \"46019900000001\",\n      \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n      \"currency\": \"KES\",\n      \"instructions\": \"Transfer to the account number above and put the order’s paymentReference in the narration.\"\n    }\n  ],\n  \"usdt\": {\n    \"chain\": \"tron\",\n    \"asset\": \"USDT\",\n    \"address\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n    \"memo\": null,\n    \"instructions\": \"Send USDT on the Tron (TRC-20) network to this address. It is your permanent deposit address and is the same for every sell order, so we cannot tell your orders apart by it — after sending, attach the transaction hash with PATCH /v1/orders/{id}. 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 payout destinations. OnLink registers these out of band — 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\nThe destinations a sell order may name in `payoutAccountId`. Read-only, and only active destinations are listed.\n\nAccount numbers come back as **the last four digits only**. You select a destination 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 destination 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\": \"Primary settlement\"\n  }\n]"
            },
            {
              "name": "EXAMPLE — 200 none registered 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": "02 Addresses",
      "description": "The withdrawal-address registry, and the administrators who approve it.\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('starts at pending_partner_approval, never active', function () {",
                  "  // Two approvals stand between registration and delivery. A first",
                  "  // integration that treats registration as completion sends to an address",
                  "  // the API will refuse.",
                  "  pm.expect(w.status).to.eql('pending_partner_approval');",
                  "});",
                  "",
                  "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', 'flagged']).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 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\": \"TV16bzw8AQa7ToEF31exg58v335mC9oVDC\",\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\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\": \"TV16bzw8AQa7ToEF31exg58v335mC9oVDC\",\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, so the buy leg will skip.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            }
          ],
          "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}}`, which is what the buy leg delivers to. If none is active, the buy order request skips itself and says so — the buy leg cannot run against an unapproved 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\": \"TV16bzw8AQa7ToEF31exg58v335mC9oVDC\",\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\": \"TBM9HegFxgjBspHTYdCygpqregdfbihAh1\",\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, pay, poll.\n\nYou pay KES to a collection account that is **yours**, and we deliver USDT to a withdrawal address you registered and both parties approved.\n\n`payTo` and `reference` identify different things and conflating them is the mistake that makes attribution guesswork. `payTo` is your own dedicated collection account and identifies **you**. `reference` is the value **we** issue for **this order**. On a bank rail put the reference in the narration; on `mpesa` it cannot be transmitted at all, because M-PESA carries no narration field — so keep it in your own records and quote it if you need to ask us about the order.\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('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  \"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.');",
                  "}",
                  "",
                  "const active = (pm.collectionVariables.get('activeWalletId') || '').trim();",
                  "if (!active) {",
                  "  skip(",
                  "    'SKIPPED: no active withdrawal address. Delivery requires BOTH your ' +",
                  "      'administrator’s confirmation and OnLink approval, and this order would ' +",
                  "      'be refused (409 WALLET_NOT_DELIVERABLE) without one.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "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 starts at awaiting_payment', function () {",
                  "  pm.expect(order.status).to.eql('awaiting_payment');",
                  "});",
                  "",
                  "// 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('payment instructions state every field explicitly', function () {",
                  "  const i = order.paymentInstructions;",
                  "  pm.expect(i.rail).to.be.a('string');",
                  "  pm.expect(i.payTo).to.be.a('string');",
                  "  pm.expect(i.reference).to.be.a('string');",
                  "  pm.expect(i.currency).to.eql('KES');",
                  "  pm.expect(i.payBefore).to.be.a('string');",
                  "  pm.expect(i.instructions).to.be.a('string');",
                  "  // Integrate against the fields. `instructions` is prose for a person, and a",
                  "  // parser over our sentence structure breaks the first time we reword it.",
                  "});",
                  "",
                  "pm.test('the instructions amount is a string, and matches the order', function () {",
                  "  pm.expect(order.paymentInstructions.kesAmount).to.be.a('string');",
                  "  pm.expect(order.paymentInstructions.kesAmount).to.eql(order.kesAmount);",
                  "});",
                  "",
                  "pm.test('paymentReference is restated at the top level', function () {",
                  "  // The same string as paymentInstructions.reference, so a client polling the",
                  "  // order need not reach into the instructions for the value it quotes.",
                  "  pm.expect(order.paymentReference).to.eql(order.paymentInstructions.reference);",
                  "});",
                  "",
                  "pm.test('payBefore is the same instant as expiresAt', function () {",
                  "  pm.expect(new Date(order.paymentInstructions.payBefore).getTime()).to.eql(",
                  "    new Date(order.expiresAt).getTime(),",
                  "  );",
                  "});",
                  "",
                  "pm.test('on the mpesa rail a paybill is present', function () {",
                  "  if (order.paymentInstructions.rail === 'mpesa') {",
                  "    pm.expect(order.paymentInstructions.paybill).to.be.a('string');",
                  "  }",
                  "});",
                  "",
                  "pm.collectionVariables.set('orderId', order.orderId);",
                  "pm.collectionVariables.set('buyOrderId', order.orderId);",
                  "pm.collectionVariables.set('paymentReference', order.paymentReference);",
                  "console.log(",
                  "  'orderId=' + order.orderId + ' paymentReference=' + order.paymentReference,",
                  ");",
                  ""
                ]
              }
            }
          ],
          "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 returns the payment instructions: where to send the KES, how much, by when, and the payment reference we issue for this order.\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. Use your own idempotency key there.\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\nNeeds an **active** withdrawal address, so it skips itself when `GET /v1/wallets` found none.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"quoteId\": \"{{buyQuoteId}}\",\n  \"partnerReference\": \"{{$guid}}\",\n  \"walletId\": \"{{activeWalletId}}\",\n  \"paymentRail\": \"{{buyPaymentRail}}\",\n  \"expectedUsdtAmount\": \"{{buyQuoteUsdtAmount}}\"\n}"
            }
          },
          "response": [
            {
              "name": "EXAMPLE — 202 created, awaiting your KES payment",
              "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\": \"awaiting_payment\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\"\n}"
            },
            {
              "name": "EXAMPLE — 404 unknown quoteId or walletId",
              "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\": \"walletId 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 the withdrawal address is not deliverable",
              "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\": \"WALLET_NOT_DELIVERABLE\",\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 — 503 no KES collection account 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 KES collection account 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": "EXAMPLE — 503 the collection account is still being activated",
              "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\": \"Your KES collection account is still being activated. Try again shortly.\",\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([",
                  "    'awaiting_payment',",
                  "    'awaiting_usdt',",
                  "    'payment_matched',",
                  "    'usdt_received',",
                  "    'awaiting_approval',",
                  "    'rejected',",
                  "    'sending',",
                  "    'paying_out',",
                  "    '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('a buy order carries payment instructions, not a deposit address', function () {",
                  "  pm.expect(order.paymentInstructions).to.be.an('object');",
                  "  pm.expect(order).to.not.have.property('depositAddress');",
                  "});",
                  "",
                  "pm.test('payment evidence has both fields, null until matched', function () {",
                  "  pm.expect(order.payment).to.have.property('providerReference');",
                  "  pm.expect(order.payment).to.have.property('bankTransactionId');",
                  "});",
                  "",
                  "pm.test('no internal field leaked into the read', function () {",
                  "  // The response is an allowlist. None of these is a partner-facing field,",
                  "  // and each was reachable from the order row.",
                  "  [",
                  "    'reviewReason',",
                  "    'bridgeTransferId',",
                  "    'bridgeState',",
                  "    'payoutTxId',",
                  "    'destinationTxHash',",
                  "    'depositWalletId',",
                  "    'matchedTransactionId',",
                  "    'settlementEstimateSeconds',",
                  "    'achievedSettlementSeconds',",
                  "    'approvalRequestedAt',",
                  "  ].forEach(function (field) {",
                  "    pm.expect(order).to.not.have.property(field);",
                  "  });",
                  "});",
                  ""
                ]
              }
            }
          ],
          "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 `paymentInstructions` versus `depositAddress`, or keep the side you created it with.\n\nA buy order also carries `payment` — the provider’s own references for the payment we attributed to it: the M-PESA or PesaLink code your own end user recognises, plus the bank’s transaction id. Both are `null` until the payment is matched, and two nullable fields rather than an absent object so you can tell \"not matched yet\" from \"matched, no reference on this rail\".\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 awaiting_payment (nothing sent yet)",
              "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_payment\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\",\n  \"payment\": {\n    \"providerReference\": null,\n    \"bankTransactionId\": null\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 payment_matched (your KES was attributed)",
              "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\": \"payment_matched\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\",\n  \"payment\": {\n    \"providerReference\": \"SJK4Q1T8ZP\",\n    \"bankTransactionId\": \"CB2026090400001234\"\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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\",\n  \"payment\": {\n    \"providerReference\": \"SJK4Q1T8ZP\",\n    \"bankTransactionId\": \"CB2026090400001234\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 rejected (terminal: refused, do not resend)",
              "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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\",\n  \"payment\": {\n    \"providerReference\": \"SJK4Q1T8ZP\",\n    \"bankTransactionId\": \"CB2026090400001234\"\n  }\n}"
            },
            {
              "name": "EXAMPLE — 200 expired (terminal: quote a new order and send again)",
              "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\": \"expired\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"paymentInstructions\": {\n    \"rail\": \"mpesa\",\n    \"paybill\": \"444174\",\n    \"payTo\": \"46019900000001\",\n    \"beneficiaryName\": \"ONLINK MASTER_YOURCOMPANY\",\n    \"reference\": \"OL-7F3K9QB2\",\n    \"kesAmount\": \"1281000.00\",\n    \"currency\": \"KES\",\n    \"payBefore\": \"2026-09-05T09:30:00.000Z\",\n    \"instructions\": \"M-PESA > Lipa na M-PESA > Pay Bill > Business number 444174 > Account number 46019900000001 > Amount 1281000.00. The Account number is what identifies you to us; M-PESA carries no narration, so keep the reference OL-7F3K9QB2 in your own records and quote it to us if you need to ask about this order.\"\n  },\n  \"paymentReference\": \"OL-7F3K9QB2\",\n  \"payment\": {\n    \"providerReference\": null,\n    \"bankTransactionId\": null\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, send USDT, attach the hash, poll.\n\nYour deposit address is **permanent and identical for every one of your orders**, so it cannot tell your orders apart. The transaction hash you attach is what attributes a deposit to an order. That is the single most important property of this leg, and skipping the PATCH leaves a real deposit unattributed.\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.",
      "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('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  \"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 OnLink ' +",
                  "      'registers destinations out of band — 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 starts at awaiting_usdt', function () {",
                  "  pm.expect(order.status).to.eql('awaiting_usdt');",
                  "});",
                  "",
                  "// 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('the deposit address is rendered in full', function () {",
                  "  // 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);",
                  "pm.collectionVariables.set('depositAddress', order.depositAddress);",
                  "console.log(",
                  "  'Send ' +",
                  "    order.usdtAmount +",
                  "    ' USDT (TRC-20) to ' +",
                  "    order.depositAddress +",
                  "    ', then PATCH the transaction hash onto order ' +",
                  "    order.orderId,",
                  ");",
                  ""
                ]
              }
            }
          ],
          "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 returns your deposit address. **202**, because the order exists and nothing has happened to the money.\n\nRetry-safe on `partnerReference`: creating twice with the same value returns the original order.\n\n`payoutAccountId` must be one of your active destinations. A revoked one 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, awaiting your USDT",
              "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\": \"awaiting_usdt\",\n  \"kesAmount\": \"1281000.00\",\n  \"usdtAmount\": \"10000.000000\",\n  \"rate\": \"128.1000\",\n  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\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 destination is revoked",
              "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 — 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": "PATCH /v1/orders/:id — attach your 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 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.');",
                  "}",
                  "",
                  "const hash = (pm.environment.get('txHash') || '').trim();",
                  "if (!hash) {",
                  "  skip(",
                  "    'SKIPPED: txHash is empty. This step reports a USDT send you actually ' +",
                  "      'made — there is no hash to attach until you have sent to the deposit ' +",
                  "      'address. Set txHash and re-run this folder.',",
                  "  );",
                  "}",
                  ""
                ]
              }
            },
            {
              "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 until our matcher confirms the deposit on-chain.",
                  "  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**This is what attributes your deposit to this order.** The deposit address is shared across all your orders, so without the hash a real deposit has no order to belong to.\n\nAttaching a **different** hash to an order that already has one is refused (409 `TX_HASH_ALREADY_ATTACHED`), never an overwrite: the first hash may already have been matched, and re-pointing the order would orphan a real deposit. 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**: the status stays `awaiting_usdt` until our matcher confirms the deposit on-chain.\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": "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('txHash is present, and null until you attach one', function () {",
                  "  pm.expect(order).to.have.property('txHash');",
                  "  if (order.txHash !== null) {",
                  "    pm.expect(order.txHash).to.match(/^[0-9a-f]{64}$/);",
                  "  }",
                  "});",
                  "",
                  "pm.test('no internal field leaked into the read', function () {",
                  "  [",
                  "    'reviewReason',",
                  "    'bridgeTransferId',",
                  "    'payoutTxId',",
                  "    'destinationTxHash',",
                  "    'depositWalletId',",
                  "    'payoutAccountId',",
                  "    'matchedTransactionId',",
                  "  ].forEach(function (field) {",
                  "    pm.expect(order).to.not.have.property(field);",
                  "  });",
                  "});",
                  "",
                  "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 and the transaction hash you attached, which is `null` until you attach one.\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, no hash attached yet",
              "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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": null\n}"
            },
            {
              "name": "EXAMPLE — 200 usdt_received (your deposit was attributed)",
              "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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": \"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070\"\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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": \"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070\"\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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": \"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070\"\n}"
            },
            {
              "name": "EXAMPLE — 200 expired (terminal: 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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\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  \"expiresAt\": \"2026-09-05T09:30:00.000Z\",\n  \"depositAddress\": \"TQiBwkXtUUNygiLdSwdeaZCKagwPsD1a7C\",\n  \"chain\": \"tron\",\n  \"sendBefore\": \"2026-09-05T09:30:00.000Z\",\n  \"txHash\": \"5d6a3c7b1e4f2a908c6d5b4a3928170f6e5d4c3b2a1908f7e6d5c4b3a2918070\"\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 Webhooks",
      "description": "How OnLink tells you an order moved, and how you verify that it was us.\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 four events, and what is not among them\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. |\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 | 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 payload is a fixed seven-field `data` object. 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## 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  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Your USDT deposit was attributed to this order. On the buy leg the same event carries `status: \"payment_matched\"` — one event, one handler, and you need not know which internal status name carried it."
          },
          "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\": \"payment_matched\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "The buy leg’s funds-confirmed delivery. Identical `type` to the sell leg’s; only `data.side` and `data.status` differ."
          },
          "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  }\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\": \"3a7c1f90-8b2d-4e56-9a01-cd23ef456789\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"sell\",\n    \"status\": \"rejected\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\"\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\": \"9c1e4b07-6d52-4a83-91fe-40ab72c5d318\",\n    \"partnerReference\": \"your-own-reference-0001\",\n    \"side\": \"buy\",\n    \"status\": \"expired\",\n    \"kesAmount\": \"1281000.00\",\n    \"usdtAmount\": \"10000.000000\",\n    \"rate\": \"128.1000\"\n  }\n}"
            },
            "url": {
              "raw": "{{webhookUrl}}",
              "host": ["{{webhookUrl}}"]
            },
            "description": "Terminal non-settlement — nothing arrived inside the window. 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}"
            }
          ]
        }
      ]
    }
  ],
  "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 \"05 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": "paymentReference",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/orders/buy. The reference OnLink issued for that order — quote it to us, and keep it in your own records on the mpesa rail where it cannot be transmitted."
    },
    {
      "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`. The buy leg delivers here, and skips when this is empty."
    },
    {
      "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": "depositAddress",
      "value": "",
      "type": "string",
      "description": "Chained by POST /v1/orders/sell, in full."
    }
  ]
}
