{
  "info": {
    "_postman_id": "8f2c1a90-6b3d-4d21-9f4e-partner00payg",
    "name": "HashBack Partner API (Pay-As-You-Go)",
    "description": "Partner-only API for provisioning and managing HashPay collection channels (M-Pesa Paybill / Till).\n\n## Access\n\nEvery endpoint requires a developer `API_KEY` belonging to an account whose `user_type` is `patner`. Any other account type receives **403**. Account **linking is partner-only** — this is the only API in the platform that creates a payment channel.\n\n## Billing — Pay-As-You-Go\n\n**Linking a channel costs 10 service tokens**, charged once per successful link. There is no plan, no monthly fee and no renewal.\n\nA linked channel bills **1 service token per STK prompt**, charged only when Safaricom accepts the push (`ResponseCode 0`). Top up service tokens in the dashboard under **Credits**.\n\n### About `active_till`\n\nA PAYG channel is **not** governed by `active_till`. While Pay-As-You-Go is on the channel keeps collecting for as long as service tokens last, and the expiry date is ignored entirely.\n\n`active_till` (30 days from creation) is a **dormant subscription window**. It is read at exactly one moment — if the partner switches Pay-As-You-Go off in the dashboard:\n\n| On switching PAYG off | Resulting status |\n|---|---|\n| `active_till` still in the future | `1` — Active on subscription |\n| `active_till` lapsed or unset | `2` — Expired |\n\n## Conventions\n\n- All endpoints accept **JSON** or **form-data**. List endpoints also accept **GET** query parameters.\n- Every response carries a `ResultCode`. `\"0\"` means success; any other value mirrors the HTTP status.\n- Rate limits, where applied: 30 requests / 10s per IP, and 40 requests / 60s per API key.\n\n## Setup\n\nSet the `API_KEY` collection variable to your developer key before sending anything.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.hashback.co.ke",
      "type": "string"
    },
    {
      "key": "API_KEY",
      "value": "",
      "type": "string",
      "description": "Your partner developer API key."
    },
    {
      "key": "account_id",
      "value": "HPAP202608130001",
      "type": "string",
      "description": "A HashPay account id returned by Link Account."
    }
  ],
  "item": [
    {
      "name": "Link Account",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" }
        ],
        "url": {
          "raw": "{{baseUrl}}/linkaccount",
          "host": ["{{baseUrl}}"],
          "path": ["linkaccount"]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Mama Njeri Groceries\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\",\n  \"account_no\": \"MNG001\"\n}"
        },
        "description": "Creates a new HashPay collection channel under your partner account.\n\nThe paybill or till is verified against Safaricom (Hakikisha) before anything is written, and the channel goes live immediately in Pay-As-You-Go mode.\n\n**This call costs 10 service tokens**, deducted once when the link succeeds — a rejected shortcode or a failed request costs nothing. No plan and no renewal. The channel then costs 1 service token per STK prompt.\n\n**You do not need to set a callback here.** Set a single **global webhook** in the portal under **Dashboard → Webhooks** (choose *All accounts (Global webhook)*) and every channel you own — including ones you link later — delivers there automatically. `callback_webhook` is only for overriding that endpoint on one specific channel.\n\n### Fields\n\n| Field | Required | Notes |\n|---|---|---|\n| `API_KEY` | yes | Partner developer key |\n| `accountName` | yes | Display name. If omitted, the verified merchant name from Safaricom is used |\n| `accountType` | yes | `CustomerPayBillOnline` or `CustomerBuyGoodsOnline` |\n| `paybill_no` | conditional | Required for `CustomerPayBillOnline` |\n| `account_no` | no | Account reference for paybill channels |\n| `till_no` | conditional | Required for `CustomerBuyGoodsOnline` |\n| `callback_webhook` | no | Must be a valid URL. Not needed — set a global webhook in the portal instead; use this only to override it for this channel |\n| `plan` | no | **Deprecated.** Accepted and ignored — kept so older integrations don't break |\n\n### Notes\n\n- **The same paybill or till can be linked as many times as you need.** One shortcode often sits behind several platforms (website, app, bot) — call this once per platform and each gets its own `account_id`. That `account_id` is the identifier everywhere else: webhook routing, transaction history, enabling or disabling a single channel. Duplicates are never rejected.\n- `sibling_channels` lists channels that already pointed at this same shortcode before the call — empty on a first link. Non-empty is normal when adding a platform deliberately, but it is also what an accidental retry looks like.\n- `active_till` comes back set 30 days out. This does **not** expire the channel — see the collection description.\n- Rate limited: 30/10s per IP, 40/60s per key.\n- If you have opted into the *New Channel Linked* WhatsApp alert (Settings → Notifications), a message is sent on success and costs 1 service token."
      },
      "response": [
        {
          "name": "200 — Linked successfully",
          "originalRequest": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Mama Njeri Groceries\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\",\n  \"account_no\": \"MNG001\"\n}" }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"message\": \"HashPay account linked successfully\",\n  \"account_id\": \"HPAP202608130417\",\n  \"accountName\": \"Mama Njeri Groceries\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"status\": \"active\",\n  \"active_till\": \"2026-09-12 14:22:05\",\n  \"sibling_channels\": [],\n  \"billing\": {\n    \"mode\": \"payg\",\n    \"cost_per_stk_tokens\": 1,\n    \"token_balance\": 480,\n    \"note\": \"Linking cost 10 service tokens. No renewal fee. Each STK prompt costs 1 service token.\"\n  },\n  \"validation\": {\n    \"type\": \"paybill\",\n    \"shortcode\": \"247247\",\n    \"merchant_name\": \"MAMA NJERI GROCERIES LTD\",\n    \"verified\": true\n  }\n}"
        },
        {
          "name": "400 — Missing required fields",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: API_KEY, accountName and accountType are required\"\n}"
        },
        {
          "name": "400 — Missing paybill for account type",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Test\",\n  \"accountType\": \"CustomerPayBillOnline\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: paybill_no is required for CustomerPayBillOnline\"\n}"
        },
        {
          "name": "400 — Shortcode rejected by Safaricom",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Test\",\n  \"accountType\": \"CustomerBuyGoodsOnline\",\n  \"till_no\": \"999999\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Invalid till number: The till number does not exist or is not active\",\n  \"validation_error\": \"The till number does not exist or is not active\",\n  \"code\": \"4001\"\n}"
        },
        {
          "name": "401 — Invalid API key",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"bad-key\",\n  \"accountName\": \"Test\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\"\n}" }
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"401\",\n  \"message\": \"Authentication error: Invalid API key\"\n}"
        },
        {
          "name": "403 — Not a partner account",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Test\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\"\n}" }
          },
          "status": "Forbidden",
          "code": 403,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"403\",\n  \"message\": \"Forbidden: Only partner accounts can link HashPay accounts via API\"\n}"
        },
        {
          "name": "200 — Same shortcode, second platform",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Mama Njeri — Mobile App\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\",\n  \"account_no\": \"MNG001\"\n}" }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"message\": \"HashPay account linked successfully\",\n  \"account_id\": \"HPAP202608147732\",\n  \"accountName\": \"Mama Njeri — Mobile App\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"status\": \"active\",\n  \"active_till\": \"2026-09-13 09:05:41\",\n  \"sibling_channels\": [\n    { \"account_id\": \"HPAP202608130417\", \"accountName\": \"Mama Njeri Groceries\" }\n  ],\n  \"billing\": {\n    \"mode\": \"payg\",\n    \"cost_per_stk_tokens\": 1,\n    \"token_balance\": 479,\n    \"note\": \"Linking cost 10 service tokens. No renewal fee. Each STK prompt costs 1 service token.\"\n  },\n  \"validation\": {\n    \"type\": \"paybill\",\n    \"shortcode\": \"247247\",\n    \"merchant_name\": \"MAMA NJERI GROCERIES LTD\",\n    \"verified\": true\n  }\n}"
        },
        {
          "name": "429 — Rate limit exceeded",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\"\n}" }
          },
          "status": "Too Many Requests",
          "code": 429,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"429\",\n  \"message\": \"Too Many Requests: Key rate limit exceeded\"\n}"
        },
        {
          "name": "405 — Wrong method",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] }
          },
          "status": "Method Not Allowed",
          "code": 405,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"405\",\n  \"message\": \"Method Not Allowed\"\n}"
        },
        {
          "name": "500 — Could not create",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/linkaccount", "host": ["{{baseUrl}}"], "path": ["linkaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"accountName\": \"Test\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\"\n}" }
          },
          "status": "Internal Server Error",
          "code": 500,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"500\",\n  \"message\": \"Could not link the account. Please try again.\"\n}"
        }
      ]
    },
    {
      "name": "List Linked Accounts",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/listlinkedaccounts?API_KEY={{API_KEY}}&status=payg",
          "host": ["{{baseUrl}}"],
          "path": ["listlinkedaccounts"],
          "query": [
            { "key": "API_KEY", "value": "{{API_KEY}}" },
            { "key": "status", "value": "payg", "description": "Optional filter: unactivated | active | expired | archived | pending | suspended | payg | sandbox" }
          ]
        },
        "description": "Lists every HashPay channel belonging to your partner account, with a status summary and your current service-token balance.\n\nAccepts **GET** query parameters, or **POST** with JSON / form-data.\n\n### Status values\n\n| Label | Code | Meaning |\n|---|---|---|\n| `unactivated` | 0 | Created but never activated |\n| `active` | 1 | Live on a subscription window |\n| `expired` | 2 | Subscription window lapsed |\n| `archived` | 3 | Archived |\n| `pending` | 4 | Awaiting admin review |\n| `suspended` | 5 | Suspended — admin lift only |\n| `payg` | 7 | **Live on Pay-As-You-Go.** Newly linked partner channels land here |\n| `sandbox` | 8 | Pre-go-live test mode |\n\n### Per-account fields\n\n- `billing_mode` — `payg` or `plan`.\n- `is_expired` / `days_remaining` — always `false` / `null` for PAYG channels, since `active_till` does not gate them.\n- `token_balance` (top level, under `billing`) — funds every STK prompt across all your channels."
      },
      "response": [
        {
          "name": "200 — Accounts listed",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/listlinkedaccounts?API_KEY={{API_KEY}}",
              "host": ["{{baseUrl}}"],
              "path": ["listlinkedaccounts"],
              "query": [{ "key": "API_KEY", "value": "{{API_KEY}}" }]
            }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"count\": 2,\n  \"summary\": {\n    \"total\": 2,\n    \"active\": 0,\n    \"unactivated\": 0,\n    \"expired\": 0,\n    \"archived\": 0,\n    \"pending\": 0,\n    \"suspended\": 0,\n    \"payg\": 2,\n    \"sandbox\": 0\n  },\n  \"billing\": {\n    \"mode\": \"payg\",\n    \"cost_per_stk_tokens\": 1,\n    \"token_balance\": 480\n  },\n  \"data\": [\n    {\n      \"billing_mode\": \"payg\",\n      \"account_id\": \"HPAP202608130417\",\n      \"accountName\": \"Mama Njeri Groceries\",\n      \"accountType\": \"CustomerPayBillOnline\",\n      \"till_no\": null,\n      \"paybill_no\": \"247247\",\n      \"account_no\": \"MNG001\",\n      \"status\": \"payg\",\n      \"verification_status\": \"active\",\n      \"activated_at\": \"2026-08-13 14:22:05\",\n      \"active_till\": \"2026-09-12 14:22:05\",\n      \"is_expired\": false,\n      \"days_remaining\": null,\n      \"callback_webhook\": \"https://example.com/hashback/callback\",\n      \"created_at\": \"2026-08-13 14:22:05\"\n    },\n    {\n      \"billing_mode\": \"payg\",\n      \"account_id\": \"HPAP202608119042\",\n      \"accountName\": \"Kibera Electronics\",\n      \"accountType\": \"CustomerBuyGoodsOnline\",\n      \"till_no\": \"884422\",\n      \"paybill_no\": null,\n      \"account_no\": null,\n      \"status\": \"payg\",\n      \"verification_status\": \"active\",\n      \"activated_at\": \"2026-08-11 09:10:44\",\n      \"active_till\": \"2026-09-10 09:10:44\",\n      \"is_expired\": false,\n      \"days_remaining\": null,\n      \"callback_webhook\": null,\n      \"created_at\": \"2026-08-11 09:10:44\"\n    }\n  ]\n}"
        },
        {
          "name": "400 — API_KEY missing",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listlinkedaccounts", "host": ["{{baseUrl}}"], "path": ["listlinkedaccounts"] }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: API_KEY is required\"\n}"
        },
        {
          "name": "400 — Invalid status filter",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/listlinkedaccounts?API_KEY={{API_KEY}}&status=frozen",
              "host": ["{{baseUrl}}"],
              "path": ["listlinkedaccounts"],
              "query": [
                { "key": "API_KEY", "value": "{{API_KEY}}" },
                { "key": "status", "value": "frozen" }
              ]
            }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: Invalid status. Valid values: unactivated, active, expired, archived, pending, suspended\"\n}"
        },
        {
          "name": "401 — Invalid API key",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listlinkedaccounts?API_KEY=bad-key", "host": ["{{baseUrl}}"], "path": ["listlinkedaccounts"], "query": [{ "key": "API_KEY", "value": "bad-key" }] }
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"401\",\n  \"message\": \"Authentication error: Invalid API key\"\n}"
        },
        {
          "name": "403 — Not a partner account",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listlinkedaccounts?API_KEY={{API_KEY}}", "host": ["{{baseUrl}}"], "path": ["listlinkedaccounts"], "query": [{ "key": "API_KEY", "value": "{{API_KEY}}" }] }
          },
          "status": "Forbidden",
          "code": 403,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"403\",\n  \"message\": \"Forbidden: Only partner accounts can access this endpoint\"\n}"
        }
      ]
    },
    {
      "name": "Edit Linked Account",
      "request": {
        "method": "POST",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "url": {
          "raw": "{{baseUrl}}/editlinkedaccount",
          "host": ["{{baseUrl}}"],
          "path": ["editlinkedaccount"]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"accountType\": \"CustomerBuyGoodsOnline\",\n  \"till_no\": \"884422\",\n  \"accountName\": \"Kibera Electronics\"\n}"
        },
        "description": "Updates an existing channel. **Free — nothing is charged for edits.**\n\nThe account must belong to your partner account; another partner's `account_id` returns **404**.\n\n### Fields\n\n| Field | Required | Notes |\n|---|---|---|\n| `API_KEY` | yes | Partner developer key |\n| `account_id` | yes | The channel to edit |\n| `accountType` | yes | `CustomerPayBillOnline` or `CustomerBuyGoodsOnline` — always send it, even if unchanged |\n| `accountName` | no | New display name |\n| `till_no` | conditional | Required when switching **to** `CustomerBuyGoodsOnline` |\n| `paybill_no` | conditional | Required when switching **to** `CustomerPayBillOnline` |\n| `account_no` | no | Send an empty string to clear it |\n| `callback_webhook` | no | Send an empty string to remove it |\n\n### Behaviour\n\n- Any new till or paybill is re-verified against Safaricom before it is saved.\n- Switching type nullifies the other shortcode field — Paybill → Till clears `paybill_no`, and vice versa. The cleared field appears in `updated_fields` as e.g. `paybill_no (cleared)`.\n- If `accountName` is omitted but Safaricom returns a merchant name, that name is used.\n- Sending nothing changeable returns **400 No changes detected**."
      },
      "response": [
        {
          "name": "200 — Updated successfully",
          "originalRequest": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{baseUrl}}/editlinkedaccount", "host": ["{{baseUrl}}"], "path": ["editlinkedaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"accountType\": \"CustomerBuyGoodsOnline\",\n  \"till_no\": \"884422\"\n}" }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"message\": \"Account updated successfully\",\n  \"account_id\": \"HPAP202608130417\",\n  \"updated_fields\": [\n    \"till_no\",\n    \"paybill_no (cleared)\",\n    \"accountType\",\n    \"accountName\"\n  ],\n  \"rows_affected\": 1,\n  \"current_state\": {\n    \"accountType\": \"CustomerBuyGoodsOnline\",\n    \"accountName\": \"KIBERA ELECTRONICS\",\n    \"till_no\": \"884422\",\n    \"paybill_no\": null,\n    \"account_no\": null,\n    \"callback_webhook\": \"https://example.com/hashback/callback\",\n    \"validation\": {\n      \"type\": \"till\",\n      \"shortcode\": \"884422\",\n      \"name\": \"KIBERA ELECTRONICS\",\n      \"verified\": true\n    }\n  }\n}"
        },
        {
          "name": "400 — accountType required",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/editlinkedaccount", "host": ["{{baseUrl}}"], "path": ["editlinkedaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: accountType is required\"\n}"
        },
        {
          "name": "400 — No changes detected",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/editlinkedaccount", "host": ["{{baseUrl}}"], "path": ["editlinkedaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"accountType\": \"CustomerPayBillOnline\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: No changes detected\"\n}"
        },
        {
          "name": "404 — Not yours / not found",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/editlinkedaccount", "host": ["{{baseUrl}}"], "path": ["editlinkedaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"HPAP000000000000\",\n  \"accountType\": \"CustomerPayBillOnline\",\n  \"paybill_no\": \"247247\"\n}" }
          },
          "status": "Not Found",
          "code": 404,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"404\",\n  \"message\": \"Not Found: Account not found or does not belong to your account\"\n}"
        },
        {
          "name": "403 — Not a partner account",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/editlinkedaccount", "host": ["{{baseUrl}}"], "path": ["editlinkedaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"accountType\": \"CustomerPayBillOnline\"\n}" }
          },
          "status": "Forbidden",
          "code": 403,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"403\",\n  \"message\": \"Forbidden: Only partner accounts can manage HashPay accounts via API\"\n}"
        }
      ]
    },
    {
      "name": "Register Webhook",
      "request": {
        "method": "POST",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "url": {
          "raw": "{{baseUrl}}/registerwebhook",
          "host": ["{{baseUrl}}"],
          "path": ["registerwebhook"]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"webhook_url\": \"https://example.com/hashback/callback\"\n}"
        },
        "description": "Sets or clears the payment callback URL for one channel. **Free.**\n\n**Most partners never need this.** A single **global webhook** set in the portal under **Dashboard → Webhooks** covers every channel you own, present and future. Use this endpoint only when one channel must post to a different URL — a per-channel webhook overrides the global one for that channel.\n\nSend `webhook_url` as an **empty string** to remove the webhook — the response then reports `\"Webhook removed successfully\"` and `webhook_url: null`.\n\nThe account must belong to your partner account.\n\n| Field | Required | Notes |\n|---|---|---|\n| `API_KEY` | yes | Partner developer key |\n| `account_id` | yes | The channel to update |\n| `webhook_url` | yes | Valid URL, or `\"\"` to clear |\n\nThis is the same thing as passing `callback_webhook` to **Edit Linked Account** — use whichever fits your flow."
      },
      "response": [
        {
          "name": "200 — Webhook set",
          "originalRequest": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{baseUrl}}/registerwebhook", "host": ["{{baseUrl}}"], "path": ["registerwebhook"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"webhook_url\": \"https://example.com/hashback/callback\"\n}" }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"message\": \"Webhook updated successfully\",\n  \"account_id\": \"HPAP202608130417\",\n  \"webhook_url\": \"https://example.com/hashback/callback\"\n}"
        },
        {
          "name": "200 — Webhook removed",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/registerwebhook", "host": ["{{baseUrl}}"], "path": ["registerwebhook"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"webhook_url\": \"\"\n}" }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"message\": \"Webhook removed successfully\",\n  \"account_id\": \"HPAP202608130417\",\n  \"webhook_url\": null\n}"
        },
        {
          "name": "400 — Invalid URL",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/registerwebhook", "host": ["{{baseUrl}}"], "path": ["registerwebhook"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"webhook_url\": \"not-a-url\"\n}" }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: webhook_url must be a valid HTTPS URL\"\n}"
        },
        {
          "name": "404 — Not yours / not found",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/registerwebhook", "host": ["{{baseUrl}}"], "path": ["registerwebhook"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"HPAP000000000000\",\n  \"webhook_url\": \"https://example.com/cb\"\n}" }
          },
          "status": "Not Found",
          "code": 404,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"404\",\n  \"message\": \"Not Found: Account not found or does not belong to your account\"\n}"
        },
        {
          "name": "403 — Not a partner account",
          "originalRequest": {
            "method": "POST",
            "header": [],
            "url": { "raw": "{{baseUrl}}/registerwebhook", "host": ["{{baseUrl}}"], "path": ["registerwebhook"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"webhook_url\": \"https://example.com/cb\"\n}" }
          },
          "status": "Forbidden",
          "code": 403,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"403\",\n  \"message\": \"Forbidden: Only partner accounts can manage HashPay accounts via API\"\n}"
        }
      ]
    },
    {
      "name": "List Banks & Paybills",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/listbankspaybill?API_KEY={{API_KEY}}",
          "host": ["{{baseUrl}}"],
          "path": ["listbankspaybill"],
          "query": [{ "key": "API_KEY", "value": "{{API_KEY}}" }]
        },
        "description": "Reference list of Kenyan banks and their M-Pesa paybill numbers — useful for populating a bank picker in your onboarding UI.\n\nAccepts **GET** query parameters or **POST** with JSON / form-data. Partner-only, like the rest of this collection.\n\nThe list is static and served from the platform; it does not reflect your account in any way."
      },
      "response": [
        {
          "name": "200 — Banks listed",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listbankspaybill?API_KEY={{API_KEY}}", "host": ["{{baseUrl}}"], "path": ["listbankspaybill"], "query": [{ "key": "API_KEY", "value": "{{API_KEY}}" }] }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"0\",\n  \"count\": 42,\n  \"data\": [\n    { \"name\": \"ABC Bank\", \"paybill\": \"111777\", \"type\": \"bank\" },\n    { \"name\": \"ABSA Bank\", \"paybill\": \"303030\", \"type\": \"bank\" },\n    { \"name\": \"Cooperative Bank\", \"paybill\": \"400200\", \"type\": \"bank\" },\n    { \"name\": \"Equity Bank\", \"paybill\": \"247247\", \"type\": \"bank\" }\n  ]\n}"
        },
        {
          "name": "400 — API_KEY missing",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listbankspaybill", "host": ["{{baseUrl}}"], "path": ["listbankspaybill"] }
          },
          "status": "Bad Request",
          "code": 400,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"400\",\n  \"message\": \"Bad Request: API_KEY is required\"\n}"
        },
        {
          "name": "403 — Not a partner account",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": { "raw": "{{baseUrl}}/listbankspaybill?API_KEY={{API_KEY}}", "host": ["{{baseUrl}}"], "path": ["listbankspaybill"], "query": [{ "key": "API_KEY", "value": "{{API_KEY}}" }] }
          },
          "status": "Forbidden",
          "code": 403,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"403\",\n  \"message\": \"Forbidden: Only partner accounts can access this endpoint\"\n}"
        }
      ]
    },
    {
      "name": "Renew Account (RETIRED)",
      "request": {
        "method": "POST",
        "header": [{ "key": "Content-Type", "value": "application/json" }],
        "url": {
          "raw": "{{baseUrl}}/renewhashpayaccount",
          "host": ["{{baseUrl}}"],
          "path": ["renewhashpayaccount"]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"plan\": \"1month\",\n  \"wallet_pin\": \"1234\"\n}"
        },
        "description": "**Retired — always returns 410 Gone.**\n\nPartner channels run on Pay-As-You-Go, so there is no subscription to renew. Linking costs 10 service tokens once, and each STK prompt costs 1 service token.\n\nIf your integration still calls this, remove it. To keep channels collecting, **top up service tokens** in the dashboard under **Credits** instead.\n\nThe endpoint is kept alive rather than deleted so existing integrations get this explanation instead of a bare 404."
      },
      "response": [
        {
          "name": "410 — Gone",
          "originalRequest": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": { "raw": "{{baseUrl}}/renewhashpayaccount", "host": ["{{baseUrl}}"], "path": ["renewhashpayaccount"] },
            "body": { "mode": "raw", "raw": "{\n  \"API_KEY\": \"{{API_KEY}}\",\n  \"account_id\": \"{{account_id}}\",\n  \"plan\": \"1month\"\n}" }
          },
          "status": "Gone",
          "code": 410,
          "_postman_previewlanguage": "json",
          "header": [{ "key": "Content-Type", "value": "application/json" }],
          "body": "{\n  \"ResultCode\": \"410\",\n  \"message\": \"Renewals are no longer required. Partner channels run on Pay-As-You-Go: linking costs 10 service tokens once and each STK prompt costs 1 service token. Top up your service tokens in the dashboard under Credits.\",\n  \"billing\": {\n    \"mode\": \"payg\",\n    \"cost_per_stk_tokens\": 1\n  }\n}"
        }
      ]
    }
  ]
}
