{
  "info": {
    "_postman_id": "b7e2c1a4-3f6d-4c9e-8a12-9d5f0e7a2b31",
    "name": "PaymentStack API",
    "description": "PaymentStack Payment Gateway — merchant REST API.\n\n## Setup (do this first)\nOpen the collection's **Variables** tab and set:\n- `baseUrl` — your API base URL (e.g. https://api.your-domain.com). No trailing slash.\n- `merchant_id` — your merchant identifier.\n- `token` — your API token.\n- `secret` — your HMAC secret (used to sign every request). Keep it private.\n\nThen open any request and press **Send**.\n\n## How signing works\nEvery authenticated endpoint requires the `X-SIGNATURE` header = `HMAC-SHA256(rawRequestBody, secret)` in lowercase hex. You do **not** compute this by hand — a collection-level **pre-request script** does it for you:\n1. sets a fresh `time` (Unix epoch seconds) on every send,\n2. resolves the exact JSON body Postman is about to send,\n3. signs those exact bytes and puts the result into `X-SIGNATURE`.\n\nBecause it signs the resolved body byte-for-byte, the signature always matches what is sent — the #1 cause of `signature-error` (re-serializing the body) is avoided.\n\n## What's a variable vs. what's in the body\n- Base credentials (`baseUrl`, `merchant_id`, `token`, `secret`) are **collection variables** — set once.\n- Everything else (amount, bank, account_no, order ids, notify_url, ...) lives **in each request body** as an editable example — change it per call.\n\n---\n\nPaymentStack Payment Gateway — REST API สำหรับร้านค้า\n\n**ตั้งค่าก่อนใช้**: เปิดแท็บ Variables ของ collection แล้วกรอก `baseUrl`, `merchant_id`, `token`, `secret` จากนั้นกด Send ได้เลย — ระบบจะเซ็น `X-SIGNATURE` (HMAC-SHA256 ของ body) ให้อัตโนมัติผ่าน pre-request script (คำนวณ `time` ใหม่ทุกครั้ง และเซ็น body ชุดเดียวกับที่ส่งจริง จึงไม่เจอปัญหา `signature-error`). ค่าพื้นฐานอยู่ใน Variables ส่วนค่าอื่น ๆ (amount, bank, เลขบัญชี, order id ฯลฯ) แก้ได้ใน body ของแต่ละ request",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Connectivity",
      "item": [
        {
          "name": "GET /health",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/health",
              "host": ["{{baseUrl}}"],
              "path": ["health"]
            },
            "description": "Unauthenticated health / connectivity check. No signature, no body — use it to confirm `baseUrl` is reachable before signing real requests.\n\nตรวจการเชื่อมต่อ (ไม่ต้องเซ็น ไม่มี body) — ใช้ยืนยันว่า `baseUrl` เรียกได้ก่อนลองยิง endpoint จริง"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Balance",
      "item": [
        {
          "name": "POST /balance",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}}\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/balance",
              "host": ["{{baseUrl}}"],
              "path": ["balance"]
            },
            "description": "Retrieve current balance, frozen balance, and unsettled balance. Read-only.\n\nดูยอดเงินคงเหลือ / ยอดที่ถูกล็อก / ยอดที่ยังไม่ settle (read-only)"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Payments (Deposit)",
      "item": [
        {
          "name": "POST /payment/create (QR)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"merchant_order_id\": \"ORDER2026001\",\n  \"amount\": \"500.00\",\n  \"bank\": \"KBANK\",\n  \"account_name\": \"สมชาย ใจดี\",\n  \"account_no\": \"1234567890\",\n  \"notify_url\": \"https://your-merchant-webhook-URL.example.com/payment-callback\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/payment/create",
              "host": ["{{baseUrl}}"],
              "path": ["payment", "create"]
            },
            "description": "Create a deposit order and return a PromptPay QR string. `merchant_order_id` must be unique per merchant for 7 days — change it before re-sending or you'll get `duplicate-entry`. `bank` must be an uppercase bank code (KBANK, SCB, BBL, ...).\n\nสร้างคำสั่งฝากและรับ PromptPay QR — `merchant_order_id` ห้ามซ้ำใน 7 วัน (เปลี่ยนก่อนยิงซ้ำ ไม่งั้นได้ `duplicate-entry`); `bank` เป็นรหัสธนาคารตัวพิมพ์ใหญ่"
          },
          "response": []
        },
        {
          "name": "POST /payment/create-transfer",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"merchant_order_id\": \"ORDER2026001\",\n  \"amount\": \"500.00\",\n  \"bank\": \"KBANK\",\n  \"account_name\": \"สมชาย ใจดี\",\n  \"account_no\": \"1234567890\",\n  \"notify_url\": \"https://your-merchant-webhook-URL.example.com/payment-callback\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/payment/create-transfer",
              "host": ["{{baseUrl}}"],
              "path": ["payment", "create-transfer"]
            },
            "description": "Same body as /payment/create, but the response returns destination bank account info instead of a QR (`payment_type: TRANSFER`, `qrcode: null`, plus `deposit_*` fields). Use when the receiving account doesn't support PromptPay.\n\nbody เหมือน /payment/create แต่ response ส่งข้อมูลบัญชีปลายทางแทน QR (ใช้เมื่อไม่มี PromptPay)"
          },
          "response": []
        },
        {
          "name": "POST /payment/create-flex",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"merchant_order_id\": \"ORDER2026003\",\n  \"amount\": \"500.00\",\n  \"bank\": \"KBANK\",\n  \"account_name\": \"สมชาย ใจดี\",\n  \"account_no\": \"1234567890\",\n  \"notify_url\": \"https://your-merchant-webhook-URL.example.com/payment-callback\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/payment/create-flex",
              "host": ["{{baseUrl}}"],
              "path": ["payment", "create-flex"]
            },
            "description": "Same body as /payment/create — but the gateway picks the deposit channel per order (the platform's configured selection algorithm runs over all deposit accounts) and the response returns `payment_type: FLEX` and a `payment_url` ONLY (no `qrcode`, no `transfer_amount`, no `deposit_*`). Redirect the customer to `payment_url`; the hosted page renders the selected channel or redirects onward to that channel's payment page. Requires a payment-page domain configured for the platform.\n\nbody เหมือน /payment/create แต่ระบบเลือก channel ให้เองตาม algorithm ที่ตั้งไว้ และ response ส่งกลับ `payment_url` อย่างเดียว (ไม่มี qrcode / transfer_amount / deposit_*) — redirect ลูกค้าไป payment_url — ต้องตั้ง payment-page domain ของแพลตฟอร์มไว้ก่อน"
          },
          "response": []
        },
        {
          "name": "POST /payment/query",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"platform_order_id\": \"ABCP20260508abc123XYZ456\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/payment/query",
              "host": ["{{baseUrl}}"],
              "path": ["payment", "query"]
            },
            "description": "Look up a payment order by `platform_order_id` (the 24-char id returned by /payment/create, mode marker `P`). Replace the example id with a real one — tip: after a successful create, the collection saves it to `{{last_platform_order_id}}`.\n\nเช็คสถานะการฝากด้วย `platform_order_id` (id 24 ตัวจาก /payment/create) — แทนค่าตัวอย่างด้วย id จริง (create สำเร็จแล้วระบบเก็บไว้ที่ `{{last_platform_order_id}}`)"
          },
          "response": []
        },
        {
          "name": "POST /slip/upload",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"platform_order_id\": \"ABCP20260508abc123XYZ456\",\n  \"image_base64\": \"PASTE_BASE64_IMAGE_BYTES_HERE\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/slip/upload",
              "host": ["{{baseUrl}}"],
              "path": ["slip", "upload"]
            },
            "description": "Upload a payment slip image (base64) for an OPEN payment order (mode marker `P`). Replace `image_base64` with real base64 bytes of a PNG/JPG (max 2,000,000 bytes, no `data:image/...;base64,` prefix).\n\nอัปโหลดสลิป (base64) ให้ payment order ที่ยัง open — ใส่ค่า base64 ของ PNG/JPG จริง (ไม่เกิน 2,000,000 bytes, ห้ามใส่ prefix `data:image/...;base64,`)"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Withdrawals (Payout)",
      "item": [
        {
          "name": "POST /withdraw/create",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"merchant_order_id\": \"PAYOUT2026001\",\n  \"amount\": \"1000.00\",\n  \"bank\": \"KBANK\",\n  \"account_name\": \"ลูกค้า ปลายทาง\",\n  \"account_no\": \"1234567890\",\n  \"notify_url\": \"https://your-merchant-webhook-URL.example.com/withdraw-callback\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/withdraw/create",
              "host": ["{{baseUrl}}"],
              "path": ["withdraw", "create"]
            },
            "description": "Pay out from your merchant balance to an END CUSTOMER's bank account. Your balance is held for 10 minutes; final status arrives via webhook or /withdraw/query. To withdraw to your OWN company account, use /thb-settlement/create instead.\n\nจ่ายเงินจาก merchant balance ไปยังบัญชี 'ลูกค้าปลายทาง' (hold 10 นาที) — ถ้าจะถอนเข้าบัญชีบริษัทตัวเองให้ใช้ /thb-settlement/create"
          },
          "response": []
        },
        {
          "name": "POST /withdraw/query",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"platform_order_id\": \"ABCW20260508abc123XYZ456\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/withdraw/query",
              "host": ["{{baseUrl}}"],
              "path": ["withdraw", "query"]
            },
            "description": "Look up a withdraw order by `platform_order_id` (mode marker `W`). `status`: open | success | failed.\n\nเช็คสถานะการถอนด้วย `platform_order_id` (mode `W`)"
          },
          "response": []
        }
      ]
    },
    {
      "name": "Settlement (THB)",
      "item": [
        {
          "name": "POST /thb-settlement/create",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"merchant_order_id\": \"SETTLE2026001\",\n  \"amount\": \"50000.00\",\n  \"bank\": \"KBANK\",\n  \"account_name\": \"บริษัท เมอร์แชนต์ จำกัด\",\n  \"account_no\": \"1234567890\",\n  \"notify_url\": \"https://your-merchant-webhook-URL.example.com/settlement-callback\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/thb-settlement/create",
              "host": ["{{baseUrl}}"],
              "path": ["thb-settlement", "create"]
            },
            "description": "Withdraw your OWN merchant balance to your COMPANY's bank account (periodic settlement, fixed fee). Total deducted = amount + fee. `merchant_order_id` deduped atomically for 7 days. May be restricted to operating hours.\n\nถอน merchant balance ของคุณเข้าบัญชีบริษัทตัวเอง (settle เป็นรอบ ค่าธรรมเนียมคงที่) — หักรวม = amount + fee; `merchant_order_id` กันซ้ำ 7 วัน; อาจจำกัดเฉพาะเวลาทำการ"
          },
          "response": []
        },
        {
          "name": "POST /thb-settlement/query",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "X-SIGNATURE", "value": "{{signature}}" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_id\": \"{{merchant_id}}\",\n  \"token\": \"{{token}}\",\n  \"time\": {{time}},\n  \"platform_order_id\": \"ABCM20260509abc123XYZ456\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{baseUrl}}/thb-settlement/query",
              "host": ["{{baseUrl}}"],
              "path": ["thb-settlement", "query"]
            },
            "description": "Look up a settlement order by `platform_order_id` (mode marker `M`). `status`: open | success | failed.\n\nเช็คสถานะ settlement ด้วย `platform_order_id` (mode `M`)"
          },
          "response": []
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// PaymentStack — request signer (runs before every request in this collection).",
          "// Computes X-SIGNATURE = HMAC-SHA256(rawBody, secret) over the EXACT bytes that will be sent.",
          "// Requests without a raw body (e.g. GET /health) are skipped automatically.",
          "const body = (pm.request.body && pm.request.body.mode === 'raw') ? pm.request.body.raw : '';",
          "if (body) {",
          "    // Fresh Unix epoch (seconds) on every send — never hardcode `time`.",
          "    pm.variables.set('time', Math.floor(Date.now() / 1000));",
          "",
          "    // Resolve {{...}} exactly as Postman will before sending, then sign those bytes.",
          "    // Signing the resolved body byte-for-byte is what avoids `signature-error`.",
          "    const resolved = pm.variables.replaceIn(body);",
          "    const secret = pm.variables.get('secret') || '';",
          "    if (!secret || secret === 'YOUR_SECRET') {",
          "        console.warn('[PaymentStack] Set the \"secret\" collection variable to your real secret before sending.');",
          "    }",
          "",
          "    const signature = CryptoJS.HmacSHA256(resolved, secret).toString(CryptoJS.enc.Hex);",
          "    pm.collectionVariables.set('signature', signature);",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// PaymentStack — light shared test (kept lenient so intentional error demos don't show as failures).",
          "try {",
          "    const json = pm.response.json();",
          "    // Convenience: remember the latest order id so you can paste it into a *_query request.",
          "    if (json && json.data && json.data.platform_order_id) {",
          "        pm.collectionVariables.set('last_platform_order_id', json.data.platform_order_id);",
          "    }",
          "    pm.test('Response is JSON', function () { pm.expect(json).to.be.an('object'); });",
          "} catch (e) {",
          "    // Non-JSON response (e.g. GET /health) — nothing to assert.",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://your-api-domain.example.com",
      "type": "string"
    },
    {
      "key": "merchant_id",
      "value": "AA12345678",
      "type": "string"
    },
    {
      "key": "token",
      "value": "YOUR_TOKEN",
      "type": "string"
    },
    {
      "key": "secret",
      "value": "YOUR_SECRET",
      "type": "string"
    },
    {
      "key": "signature",
      "value": "",
      "type": "string"
    },
    {
      "key": "last_platform_order_id",
      "value": "",
      "type": "string"
    }
  ]
}
