{
  "info": {
    "name": "InsPay Merchant API",
    "description": "# InsPay Merchant API\n\nVersion 1.1 · 2026-10-11\n\nItems marked **Coming soon** are changes being rolled out. They are backward compatible: integrations built to today's behaviour keep working.\n\n## 1. Environments\n\n| Environment | Base URL |\n|---|---|\n| Sandbox | `https://api-sandbox.inspay.dev/merchant` |\n| Production | `https://api.inspay.dev/merchant` |\n\n- Every environment has its own API key and secret key. Sandbox keys do not work in production.\n- Your server IPs must be allow-listed per environment. Requests from other IPs get `403`.\n- All endpoints are `POST` with a JSON body (`Content-Type: application/json`).\n- JSON field names are camelCase in requests and responses. Callbacks use snake_case.\n\n## 2. Authentication and signature\n\nHeaders on every request:\n\n| Header | Value |\n|---|---|\n| `x-api-key` | Your API key |\n| `x-signature` | HMAC-SHA256 hex digest (below) |\n\nEvery body must contain `timestamp` (Unix seconds, UTC, within 5 minutes of server time) and `nonce` (unique random string).\n\nSignature:\n\n1. Take every field in the body, dropping fields whose value is `null`.\n2. Convert every value to a string (`1000` → `\"1000\"`).\n3. Serialise as JSON with keys sorted and no whitespace (`separators=(\",\", \":\")`).\n4. `HMAC-SHA256(key = secret_key, message = that string)`, hex-encoded, lowercase.\n\n```python\nimport hmac, hashlib, json\npayload = {k: str(v) for k, v in body.items() if v is not None}\ncanonical = json.dumps(payload, sort_keys=True, separators=(\",\", \":\"))\nsignature = hmac.new(secret_key.encode(), canonical.encode(), hashlib.sha256).hexdigest()\n```\n\nSend exactly the body you signed.\n\n## 3. Amounts\n\n- **JPY is whole yen.** Send integers (`1000`, or `\"1000\"`). A fractional JPY amount (`1000.5`) is rejected with `400 \"JPY amounts must be whole yen\"`.\n- JPY amounts in responses and callbacks are JSON integers (`1000`). Other currencies are decimal strings in callbacks and numbers in responses.\n- `fee` is the total fee InsPay charges you for the transaction.\n- `actualAmount` is filled in only when the transaction completes:\n  - deposit: `amount − fee` (what was credited to your balance)\n  - withdrawal: `amount + fee` (what was debited from your balance)\n- `netAmount` (transaction query) is the same calculation, available before completion.\n\n## 4. Statuses\n\n| Status | Meaning |\n|---|---|\n| `SUCCESS` | Returned when a deposit/withdrawal is **created**. Order accepted, **not paid**. |\n| `PENDING` | In progress. |\n| `COMPLETED` | Final. Money moved; your balance changed. |\n| `FAILED` | Final. Nothing moved (a withdrawal hold is released). |\n| `CANCELLED`, `RESOLVED` | Final states set by InsPay operations. Treat as not completed unless a `COMPLETED` callback was received. |\n\nOnly `COMPLETED` means paid.\n\n## 5. Endpoints\n\n### 5.1 POST /deposit\n\nRequest:\n\n| Field | Type | Req | Notes |\n|---|---|---|---|\n| `amount` | number/string | yes | JPY whole yen |\n| `channel` | string | yes | `BANK_TRANSFER` |\n| `currency` | string | yes | e.g. `JPY` |\n| `callbackUrl` | string | yes | Receives the final status |\n| `referenceId` | string | yes | Your unique order id |\n| `playerId` | string | yes | Your player id |\n| `payerAccountName` | string | no | Payer name (katakana for JPY recommended) |\n| `payerAccount` | string | no | |\n| `payerBankCode` | string | no | Thai bank codes only (`BBL`, `KBANK`, `SCB`…). **Omit for JPY.** |\n| `timestamp`, `nonce` | | yes | |\n\n```json\n{\"amount\": 50000, \"channel\": \"BANK_TRANSFER\", \"currency\": \"JPY\",\n \"callbackUrl\": \"https://merchant.example.com/callbacks/deposit\",\n \"referenceId\": \"TXN-20261011-001\", \"playerId\": \"player_abc123\",\n \"payerAccountName\": \"ヤマダ タロウ\", \"timestamp\": 1791729147, \"nonce\": \"b9ea6ab75157aa21\"}\n```\n\nResponse `200`:\n\n| Field | Notes |\n|---|---|\n| `transactionId` | InsPay transaction id (UUID) |\n| `providerOrderId` | Internal routing reference. Do not depend on it. **Coming soon removed from merchant responses** |\n| `amount`, `fee` | `fee` known at creation |\n| `actualAmount` | `null` until completed |\n| `status` | `SUCCESS` (created) |\n| `message` | Informational text |\n| `address` | Payment page for the payer: `https://pay.inspay.dev/pay/{token}` |\n| `receiverAccount`, `receiverName`, `receiverBank`, `receiverBankCode`, `receiverBankBranch` | Bank details the payer transfers to (also shown on the payment page) |\n\n```json\n{\"transactionId\": \"4598b99d-0e13-4536-926b-a662bb34ac42\", \"amount\": 50000, \"fee\": 2250,\n \"actualAmount\": null, \"status\": \"SUCCESS\", \"message\": \"Deposit created\",\n \"address\": \"https://pay.inspay.dev/pay/C6OKm4FR6vVo3XaDn_p6Fn\",\n \"receiverAccount\": \"1234567\", \"receiverName\": \"カ）エグザンプル\", \"receiverBank\": \"Example Bank(0000)\",\n \"receiverBankCode\": null, \"receiverBankBranch\": \"Example Branch（000）\"}\n```\n\nSend the payer to `address`, or show the receiver bank details yourself.\n\n### 5.2 POST /withdraw\n\n| Field | Type | Req | Notes |\n|---|---|---|---|\n| `amount`, `channel`, `currency`, `callbackUrl`, `referenceId`, `playerId` | | yes | as deposit |\n| `destination` | string | yes | Recipient **bank code** (JPY: 4-digit Zengin code, e.g. `0005`) |\n| `destinationName` | string | yes | Recipient **bank name** |\n| `destinationAccount` | string | yes | Account number |\n| `destinationAccountName` | string | yes | Account holder name (katakana for JPY) |\n| `destinationBranchCode` | string | JPY | Branch code (3 digits) |\n| `destinationBranchName` | string | JPY | Branch name |\n| `destinationAccountType` | string | no | JPY: `1` savings (default), `2` checking |\n| `destinationBranch` | string | no | Legacy single field: digits = branch code, otherwise branch name. Prefer the two fields above. |\n| `timestamp`, `nonce` | | yes | |\n\n```json\n{\"amount\": 100000, \"channel\": \"BANK_TRANSFER\", \"currency\": \"JPY\",\n \"callbackUrl\": \"https://merchant.example.com/callbacks/withdraw\",\n \"referenceId\": \"WD-20261011-001\", \"playerId\": \"player_abc123\",\n \"destination\": \"0005\", \"destinationName\": \"MUFG Bank\",\n \"destinationBranchCode\": \"001\", \"destinationBranchName\": \"Head Office\",\n \"destinationAccountType\": \"1\", \"destinationAccount\": \"0987654\",\n \"destinationAccountName\": \"タナカ ユキ\", \"timestamp\": 1791729147, \"nonce\": \"c1d2e3f4\"}\n```\n\nResponse `200`: `transactionId`, `providerOrderId` (**Coming soon removed**), `amount`, `fee`, `actualAmount` (`null` until completed), `status` (`SUCCESS`), `message`.\nThe `amount + fee` is held from your available balance at creation; insufficient balance returns `400`.\n\n### 5.3 POST /transactions\n\nBody: `id` (your `referenceId`, or our `transactionId`), `timestamp`, `nonce`.\n\nResponse: `transactionId`, `providerOrderId` (**Coming soon removed**), `merchantId`, `referenceId`, `type` (`DEPOSIT`/`WITHDRAW`), `currency`, `status`, `playerId`, `superTransactionId`, `createdAt`, `updatedAt`, `method`, `amount`, `fee`, `netAmount`, `actualAmount`, `currentBalance`, `receiverAccount`, `receiverName`, `receiverBank`, `receiverBankCode`, `receiverBankBranch`.\n\nNot found: `404 {\"message\": \"Transaction not found\"}`.\n\n### 5.4 POST /balance\n\nBody: `currency` (optional), `timestamp`, `nonce`.\n\n```json\n[{\"currency\": \"JPY\", \"available_balance\": 4955, \"ledger_balance\": 4955}]\n```\n\nNote the snake_case keys on this endpoint.\n\n### 5.5 POST /banks\n\nBody: `currency` (optional), `timestamp`, `nonce`. Returns `[{\"name\", \"branch\", \"bankCode\", \"branchCode\", \"nameShort\"}]`. May be empty for currencies where any bank is accepted (JPY).\n\n### 5.6 Player KYC (JPY)\n\nJPY deposits require the player to be registered once. A deposit for an unregistered player fails with `502 {\"code\": \"KYC_NOT_REGISTERED\"}`.\n\n**POST /kyc-register** (note the hyphen)\n\n| Field | Req | Notes |\n|---|---|---|\n| `gateway` | yes today, **Coming soon optional** | Today: send the KYC channel value given to you by InsPay onboarding. **Coming soon** omit it; InsPay routes by currency and your account set-up. Values you already send stay accepted. |\n| `playerId` | yes | |\n| `userName` | yes | Player name in full-width katakana |\n| `currency` | yes | `JPY` |\n| `callbackUrl` | no | Receives the final KYC status |\n| `timestamp`, `nonce` | yes | |\n\nResponse: `{\"playerId\", \"gateway\", \"status\": \"PENDING\", \"message\"}`. Registration can take up to 30 minutes.\n\n**POST /kyc/status** — body `gateway` (same rules), `playerId`, `timestamp`, `nonce`. Response `playerId`, `gateway`, `status`, `providerStatus` (**Coming soon InsPay-defined values**), `updatedAt`. Never registered: `404 {\"message\": \"No KYC registration found for this player.\"}`.\n\nKYC statuses: `PENDING`, `REGISTERED` (deposits accepted), `FAILED` (re-check name and re-register), `EXPIRED` (re-register).\n\n## 6. Callbacks\n\nWhen a deposit or withdrawal reaches `COMPLETED` or `FAILED`, InsPay POSTs JSON to your `callbackUrl`:\n\n```json\n{\"transaction_id\": \"4598b99d-0e13-4536-926b-a662bb34ac42\", \"provider_order_id\": \"…\",\n \"reference_id\": \"TXN-20261011-001\", \"status\": \"COMPLETED\", \"type\": \"DEPOSIT\",\n \"amount\": 50000, \"actual_amount\": 47750, \"currency\": \"JPY\", \"method\": \"BANK_TRANSFER\"}\n```\n\n- `amount` / `actual_amount`: integers for JPY, decimal strings otherwise; `actual_amount` is `null` on failure.\n- `provider_order_id`: **Coming soon removed**.\n- Reply `HTTP 200` with body `SUCCESS` (plain text or JSON string `\"SUCCESS\"`).\n- Today a callback is sent **once**. If you miss it, poll `/transactions`. **Coming soon** retries with exponential back-off until you reply `SUCCESS`; make your handler idempotent on `transaction_id`.\n- **Coming soon** callbacks carry `x-signature`, computed over the callback body with your secret key using the same algorithm as requests. Verify it.\n\nKYC callback (to the registration `callbackUrl`):\n\n```json\n{\"event\": \"kyc.status\", \"player_id\": \"player_abc123\", \"gateway\": \"<channel value>\",\n \"status\": \"REGISTERED\", \"provider_status\": \"APPROVED\", \"currency\": \"JPY\"}\n```\n\n- Today `player_id` is prefixed with your merchant number (`31_player_abc123`). **Coming soon** your own id, unprefixed. Strip a leading `<digits>_` to be safe.\n- `gateway` / `provider_status`: **Coming soon** neutral InsPay values.\n\n## 7. Errors\n\nAll errors return JSON `{\"message\": \"...\", \"code\": \"...\"}` (`code` only where listed).\n\n| HTTP | When | Example |\n|---|---|---|\n| 400 | Validation failed | `{\"message\": \"Validation failed.\", \"fields\": [\"amount: Field required\"]}` |\n| 400 | Bad signature (**Coming soon 401**) | `{\"message\": \"Invalid signature\"}` |\n| 400 | Missing `x-signature` / timestamp outside 5 min | `{\"message\": \"Invalid or expired timestamp\"}` |\n| 400 | Fractional JPY, insufficient balance | `{\"message\": \"JPY amounts must be whole yen\"}` |\n| 401 | Missing or unknown API key | `{\"message\": \"Invalid API key\"}` |\n| 403 | Your IP is not allow-listed | `{\"message\": \"Invalid IP address.\"}` |\n| 404 | Transaction / KYC record not found | `{\"message\": \"Transaction not found\"}` |\n| 502 | Upstream payment network error | `{\"message\": \"...\", \"code\": \"KYC_NOT_REGISTERED\" \\| \"GATEWAY_NOT_ENABLED\" \\| \"KYC_NOT_SUPPORTED\"}` |\n\nTreat any `5xx` or timeout on deposit/withdraw as unknown: query `/transactions` with your `referenceId` before retrying.\n\n## 8. Changelog\n\n- 1.1 (2026-10-11): InsPay branding; JPY whole-yen integers; `fee`, `netAmount`, receiver bank fields; JPY withdrawal branch/account-type fields; corrected paths, status codes, error body, balance keys, `destinationName` meaning; payment page address; marked upcoming changes.\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Deposit",
      "description": "Returns status SUCCESS (created, not paid) and address = https://pay.inspay.dev/pay/{token}. JPY whole yen.",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/deposit",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"amount\": 1000,\n  \"channel\": \"BANK_TRANSFER\",\n  \"currency\": \"JPY\",\n  \"callbackUrl\": \"https://merchant.example.com/callbacks/deposit\",\n  \"referenceId\": \"TXN-EXAMPLE-001\",\n  \"playerId\": \"player_abc123\",\n  \"payerAccountName\": \"ヤマダ タロウ\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Withdraw",
      "description": "destination = bank code, destinationName = bank name, destinationAccountName = holder.",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/withdraw",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"amount\": 1000,\n  \"channel\": \"BANK_TRANSFER\",\n  \"currency\": \"JPY\",\n  \"callbackUrl\": \"https://merchant.example.com/callbacks/withdraw\",\n  \"referenceId\": \"WD-EXAMPLE-001\",\n  \"playerId\": \"player_abc123\",\n  \"destination\": \"0005\",\n  \"destinationName\": \"MUFG Bank\",\n  \"destinationBranchCode\": \"001\",\n  \"destinationBranchName\": \"Head Office\",\n  \"destinationAccountType\": \"1\",\n  \"destinationAccount\": \"0987654\",\n  \"destinationAccountName\": \"タナカ ユキ\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Get Transaction",
      "description": "id = your referenceId or our transactionId.",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/transactions",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"id\": \"TXN-EXAMPLE-001\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Get Balance",
      "description": "Response keys are snake_case: available_balance, ledger_balance.",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/balance",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"currency\": \"JPY\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Get Banks",
      "description": "",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/banks",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"currency\": \"JPY\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Register KYC",
      "description": "Path has a hyphen. gateway: value from onboarding today; Coming soon optional.",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/kyc-register",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"gateway\": \"{{kycChannel}}\",\n  \"playerId\": \"player_abc123\",\n  \"userName\": \"ヤマダ タロウ\",\n  \"currency\": \"JPY\",\n  \"callbackUrl\": \"https://merchant.example.com/callbacks/kyc\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    },
    {
      "name": "Get KYC Status",
      "description": "",
      "request": {
        "method": "POST",
        "url": "{{baseUrl}}/kyc/status",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"gateway\": \"{{kycChannel}}\",\n  \"playerId\": \"player_abc123\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        }
      }
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Canonical JSON matches Python json.dumps(sort_keys=True, separators=(\",\", \":\"))",
          "// after nulls are dropped and every value is stringified. Non-ASCII is \\uXXXX.",
          "function inspayEscape(value) {",
          "  let out = '\"';",
          "  for (const ch of String(value)) {",
          "    const cp = ch.codePointAt(0);",
          "    if (ch === '\"') out += '\\\\\"';",
          "    else if (ch === '\\\\') out += '\\\\\\\\';",
          "    else if (ch === '\\b') out += '\\\\b';",
          "    else if (ch === '\\f') out += '\\\\f';",
          "    else if (ch === '\\n') out += '\\\\n';",
          "    else if (ch === '\\r') out += '\\\\r';",
          "    else if (ch === '\\t') out += '\\\\t';",
          "    else if (cp < 0x20 || cp > 0x7e) {",
          "      if (cp > 0xffff) {",
          "        const adj = cp - 0x10000;",
          "        const hi = 0xd800 + (adj >> 10);",
          "        const lo = 0xdc00 + (adj & 0x3ff);",
          "        out += '\\\\u' + hi.toString(16).padStart(4, '0');",
          "        out += '\\\\u' + lo.toString(16).padStart(4, '0');",
          "      } else {",
          "        out += '\\\\u' + cp.toString(16).padStart(4, '0');",
          "      }",
          "    } else out += ch;",
          "  }",
          "  return out + '\"';",
          "}",
          "",
          "function inspayCanonical(body) {",
          "  const keys = Object.keys(body)",
          "    .filter((key) => body[key] !== null && body[key] !== undefined)",
          "    .sort();",
          "  return '{' + keys.map((key) => inspayEscape(key) + ':' + inspayEscape(String(body[key]))).join(',') + '}';",
          "}",
          "",
          "const body = JSON.parse(pm.request.body && pm.request.body.raw ? pm.request.body.raw : '{}');",
          "body.timestamp = Math.floor(Date.now() / 1000);",
          "body.nonce = CryptoJS.lib.WordArray.random(8).toString();",
          "const canonical = inspayCanonical(body);",
          "const signature = CryptoJS.HmacSHA256(canonical, pm.environment.get('secretKey')).toString(CryptoJS.enc.Hex);",
          "pm.request.body.raw = canonical;",
          "pm.request.headers.upsert({ key: 'Content-Type', value: 'application/json' });",
          "pm.request.headers.upsert({ key: 'x-api-key', value: pm.environment.get('apiKey') });",
          "pm.request.headers.upsert({ key: 'x-signature', value: signature });"
        ]
      }
    }
  ],
  "auth": {
    "type": "noauth"
  }
}
