{
  "info": {
    "_postman_id": "6e656f2d-7761-4c6c-8574-6b6974000001",
    "name": "Neo Wallet API",
    "description": "# Neo Wallet API\n\nA guided tour of the Neo Wallet API, from a first health check to a wallet credit, a safe retry, a payout, a debit on a mandate and a transfer. It holds every route of the API. Every request is signed for you by the collection's pre-request script: you never build a signature by hand.\n\n## 1. Before you start\n\nYou need a client portal account for the test environment, and an Ed25519 key pair:\n\n```bash\nopenssl genpkey -algorithm ed25519 -out key.pem\nopenssl pkey -in key.pem -pubout -out pub.pem\n```\n\nIn the client portal, open **API credentials** (owner role) and:\n\n1. register `pub.pem`: you get a **key id** (`kid_…`);\n2. rotate the API key: you get an **API key** (`nwak_…`), shown once.\n\n`key.pem` stays with you. Neo Wallet only ever holds the public key.\n\n## 2. Set up Postman (once)\n\n1. **Import** the two files of the kit's `postman/` folder: this collection and `neo-wallet.test.postman_environment.json`.\n2. Select the **Neo Wallet (test)** environment in the environment picker, top right.\n3. Open the environment and fill in these four values:\n\n| Variable     | Value                                                                                    |\n| ------------ | ---------------------------------------------------------------------------------------- |\n| `baseUrl`    | The API base URL you were given, without a trailing slash                                |\n| `keyId`      | Your key id, `kid_…`                                                                     |\n| `apiKey`     | Your API key, `nwak_…`                                                                   |\n| `signingKey` | The whole content of `key.pem`, pasted as it is. A base64 32-byte seed is accepted too   |\n\n4. **Save** the environment.\n\n`apiKey` and `signingKey` are secret variables: Postman masks them. Keep them as local values, and never share or export an environment that holds them.\n\n## 3. Run the steps in order\n\nOpen each request, press **Send**, and read its **Docs** and **Test results** tabs. Requests save what the next ones need, so the folders work top to bottom.\n\n| Step                        | Request                    | What happens                                                        |\n| --------------------------- | -------------------------- | ------------------------------------------------------------------- |\n| **1 · Check the connection** | Health                     | Proves `baseUrl` is right. Needs no credentials                     |\n| **2 · Customers and wallets** | Onboard a customer         | Creates a customer, saves `customerUid` and `walletUid`             |\n|                             | Customer summary           | Your first **signed** request. A `200` proves your credentials work |\n|                             | Attach an identifier       | Adds an email address, saves `attachedIdentifierUid`                |\n|                             | Update an identifier       | Marks that email address as verified                                |\n|                             | Record an agreement        | The customer accepted a new document                                |\n|                             | Submit a KYC version       | A periodic review of the customer                                   |\n|                             | Wallet balance             | Current, held and available balance                                 |\n| **3 · Move money**          | Fee quote                  | What a credit of this amount costs                                  |\n|                             | Wallet credit              | Credits the wallet, saves `operationUid`                            |\n|                             | Wallet credit (safe retry) | Resends the same credit under the same `Idempotency-Key`            |\n|                             | Operation detail           | Status and state timeline of the credit                             |\n|                             | Operation receipt          | Its receipt                                                         |\n| **4 · Top up and pay out**  | Top-up intent              | The reference the customer quotes when paying in. Needs `railCode`  |\n|                             | Payout                     | Pays out to the customer's phone number. Needs `railCode`           |\n| **5 · Mandates and debits** | Register a mandate         | Records the customer's consent, saves `mandateUid`. Needs `beneficiaryEntityCode` |\n|                             | Mandate detail             | Reads the mandate                                                   |\n|                             | Wallet debit               | Debits the wallet on the mandate, with the customer present         |\n|                             | Mandate debit              | Debits on the mandate alone, with the customer absent               |\n|                             | Revoke a mandate           | Ends the mandate                                                    |\n| **6 · Transfer between wallets** | Onboard the receiving customer | A second customer, saves `destinationWalletUid`            |\n|                             | Wallet transfer            | The customer pays the receiving customer                            |\n\nAlready have a customer? Skip **Onboard a customer** and set `customerUid`, `walletUid` and `identifierUid` in the environment yourself.\n\nSteps 4 and 5 need two more values, which depend on what your account is approved for: `railCode`, the payment provider to pay out through, and `beneficiaryEntityCode`, the beneficiary your mandates pay. A request whose value is missing is skipped, with a note in the Postman console.\n\nTo run a whole folder, right-click it and choose **Run folder**.\n\n## What the collection does for you\n\nBefore each request, the collection script:\n\n- fills in the `{{variables}}` of the URL and the body, so that it signs exactly what is sent;\n- builds the canonical string (method, path with query, timestamp, SHA-256 of the body) and signs it with your Ed25519 key;\n- adds `X-Api-Key`, `X-Key-Id`, `X-Timestamp` and `X-Signature`;\n- adds a fresh `X-Request-Id`;\n- never sends the same signature twice, as a signature is accepted once;\n- adds a fresh `Idempotency-Key` to every write that does not set its own;\n- fills in the customer `assertion` of a payout, a mandate, a debit or a transfer: its two timestamps, and the `transactionBinding` hash computed from the body you send.\n\nYou can read it under the collection's **Scripts** tab. The Authentication guide of the developer portal describes each step, and the kit's TypeScript and Python helpers do the same in your own code.\n\nAdd your own requests anywhere in the collection: they are signed the same way. Send JSON bodies as **raw**.\n\n## Variables\n\n| Variable                             | Set by                   | Used by                                 |\n| ------------------------------------ | ------------------------ | --------------------------------------- |\n| `baseUrl`, `keyId`                   | You                      | Every request                           |\n| `apiKey`, `signingKey`               | You (secret)             | The signing script                      |\n| `customerUid`, `walletUid`           | Onboard a customer, or you | Steps 2 and 3                         |\n| `identifierUid`                      | Onboard a customer, or you | Payout: where the money goes          |\n| `attachedIdentifierUid`              | Attach an identifier     | Update an identifier                    |\n| `destinationWalletUid`               | Onboard the receiving customer, or you | Wallet transfer           |\n| `operationUid`                       | Every request that moves money | Operation detail, Operation receipt |\n| `railCode`                           | You                      | Top-up intent, Payout                   |\n| `beneficiaryEntityCode`              | You                      | Register a mandate                      |\n| `creditIdempotencyKey`, `creditBody` | Wallet credit            | Wallet credit (safe retry)              |\n| `mandateUid`                         | Register a mandate, or you | The other requests of step 5          |\n| `assertionAuthenticatedAt`, `assertionExpiresAt`, `transactionBindingHash` | The signing script | The `assertion` of steps 4, 5 and 6 |\n| `randomMsisdn`                       | The signing script       | Onboard a customer: a new phone number for each send |\n\n## Retrying a write\n\nA write is identified by its `Idempotency-Key`. Resending the same key with the same body returns the stored response with `Idempotent-Replayed: true`, and nothing is done twice. **Wallet credit (safe retry)** shows it: it resends the last **Wallet credit** and gets the same operation back.\n\nPressing **Send** again on **Wallet credit** itself is a new credit, with a new key. To retry any other write under a key of your choice, add an `Idempotency-Key` header to that request: the script leaves it untouched.\n\n## Customer-authenticated writes\n\nA payout, a mandate, a debit with the customer present and a transfer each carry an `assertion`: your evidence that the customer authenticated for this request. Above the customer's two-factor threshold it must also be bound to the transaction. The requests of steps 4, 5 and 6 always send `two_factor` with a binding, so you do not need to track the threshold.\n\nIn Postman the script plays the part of your channel: it states that the customer authenticated now. In your integration, build the assertion only after the customer has really authenticated. A `422` `AssertionNotBoundToTransaction` means the hash does not match the body, and `AuthorizationInsufficient` means the assertion expired or was too weak.\n\n## Got a `401`?\n\nA `401` has the same body whichever check failed. Check, in this order:\n\n1. **Clock**: your computer's clock is within 300 seconds of real time.\n2. **Key id**: `keyId` names the key whose private half is in `signingKey`, and it is still active.\n3. **API key**: `apiKey` is the latest one. Rotating the API key replaces the previous one.\n4. **Environment**: the environment is selected and saved, and `baseUrl` has no trailing slash.\n\nA `403` means the request was signed correctly but your account lacks the scope the route needs.\n\nEvery response carries an `X-Request-Id`. Quote it when you contact support.\n\n## Run from a terminal\n\nWith [Newman](https://www.npmjs.com/package/newman), from the kit's `postman/` folder:\n\n```bash\nnpx newman run neo-wallet.postman_collection.json \\\n  -e neo-wallet.test.postman_environment.json \\\n  --env-var baseUrl=\"$NEO_BASE_URL\" --env-var keyId=\"$NEO_KEY_ID\" \\\n  --env-var apiKey=\"$NEO_API_KEY\" --env-var signingKey=\"$NEO_SIGNING_KEY\"\n```",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1 · Check the connection",
      "description": "An open route. It needs no credentials, so it tells a wrong `baseUrl` apart from a wrong key.",
      "item": [
        {
          "name": "Health",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The API is reachable\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "health"
              ]
            },
            "description": "Answers `200` when the API is up. If this fails, check `baseUrl` in the environment before anything else."
          },
          "response": []
        }
      ]
    },
    {
      "name": "2 · Customers and wallets",
      "description": "From here on every request is signed. Fill in `keyId`, `apiKey` and `signingKey` in the environment first.",
      "item": [
        {
          "name": "Onboard a customer",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The customer is onboarded\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"customerUid, walletUid and identifierUid are saved\", function () {",
                  "  const customer = pm.response.json();",
                  "  pm.expect(customer.uid).to.be.a(\"string\");",
                  "  pm.expect(customer.walletUid).to.be.a(\"string\");",
                  "  pm.environment.set(\"customerUid\", customer.uid);",
                  "  pm.environment.set(\"walletUid\", customer.walletUid);",
                  "  pm.environment.set(\"identifierUid\", customer.identifiers[0].uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customerType\": \"person\",\n  \"displayName\": \"Abebe Kebede\",\n  \"identifiers\": [\n    {\n      \"type\": \"msisdn\",\n      \"value\": \"{{randomMsisdn}}\"\n    }\n  ],\n  \"kyc\": {\n    \"tierCode\": \"tier_1\",\n    \"sourceReference\": \"KIT-KYC-{{$timestamp}}\",\n    \"evidence\": {\n      \"checkedBy\": \"postman\"\n    },\n    \"riskRating\": \"low\"\n  },\n  \"agreements\": [\n    {\n      \"agreementType\": \"wallet_opening\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/wallet-opening\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    },\n    {\n      \"agreementType\": \"privacy\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/privacy\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    },\n    {\n      \"agreementType\": \"fee_schedule\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/fee-schedule\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    }\n  ],\n  \"clientReference\": \"KIT-CUSTOMER-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers"
              ]
            },
            "description": "Creates a customer and opens its main wallet. Needs the `customers:write` scope.\n\nThe body carries the customer's contact identifier, the KYC level you verified and the three agreements the customer accepted in your channel: `wallet_opening`, `privacy` and `fee_schedule`.\n\nEach send onboards a **new** customer: `{{randomMsisdn}}` is a new phone number and `clientReference` is unique. Replace them with real values when you test your own data.\n\n**Saves** `customerUid` and `walletUid` for the next requests, and `identifierUid`, the phone number a payout can go to."
          },
          "response": []
        },
        {
          "name": "Customer summary",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The signed request is accepted\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "pm.test(\"It is the customer asked for\", function () {",
                  "  pm.expect(JSON.stringify(pm.response.json())).to.include(pm.variables.get(\"customerUid\"));",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers/{{customerUid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers",
                "{{customerUid}}"
              ]
            },
            "description": "Status, KYC, wallets, identifiers, agreements and mandates of one customer. Needs the `customers:read` scope.\n\nIdentifier values come back masked."
          },
          "response": []
        },
        {
          "name": "Attach an identifier",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The identifier is attached\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"attachedIdentifierUid is saved\", function () {",
                  "  const identifier = pm.response.json();",
                  "  pm.expect(identifier.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"attachedIdentifierUid\", identifier.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"class\": \"contact\",\n  \"type\": \"email\",\n  \"value\": \"kit-{{$timestamp}}@example.com\",\n  \"verificationStatus\": \"pending\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers/{{customerUid}}/identifiers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers",
                "{{customerUid}}",
                "identifiers"
              ]
            },
            "description": "Adds an identifier to the customer: here an email address you have not verified yet. Needs the `customers:write` scope.\n\nThe value comes back masked, as on every read.\n\n**Saves** `attachedIdentifierUid` for **Update an identifier**."
          },
          "response": []
        },
        {
          "name": "Update an identifier",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"attachedIdentifierUid\")) {",
                  "  console.info(\"Skipped. Run Attach an identifier first.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The identifier is updated\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "pm.test(\"It is verified\", function () {",
                  "  pm.expect(pm.response.json().verificationStatus).to.eql(\"verified\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"verificationStatus\": \"verified\",\n  \"verifiedAt\": \"{{$isoTimestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers/{{customerUid}}/identifiers/{{attachedIdentifierUid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers",
                "{{customerUid}}",
                "identifiers",
                "{{attachedIdentifierUid}}"
              ]
            },
            "description": "Changes the verification state of an identifier: here the email address is now verified. Needs the `customers:write` scope.\n\n`verifiedAt` is required when the new state is `verified`. A verified contact identifier is one a payout can go to: see `payoutEligible` in the answer."
          },
          "response": []
        },
        {
          "name": "Record an agreement",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The agreement is recorded\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agreementType\": \"fee_schedule\",\n  \"documentType\": \"fee_schedule\",\n  \"documentVersion\": \"2026-02\",\n  \"documentUri\": \"https://legal.example.com/fee-schedule\",\n  \"documentHash\": \"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\n  \"acceptedAt\": \"{{$isoTimestamp}}\",\n  \"acceptedChannel\": \"postman\",\n  \"evidence\": {\n    \"channel\": \"postman\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers/{{customerUid}}/agreements",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers",
                "{{customerUid}}",
                "agreements"
              ]
            },
            "description": "Records that the customer accepted a document in your channel, for example a new version of your fee schedule. Needs the `customers:write` scope.\n\n`documentHash` is the SHA-256 of the document the customer saw. Your `evidence` is stored and never returned."
          },
          "response": []
        },
        {
          "name": "Submit a KYC version",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The KYC version is submitted\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tierCode\": \"tier_1\",\n  \"reviewType\": \"periodic_review\",\n  \"verificationSource\": \"client_kyc\",\n  \"sourceReference\": \"KIT-KYC-REVIEW-{{$timestamp}}\",\n  \"evidence\": {\n    \"checkedBy\": \"postman\"\n  },\n  \"riskRating\": \"low\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers/{{customerUid}}/kyc-versions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers",
                "{{customerUid}}",
                "kyc-versions"
              ]
            },
            "description": "Adds a version to the customer's KYC history: here a periodic review that keeps the customer on `tier_1`. Needs the `customers:write` scope.\n\nA review is `pending` until Neo Wallet's compliance review answers, and `customer.kyc_changed` follows on your webhook. A customer has one pending version at a time: a second send for the same customer is a `409`."
          },
          "response": []
        },
        {
          "name": "Wallet balance",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The balance is returned\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/wallets/{{walletUid}}/balance",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "wallets",
                "{{walletUid}}",
                "balance"
              ]
            },
            "description": "Current, held and available balance of one wallet. Needs the `wallets:read` scope.\n\nEvery amount is a decimal string with two decimal places, such as `\"2500.00\"`. It is never a JSON number."
          },
          "response": []
        }
      ]
    },
    {
      "name": "3 · Move money",
      "description": "Quote a credit, make it, retry it safely and follow the operation. Credits are paid from your prefunded balance.",
      "item": [
        {
          "name": "Fee quote",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The quote is returned\", function () {",
                  "  pm.expect(pm.response.code).to.be.oneOf([200, 201]);",
                  "  pm.expect(pm.response.json().total).to.be.a(\"string\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"operationType\": \"wallet_credit\",\n  \"walletUid\": \"{{walletUid}}\",\n  \"amount\": \"2500.00\",\n  \"currency\": \"ETB\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/fees/quote",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "fees",
                "quote"
              ]
            },
            "description": "Fee, tax and total for an operation, before you submit it. Needs the `wallets:read` scope. A quote changes nothing on the wallet."
          },
          "response": []
        },
        {
          "name": "Wallet credit",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// The collection script has run: the request now holds the key and the body it signed.",
                  "pm.environment.set(\"creditIdempotencyKey\", pm.request.headers.get(\"Idempotency-Key\"));",
                  "pm.environment.set(\"creditBody\", pm.request.body.raw);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The credit is accepted\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"walletUid\": \"{{walletUid}}\",\n  \"amount\": \"2500.00\",\n  \"currency\": \"ETB\",\n  \"clientReference\": \"KIT-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/wallet-credits",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "wallet-credits"
              ]
            },
            "description": "Credits a customer wallet from your prefunded balance. Needs the `wallet-credits:write` scope.\n\n`amount` is a string with at most two decimal places. `clientReference` is your own reference, unique within your account: this request writes a new one for each send, so every **Send** is a new credit.\n\n**Saves** `operationUid`, and the `Idempotency-Key` and body it sent, for **Wallet credit (safe retry)**."
          },
          "response": []
        },
        {
          "name": "Wallet credit (safe retry)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"creditBody\")) {",
                  "  console.info(\"Skipped. Run Wallet credit first: there is nothing to retry yet.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The stored response is replayed\", function () {",
                  "  pm.expect(pm.response.headers.get(\"Idempotent-Replayed\")).to.eql(\"true\");",
                  "});",
                  "pm.test(\"It is the same operation, not a second credit\", function () {",
                  "  pm.expect(pm.response.json().uid).to.eql(pm.variables.get(\"operationUid\"));",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{creditIdempotencyKey}}"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{{creditBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/wallet-credits",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "wallet-credits"
              ]
            },
            "description": "Resends the last **Wallet credit**: the same body under the same `Idempotency-Key`, with a fresh timestamp, signature and `X-Request-Id`.\n\nThe API returns the stored response with `Idempotent-Replayed: true`. The wallet is credited once. This is what your code does after a timeout or a `5xx`.\n\nRun **Wallet credit** first."
          },
          "response": []
        },
        {
          "name": "Operation detail",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The operation is returned\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "pm.test(\"It is the operation asked for\", function () {",
                  "  pm.expect(pm.response.json().uid).to.eql(pm.variables.get(\"operationUid\"));",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/operations/{{operationUid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "operations",
                "{{operationUid}}"
              ]
            },
            "description": "Status and state timeline of one operation. Needs the `operations:read` scope.\n\nWebhooks tell you when an operation settles. This request is how you ask."
          },
          "response": []
        },
        {
          "name": "Operation receipt",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The receipt is returned\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/operations/{{operationUid}}/receipt",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "operations",
                "{{operationUid}}",
                "receipt"
              ]
            },
            "description": "The receipt of one operation. Needs the `operations:read` scope."
          },
          "response": []
        }
      ]
    },
    {
      "name": "4 · Top up and pay out",
      "description": "Money in and out through a payment provider. Both requests need `railCode`. Send money from the customer's wallet to the customer's own phone number, through a payment provider. A payout is approved by the customer, so it carries an `assertion`: see the Customer authentication guide of the developer portal.",
      "item": [
        {
          "name": "Top-up intent",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"railCode\")) {",
                  "  console.info(\"Skipped. Set railCode in the environment to the payment provider the customer pays through.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The top-up intent is issued\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"It carries the reference to quote\", function () {",
                  "  pm.expect(pm.response.json().intentReference).to.be.a(\"string\");",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"walletUid\": \"{{walletUid}}\",\n  \"amount\": \"250.00\",\n  \"currency\": \"ETB\",\n  \"railCode\": \"{{railCode}}\",\n  \"clientReference\": \"KIT-TOPUP-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/topup-intents",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "topup-intents"
              ]
            },
            "description": "Starts a top-up: we send a payment prompt for the amount to the customer's verified phone number. Needs the `topup-intents:write` scope.\n\nNo money moves yet. The answer carries `intentReference` and `expiresAt`. The wallet is credited when the customer approves the prompt in time: `topup.confirmed` follows on your webhook, or `topup.expired` if they did not. A customer with no verified phone number is refused with `422`.\n\nThe prompt goes to the customer's primary verified phone number. To prompt another of their verified phone numbers for this top-up only, add `\"payerIdentifierUid\": \"<identifier uid>\"` to the body.\n\n**Saves** `operationUid`, so **Operation detail** now reads this top-up."
          },
          "response": []
        },
        {
          "name": "Payout",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"railCode\")) {",
                  "  console.info(\"Skipped. Set railCode in the environment to the payment provider to pay out through.\");",
                  "  pm.execution.skipRequest();",
                  "}",
                  "if (!pm.variables.get(\"identifierUid\")) {",
                  "  console.info(\"Skipped. Run Onboard a customer, or set identifierUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The payout is accepted\", function () {",
                  "  pm.response.to.have.status(202);",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"walletUid\": \"{{walletUid}}\",\n  \"destinationIdentifierUid\": \"{{identifierUid}}\",\n  \"amount\": \"100.00\",\n  \"currency\": \"ETB\",\n  \"railCode\": \"{{railCode}}\",\n  \"clientReference\": \"KIT-PAYOUT-{{$timestamp}}\",\n  \"assertion\": {\n    \"customerUid\": \"{{customerUid}}\",\n    \"assuranceLevel\": \"two_factor\",\n    \"factor1Type\": \"pin\",\n    \"factor2Type\": \"otp\",\n    \"authenticatedAt\": \"{{assertionAuthenticatedAt}}\",\n    \"expiresAt\": \"{{assertionExpiresAt}}\",\n    \"channelCode\": \"postman\",\n    \"sourceReference\": \"KIT-SESSION-{{$timestamp}}\",\n    \"transactionBinding\": {\n      \"hash\": \"{{transactionBindingHash}}\",\n      \"algorithm\": \"sha256\",\n      \"canonical\": \"customerUid|walletUid|amount|currency|destinationIdentifierUid|clientReference\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/payouts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "payouts"
              ]
            },
            "description": "Pays out to the customer's own verified contact identifier. Needs the `payouts:write` scope.\n\nSet `railCode` in the environment to a payment provider your account is approved for. `identifierUid` is saved by **Onboard a customer**. The wallet must hold the amount plus fee and tax: run **Wallet credit** first.\n\nThe `assertion` is your evidence that the customer authenticated for this request. The collection script fills in `{{assertionAuthenticatedAt}}` and `{{assertionExpiresAt}}` (now, and five minutes from now), and computes `{{transactionBindingHash}}` from the body you send: the SHA-256 of `customerUid|walletUid|amount|currency|target|clientReference`. Change the amount or the wallet and the hash follows. In your own code, the kit's `transactionBinding()` does the same.\n\nThe answer is `202` with the operation in state `processing`: the wallet is debited and the money is on its way. The outcome follows on your webhook as `payout.confirmed`, `payout.failed` or `payout.uncertain`. Never resend a payout under a new `Idempotency-Key`.\n\n**Saves** `operationUid`, so **Operation detail** now reads this payout."
          },
          "response": []
        }
      ]
    },
    {
      "name": "5 · Mandates and debits",
      "description": "A debit needs the customer's consent first, as a mandate. Registering the mandate and debiting with the customer present both carry an `assertion`: see the Customer authentication guide of the developer portal.",
      "item": [
        {
          "name": "Register a mandate",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"beneficiaryEntityCode\")) {",
                  "  console.info(\"Skipped. Set beneficiaryEntityCode in the environment to the beneficiary the debits pay.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The mandate is registered\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"mandateUid is saved\", function () {",
                  "  const mandate = pm.response.json();",
                  "  pm.expect(mandate.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"mandateUid\", mandate.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customerUid\": \"{{customerUid}}\",\n  \"walletUid\": \"{{walletUid}}\",\n  \"beneficiaryEntityCode\": \"{{beneficiaryEntityCode}}\",\n  \"clientReference\": \"KIT-MANDATE-{{$timestamp}}\",\n  \"amountRule\": \"up_to\",\n  \"fixedOrMaxAmount\": \"500.00\",\n  \"frequency\": \"ad_hoc\",\n  \"currency\": \"ETB\",\n  \"validFrom\": \"{{assertionAuthenticatedAt}}\",\n  \"agreement\": {\n    \"documentType\": \"debit_mandate_terms\",\n    \"documentVersion\": \"2026-01\",\n    \"documentUri\": \"https://legal.example.com/debit-mandate\",\n    \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n    \"acceptedAt\": \"{{assertionAuthenticatedAt}}\",\n    \"acceptedChannel\": \"postman\",\n    \"evidence\": {\n      \"channel\": \"postman\"\n    }\n  },\n  \"assertion\": {\n    \"customerUid\": \"{{customerUid}}\",\n    \"assuranceLevel\": \"two_factor\",\n    \"factor1Type\": \"pin\",\n    \"factor2Type\": \"otp\",\n    \"authenticatedAt\": \"{{assertionAuthenticatedAt}}\",\n    \"expiresAt\": \"{{assertionExpiresAt}}\",\n    \"channelCode\": \"postman\",\n    \"sourceReference\": \"KIT-SESSION-{{$timestamp}}\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/mandates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "mandates"
              ]
            },
            "description": "Registers the customer's standing consent to be debited, with the consent document the customer accepted. Needs the `mandates:write` scope.\n\nSet `beneficiaryEntityCode` in the environment to the beneficiary the debits pay. This mandate allows debits of up to `500.00` at any time.\n\nThe `assertion` is your evidence that the customer authenticated for this request. The collection script fills in `{{assertionAuthenticatedAt}}` and `{{assertionExpiresAt}}` (now, and five minutes from now). A mandate has no amount to bind, so its assertion carries no `transactionBinding`.\n\n**Saves** `mandateUid`."
          },
          "response": []
        },
        {
          "name": "Mandate detail",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"mandateUid\")) {",
                  "  console.info(\"Skipped. Run Register a mandate, or set mandateUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The mandate is returned\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/mandates/{{mandateUid}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "mandates",
                "{{mandateUid}}"
              ]
            },
            "description": "Reads one of your mandates. Needs the `mandates:read` scope.\n\n`mandateUid` is saved by **Register a mandate**. A mandate can be debited once its `status` is `active`."
          },
          "response": []
        },
        {
          "name": "Wallet debit",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"mandateUid\")) {",
                  "  console.info(\"Skipped. Run Register a mandate, or set mandateUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The debit is accepted\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"walletUid\": \"{{walletUid}}\",\n  \"mandateUid\": \"{{mandateUid}}\",\n  \"amount\": \"50.00\",\n  \"currency\": \"ETB\",\n  \"clientReference\": \"KIT-DEBIT-{{$timestamp}}\",\n  \"assertion\": {\n    \"customerUid\": \"{{customerUid}}\",\n    \"assuranceLevel\": \"two_factor\",\n    \"factor1Type\": \"pin\",\n    \"factor2Type\": \"otp\",\n    \"authenticatedAt\": \"{{assertionAuthenticatedAt}}\",\n    \"expiresAt\": \"{{assertionExpiresAt}}\",\n    \"channelCode\": \"postman\",\n    \"sourceReference\": \"KIT-SESSION-{{$timestamp}}\",\n    \"transactionBinding\": {\n      \"hash\": \"{{transactionBindingHash}}\",\n      \"algorithm\": \"sha256\",\n      \"canonical\": \"customerUid|walletUid|amount|currency|destinationIdentifierUid|clientReference\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/wallet-debits",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "wallet-debits"
              ]
            },
            "description": "Debits the wallet on the mandate, with the customer present. Needs the `wallet-debits:write` scope.\n\nThe money goes to the mandate's beneficiary at once: the `201` is already final. The amount must respect the mandate, and the wallet must hold it.\n\nThe `assertion` is your evidence that the customer authenticated for this request. The collection script fills in `{{assertionAuthenticatedAt}}` and `{{assertionExpiresAt}}` (now, and five minutes from now), and computes `{{transactionBindingHash}}` from the body you send: the SHA-256 of `customerUid|walletUid|amount|currency|target|clientReference`. Change the amount or the wallet and the hash follows. In your own code, the kit's `transactionBinding()` does the same. For a debit, the target is the `mandateUid`.\n\n**Saves** `operationUid`, so **Operation detail** now reads this debit."
          },
          "response": []
        },
        {
          "name": "Mandate debit",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"mandateUid\")) {",
                  "  console.info(\"Skipped. Run Register a mandate, or set mandateUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The debit is answered\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"walletUid\": \"{{walletUid}}\",\n  \"amount\": \"25.00\",\n  \"currency\": \"ETB\",\n  \"clientReference\": \"KIT-MANDATE-DEBIT-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/mandates/{{mandateUid}}/debits",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "mandates",
                "{{mandateUid}}",
                "debits"
              ]
            },
            "description": "Debits the wallet on the standing mandate, with the customer absent. Needs the `wallet-debits:write` scope.\n\nThe mandate is the authorization: this request carries no `assertion`. The mandate's `frequency` is enforced here. A wallet that cannot fund the debit is not an error: the `201` carries the operation in state `failed`.\n\n**Saves** `operationUid`."
          },
          "response": []
        },
        {
          "name": "Revoke a mandate",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"mandateUid\")) {",
                  "  console.info(\"Skipped. Run Register a mandate, or set mandateUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The mandate is revoked\", function () {",
                  "  pm.response.to.have.status(200);",
                  "});",
                  "pm.test(\"Its status is revoked\", function () {",
                  "  pm.expect(pm.response.json().status).to.eql(\"revoked\");",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"Customer closed the subscription\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/mandates/{{mandateUid}}/revoke",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "mandates",
                "{{mandateUid}}",
                "revoke"
              ]
            },
            "description": "Ends the mandate. Needs the `mandates:write` scope.\n\n`reason` is required. This request revokes on your own authority; add an `assertion` to record the revocation as the customer's. A revoked mandate is final: register a new one to debit again. Revoking it twice is a `409`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "6 · Transfer between wallets",
      "description": "The customer sends money to another of your customers. The transfer is approved by the sender, so it carries the sender's `assertion`.",
      "item": [
        {
          "name": "Onboard the receiving customer",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The receiving customer is onboarded\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"destinationWalletUid is saved\", function () {",
                  "  const customer = pm.response.json();",
                  "  pm.expect(customer.walletUid).to.be.a(\"string\");",
                  "  pm.environment.set(\"destinationWalletUid\", customer.walletUid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"customerType\": \"person\",\n  \"displayName\": \"Sara Tesfaye\",\n  \"identifiers\": [\n    {\n      \"type\": \"msisdn\",\n      \"value\": \"{{randomMsisdn}}\"\n    }\n  ],\n  \"kyc\": {\n    \"tierCode\": \"tier_1\",\n    \"sourceReference\": \"KIT-KYC-{{$timestamp}}\",\n    \"evidence\": {\n      \"checkedBy\": \"postman\"\n    },\n    \"riskRating\": \"low\"\n  },\n  \"agreements\": [\n    {\n      \"agreementType\": \"wallet_opening\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/wallet-opening\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    },\n    {\n      \"agreementType\": \"privacy\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/privacy\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    },\n    {\n      \"agreementType\": \"fee_schedule\",\n      \"documentVersion\": \"2026-01\",\n      \"documentUri\": \"https://legal.example.com/fee-schedule\",\n      \"documentHash\": \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n      \"acceptedAt\": \"{{$isoTimestamp}}\",\n      \"evidence\": {\n        \"channel\": \"postman\"\n      }\n    }\n  ],\n  \"clientReference\": \"KIT-RECEIVER-{{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/customers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "customers"
              ]
            },
            "description": "Onboards a second customer to receive the transfer. It is the same request as **Onboard a customer**, and leaves `customerUid` and `walletUid` untouched.\n\n**Saves** `destinationWalletUid`. Already have a second wallet? Set `destinationWalletUid` in the environment and skip this request."
          },
          "response": []
        },
        {
          "name": "Wallet transfer",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.variables.get(\"destinationWalletUid\")) {",
                  "  console.info(\"Skipped. Run Onboard the receiving customer, or set destinationWalletUid in the environment.\");",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"The transfer is accepted\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "pm.test(\"operationUid is saved\", function () {",
                  "  const operation = pm.response.json();",
                  "  pm.expect(operation.uid).to.be.a(\"string\");",
                  "  pm.environment.set(\"operationUid\", operation.uid);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sourceWalletUid\": \"{{walletUid}}\",\n  \"destinationWalletUid\": \"{{destinationWalletUid}}\",\n  \"amount\": \"75.00\",\n  \"currency\": \"ETB\",\n  \"transferCategory\": \"p2p\",\n  \"clientReference\": \"KIT-TRANSFER-{{$timestamp}}\",\n  \"assertion\": {\n    \"customerUid\": \"{{customerUid}}\",\n    \"assuranceLevel\": \"two_factor\",\n    \"factor1Type\": \"pin\",\n    \"factor2Type\": \"otp\",\n    \"authenticatedAt\": \"{{assertionAuthenticatedAt}}\",\n    \"expiresAt\": \"{{assertionExpiresAt}}\",\n    \"channelCode\": \"postman\",\n    \"sourceReference\": \"KIT-SESSION-{{$timestamp}}\",\n    \"transactionBinding\": {\n      \"hash\": \"{{transactionBindingHash}}\",\n      \"algorithm\": \"sha256\",\n      \"canonical\": \"customerUid|walletUid|amount|currency|destinationIdentifierUid|clientReference\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/tenant/wallet-transfers",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "tenant",
                "wallet-transfers"
              ]
            },
            "description": "Moves money from the customer's wallet to the receiving wallet at once: the `201` is already final. Needs the `wallet-transfers:write` scope.\n\n`p2p` pays another customer's wallet; `merchant` pays your business collection wallet. Both wallets' profiles must allow transfers, and the sender's wallet must hold the amount plus any fee.\n\nThe `assertion` is your evidence that the customer authenticated for this request. The collection script fills in `{{assertionAuthenticatedAt}}` and `{{assertionExpiresAt}}` (now, and five minutes from now), and computes `{{transactionBindingHash}}` from the body you send: the SHA-256 of `customerUid|walletUid|amount|currency|target|clientReference`. Change the amount or the wallet and the hash follows. In your own code, the kit's `transactionBinding()` does the same. For a transfer, the wallet is the `sourceWalletUid` and the target is the `destinationWalletUid`.\n\n**Saves** `operationUid`."
          },
          "response": []
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Neo Wallet request signing. Runs before every request in this collection;",
          "// see the collection's Overview tab for what it does and the variables it reads.",
          "(function () {",
          "  const CryptoJS = require(\"crypto-js\");",
          "",
          "  const fail = (message) => {",
          "    throw new Error(\"Neo Wallet kit: \" + message);",
          "  };",
          "  const setting = (name) => {",
          "    const value = pm.variables.get(name);",
          "    if (value === undefined || value === null || String(value).trim() === \"\") {",
          "      fail(\"`\" + name + \"` is empty. Select the Neo Wallet environment (top right) and fill it in.\");",
          "    }",
          "    return String(value).trim();",
          "  };",
          "",
          "  // A fresh phone number for each send of \"Onboard a customer\".",
          "  pm.variables.set(\"randomMsisdn\", \"09\" + String(Math.floor(Math.random() * 1e8)).padStart(8, \"0\"));",
          "",
          "  // The customer authenticated just now; the assertion is good for five minutes.",
          "  // One instant for both, so the lifetime is exactly five minutes.",
          "  const authenticatedAt = Date.now();",
          "  pm.variables.set(\"assertionAuthenticatedAt\", new Date(authenticatedAt).toISOString());",
          "  pm.variables.set(\"assertionExpiresAt\", new Date(authenticatedAt + 5 * 60 * 1000).toISOString());",
          "",
          "  // Postman fills in {{variables}} after this script, but the signature covers the final bytes.",
          "  const resolve = (text) => pm.variables.replaceIn(String(text));",
          "",
          "  const method = pm.request.method.toUpperCase();",
          "  // The request target exactly as sent: strip scheme and host, keep path and query untouched.",
          "  const pathWithQuery =",
          "    resolve(pm.request.url.toString())",
          "      .replace(/^(?:[a-z][a-z0-9+.-]*:\\/\\/)?[^/?#]*/i, \"\")",
          "      .replace(/#.*$/, \"\") || \"/\";",
          "",
          "  // Health is open: it works before you have credentials.",
          "  if (/\\/tenant\\/health$/.test(pathWithQuery)) return;",
          "",
          "  // --- Ed25519 (RFC 8032), signing only. The Postman sandbox has SHA-512 but no Ed25519. ---",
          "  const P = 2n ** 255n - 19n;",
          "  const L = 2n ** 252n + 27742317777372353535851937790883648493n;",
          "  const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;",
          "  const BASE_X = 15112221349535400772501151409588531511454012693041857206046113283949847762202n;",
          "  const BASE_Y = 46316835694926478169428394003475163141307993866256225615783033603165251855960n;",
          "",
          "  const mod = (value, modulus) => ((value % modulus) + modulus) % modulus;",
          "  const power = (base, exponent, modulus) => {",
          "    let result = 1n;",
          "    for (base = mod(base, modulus); exponent > 0n; exponent >>= 1n) {",
          "      if (exponent & 1n) result = (result * base) % modulus;",
          "      base = (base * base) % modulus;",
          "    }",
          "    return result;",
          "  };",
          "  // Points are [X, Y, Z, T] in extended coordinates.",
          "  const add = (a, b) => {",
          "    const A = mod((a[1] - a[0]) * (b[1] - b[0]), P);",
          "    const B = mod((a[1] + a[0]) * (b[1] + b[0]), P);",
          "    const C = mod(2n * a[3] * b[3] * D, P);",
          "    const E = mod(2n * a[2] * b[2], P);",
          "    const [e, f, g, h] = [B - A, E - C, E + C, B + A];",
          "    return [mod(e * f, P), mod(g * h, P), mod(f * g, P), mod(e * h, P)];",
          "  };",
          "  const multiplyBase = (scalar) => {",
          "    let result = [0n, 1n, 1n, 0n];",
          "    let point = [BASE_X, BASE_Y, 1n, mod(BASE_X * BASE_Y, P)];",
          "    for (; scalar > 0n; scalar >>= 1n) {",
          "      if (scalar & 1n) result = add(result, point);",
          "      point = add(point, point);",
          "    }",
          "    return result;",
          "  };",
          "",
          "  const reverseBytes = (hex) => hex.match(/../g).reverse().join(\"\");",
          "  const fromLittleEndian = (hex) => BigInt(\"0x\" + reverseBytes(hex));",
          "  const toLittleEndian = (value) => reverseBytes(value.toString(16).padStart(64, \"0\"));",
          "  const encodePoint = ([x, y, z]) => {",
          "    const inverse = power(z, P - 2n, P);",
          "    return toLittleEndian(mod(y * inverse, P) | ((mod(x * inverse, P) & 1n) << 255n));",
          "  };",
          "  const sha512 = (hex) => CryptoJS.SHA512(CryptoJS.enc.Hex.parse(hex)).toString(CryptoJS.enc.Hex);",
          "",
          "  const sign = (seedHex, messageHex) => {",
          "    const digest = sha512(seedHex);",
          "    const scalar =",
          "      (fromLittleEndian(digest.slice(0, 64)) & ((1n << 254n) - 8n)) | (1n << 254n);",
          "    const publicKey = encodePoint(multiplyBase(scalar));",
          "    const r = mod(fromLittleEndian(sha512(digest.slice(64) + messageHex)), L);",
          "    const R = encodePoint(multiplyBase(r));",
          "    const k = mod(fromLittleEndian(sha512(R + publicKey + messageHex)), L);",
          "    return R + toLittleEndian(mod(r + k * scalar, L));",
          "  };",
          "",
          "  // `signingKey` is a PKCS#8 PEM (newlines may be written as \\n) or a base64 32-byte seed.",
          "  const loadSeed = (raw) => {",
          "    const pem = /BEGIN PRIVATE KEY/.test(raw);",
          "    const base64 = raw",
          "      .replace(/\\\\n/g, \"\\n\")",
          "      .replace(/-----[A-Z ]+-----/g, \"\")",
          "      .replace(/\\s+/g, \"\");",
          "    let hex = \"\";",
          "    try {",
          "      hex = CryptoJS.enc.Base64.parse(base64).toString(CryptoJS.enc.Hex);",
          "    } catch (error) {",
          "      hex = \"\";",
          "    }",
          "    const pkcs8 = \"302e020100300506032b657004220420\";",
          "    if (pem && hex.length === 96 && hex.startsWith(pkcs8)) return hex.slice(32);",
          "    if (!pem && hex.length === 64) return hex;",
          "    return fail(\"`signingKey` must be an Ed25519 PKCS#8 PEM (key.pem) or a base64 32-byte seed.\");",
          "  };",
          "",
          "  const apiKey = setting(\"apiKey\");",
          "  const keyId = setting(\"keyId\");",
          "  const seed = loadSeed(setting(\"signingKey\"));",
          "",
          "  let body = \"\";",
          "  if (pm.request.body && pm.request.body.mode === \"raw\" && pm.request.body.raw) {",
          "    // {{transactionBindingHash}} depends on the rest of the body, so it is filled in last.",
          "    body = resolve(pm.request.body.raw.split(\"{{transactionBindingHash}}\").join(\"@@binding@@\"));",
          "    if (body.includes(\"@@binding@@\")) {",
          "      const sent = JSON.parse(body);",
          "      const amount = /^(0|[1-9]\\d*)(?:\\.(\\d{1,2}))?$/.exec(sent.amount);",
          "      if (!amount) fail(\"`amount` must be a decimal string with at most two decimal places.\");",
          "      const bound = [",
          "        sent.assertion.customerUid,",
          "        sent.walletUid || sent.sourceWalletUid,",
          "        amount[1] + \".\" + (amount[2] || \"\").padEnd(2, \"0\"),",
          "        sent.currency,",
          "        sent.destinationIdentifierUid || sent.mandateUid || sent.destinationWalletUid || \"\",",
          "        sent.clientReference || \"\",",
          "      ].join(\"|\");",
          "      body = body.split(\"@@binding@@\").join(CryptoJS.SHA256(bound).toString(CryptoJS.enc.Hex));",
          "    }",
          "    pm.request.body.update({ mode: \"raw\", raw: body });",
          "  }",
          "",
          "  const bodyHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Hex);",
          "  // A signature is accepted once. The same request sent twice within one second would sign the",
          "  // same bytes, so the second one moves its timestamp a second ahead.",
          "  const [lastRequest, lastTimestamp] = String(pm.collectionVariables.get(\"lastSigned\") || \"\").split(\"@\");",
          "  const thisRequest = [method, pathWithQuery, bodyHash].join(\" \");",
          "  let seconds = Math.floor(Date.now() / 1000);",
          "  if (lastRequest === thisRequest && Number(lastTimestamp) >= seconds) seconds = Number(lastTimestamp) + 1;",
          "  const timestamp = String(seconds);",
          "  pm.collectionVariables.set(\"lastSigned\", thisRequest + \"@\" + timestamp);",
          "  const canonical = [method, pathWithQuery, timestamp, bodyHash].join(\"\\n\");",
          "  const signature = sign(seed, CryptoJS.enc.Utf8.parse(canonical).toString(CryptoJS.enc.Hex));",
          "",
          "  pm.request.headers.upsert({ key: \"X-Api-Key\", value: apiKey });",
          "  pm.request.headers.upsert({ key: \"X-Key-Id\", value: keyId });",
          "  pm.request.headers.upsert({ key: \"X-Timestamp\", value: timestamp });",
          "  pm.request.headers.upsert({",
          "    key: \"X-Signature\",",
          "    value: CryptoJS.enc.Hex.parse(signature).toString(CryptoJS.enc.Base64),",
          "  });",
          "  pm.request.headers.upsert({ key: \"X-Request-Id\", value: resolve(\"{{$guid}}\") });",
          "",
          "  // A write without its own Idempotency-Key header gets a fresh one: a new logical request.",
          "  const isWrite = method !== \"GET\" && method !== \"HEAD\";",
          "  const ownKey = pm.request.headers.find(",
          "    (header) => header.key.toLowerCase() === \"idempotency-key\" && !header.disabled,",
          "  );",
          "  if (isWrite && !ownKey) {",
          "    pm.request.headers.upsert({ key: \"Idempotency-Key\", value: resolve(\"{{$guid}}\") });",
          "  }",
          "})();"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Hints in the Postman console (View > Show Postman Console) when a request is refused.",
          "const requestId = pm.response.headers.get(\"X-Request-Id\");",
          "if (pm.response.code === 401) {",
          "  console.warn(",
          "    \"401. Check, in this order: your computer's clock; that keyId names the key in signingKey; \" +",
          "      \"that apiKey is the latest one; that the environment is selected and saved.\",",
          "  );",
          "}",
          "if (pm.response.code === 403) {",
          "  console.warn(\"403. The request was signed correctly, but your account lacks the scope this route needs.\");",
          "}",
          "if (pm.response.code >= 400 && requestId) {",
          "  console.info(\"Quote this X-Request-Id when you contact support: \" + requestId);",
          "}"
        ]
      }
    }
  ]
}
