{
  "info": {
    "name": "PayBridge Merchant API",
    "_postman_id": "7a0f4c9e-2b1d-4c6a-9d5e-1f3b8a6c2d40",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "# PayBridge Merchant API\n\nTake card payments from your own server, then look them up, refund them and receive the result by webhook (a **postback**).\nEvery endpoint has a ready-to-send request, test scripts that save ids for the next call, and example responses.\n\n## Quick start (5 minutes)\n\n1. **Pick an environment.** Import `paybridge-staging.postman_environment.json` and select it (`baseUrl` is the staging gateway), or set the same variables on this collection.\n2. **Fill in** `merchantIdentifier`, `apiKey` and `midName` (from your sandbox access) and a public `postbackUrl` (for example a https://webhook.site address).\n3. **Run `0. Read me first / Health check`.** It needs no key and answers `200` when the gateway is up.\n4. **Run `1. Payments / 1.1 Create payment (full)`**, then `2. Lookup / 2.1 Lookup by referenceCode`, then `3. Refund / 3.1 Refund payment`.\n5. **Try the hosted checkout** with `1.4 Open hosted checkout session`: open the returned `checkoutUrl` and pay with a test card.\n\n## Two ways to take a payment\n\n| | Direct API | Hosted checkout |\n|---|---|---|\n| Request | `POST /api/payment-process` | `POST /api/payment-session` |\n| Card number | Sent by **your server** in the request | Typed by the shopper on the gateway's page (iframe/widget) |\n| Extra header | none | `Idempotency-Key` (required) |\n| Answer | Immediate status, maybe a 3-D Secure `redirectUrl` | `sessionId` + `checkoutUrl` (valid 30 minutes, one payment) |\n| Final result | Postback, or Lookup | Postback, or Lookup |\n\nPrefer the hosted checkout when you can: card data never touches your server or page.\n\n## Authentication\n\nSend your secret key in the `x-api-key` header. This collection sets it once, on the collection, from the `apiKey` variable. The key must belong to the `merchantIdentifier` in the body, otherwise the answer is `401`. After repeated failed attempts from one address the gateway answers `429 Too many failed attempts` for a few minutes. Call the API only from your server, never from a browser, and keep the key out of git and logs.\n\n## Variables\n\n| Variable | You set it? | Meaning |\n|---|---|---|\n| `baseUrl` | yes | Gateway address, no trailing slash |\n| `merchantIdentifier` | yes | Your merchant id (20-40 characters) |\n| `apiKey` | yes | Secret key, sent as `x-api-key` |\n| `midName` | yes | The account (MID) that processes your payments |\n| `postbackUrl` | yes | Public http(s) URL that receives results |\n| `yourServerUrl` | optional | Your own endpoint, used only by folder 4 |\n| `referenceCode`, `transactionUuid`, `paymentUuid`, `sessionId`, `sessionIdempotencyKey` | no | Filled in by the test scripts after each call |\n\n## Statuses\n\n| `status` | Meaning | What to do |\n|---|---|---|\n| `APPROVED` | Money captured | Fulfil the order |\n| `DECLINED` | Refused | Show a retry message |\n| `ERROR` | Technical failure | Treat as unpaid; look up again later |\n| `IN PROGRESS` | Not final | Send the customer to `redirectUrl` if present (3-D Secure); otherwise wait for the postback or poll Lookup |\n| `PENDING` | Not final, as written by **Lookup and postbacks** | Same as `IN PROGRESS`; handle both spellings |\n\n## Source of truth\n\nThe browser (widget callbacks, return from 3-D Secure) is feedback for the shopper, not proof of payment. Ship goods only after a postback or a Lookup says `APPROVED`. Do not retry a sale after a timeout until a Lookup by `referenceCode` shows it does not exist.\n\n## Folders\n\n* **0. Read me first**: full guide and health check.\n* **1. Payments**: direct sale (full, minimal, 3-D Secure) and hosted checkout session.\n* **2. Lookup**: by `referenceCode` or `paymentUuid`.\n* **3. Refund**: full refund of an approved payment.\n* **4. Postback (your server)**: simulate the gateway calling your endpoint.\n* **5. Common errors**: each request breaks one rule on purpose.\n"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "x-api-key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "transactionUuid",
      "value": ""
    },
    {
      "key": "paymentUuid",
      "value": ""
    },
    {
      "key": "refundUuid",
      "value": ""
    },
    {
      "key": "referenceCode",
      "value": ""
    },
    {
      "key": "yourServerUrl",
      "value": "https://your-server.example.com/webhooks/paybridge"
    },
    {
      "key": "baseUrl",
      "value": "https://gateway.example.com"
    },
    {
      "key": "merchantIdentifier",
      "value": ""
    },
    {
      "key": "apiKey",
      "value": ""
    },
    {
      "key": "midName",
      "value": ""
    },
    {
      "key": "postbackUrl",
      "value": "https://webhook.site/replace-with-your-id"
    },
    {
      "key": "sessionId",
      "value": ""
    },
    {
      "key": "sessionIdempotencyKey",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "0. Read me first",
      "description": "# PayBridge Merchant API - Postman collection\n\nWelcome! This collection lets you try every merchant endpoint **before** you write a line of code,\nand then copy what works into your own backend. No payment experience needed.\n\n## The 60-second picture\n\n```\nYour server ──(1) POST /api/payment-process──► Gateway ──► card processor\n     ▲                                             │\n     │◄──(2) immediate answer (APPROVED / DECLINED / IN PROGRESS + maybe redirectUrl)\n     │\n     │◄──(3) POSTBACK to your postbackUrl when the final result is known\n     │\n     └──(4) POST /payment-Lookup any time you want to double-check a payment\n     └──(5) POST /payment-refund to give money back (only for APPROVED payments)\n```\n\n**Golden rule: the gateway is called from YOUR SERVER, never from the browser.** Your API key must\nstay secret, and card data should reach you only as briefly as possible.\n\n## Step 0 - Setup (2 minutes)\n\n1. Import `paybridge-merchant-api.postman_collection.json` into Postman.\n2. Open the collection's **Variables** tab.\n3. Fill in the values your account manager gave you (or import `paybridge-staging.postman_environment.json`, select it and fill `merchantIdentifier`, `apiKey`, `midName`):\n\n| Variable | What it is | Example |\n|---|---|---|\n| `baseUrl` | Gateway address | `https://gateway.example.com` |\n| `merchantIdentifier` | Your merchant id, 20-40 characters | `a1b2c3d4-e5f6-7890-abcd-ef1234567890` |\n| `apiKey` | Your secret key, sent as `x-api-key` | (from your account manager) |\n| `midName` | The account (MID) that processes your payments | `YOUR_MID` |\n| `postbackUrl` | **Public** https URL of your server that receives results | a https://webhook.site URL |\n\nThe other variables (`referenceCode`, `transactionUuid`, `paymentUuid`...) are filled in\n**automatically** by the test scripts after each call.\n\n## Step 1 - Run the happy path (in order)\n\n1. `1. Payments / 1.1 Create payment (full)` - sends a test sale.\n2. `2. Lookup / 2.1 Lookup by referenceCode` - asks \"what happened to it?\"\n3. `3. Refund / 3.1 Refund payment` - returns the money (works only when the sale was APPROVED). The response is `{success, message, data}`: `success: true, message: \"OK\"` means refunded now; `success: true, message: \"PENDING\"` means the processor accepted it and will settle it later (confirm with Lookup or the `REFUND` postback); `success: false, message: \"ERROR\"` means no outcome or rejected.\n\n4. `1. Payments / 1.4 Open hosted checkout session` - opens a hosted-checkout session (no card fields; `Idempotency-Key` required). Open the returned `checkoutUrl` to pay with a test card. The result still arrives by postback.\n\nOpen the **Console** (View -> Show Postman Console) to see helpful messages from the scripts.\n\n## Step 2 - Understand the response\n\n| `status` | Meaning | What you do |\n|---|---|---|\n| `APPROVED` | Money captured | Fulfil the order |\n| `DECLINED` | Refused (card, risk, ban list...) | Show the customer a retry message |\n| `ERROR` | Technical failure | Treat as not paid; look up again later |\n| `IN PROGRESS` | Not final yet | If `redirectUrl` is present, send the customer there (3-D Secure). Otherwise wait for the postback / poll Lookup |\n\n> **Gotcha:** the sale response writes the pending state as `IN PROGRESS` (with a space), while\n> **Lookup and the postback write it as `PENDING`**. Handle both strings.\n\n## Step 3 - Receive postbacks (webhooks)\n\nWhen a payment reaches its result the gateway POSTs JSON to the `postbackUrl` you sent with the sale:\n\n```json\n{\n  \"transactionUuid\": \"6f1c...\", \"paymentUuid\": \"pay_123\", \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\", \"amount\": \"10.0\", \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\", \"referenceCode\": \"PM-LX3K2-AB12\"\n}\n```\n\nRules for your endpoint:\n\n- Answer with any **2xx** status quickly. Anything else (or a timeout) means \"try again\".\n- Retries use growing delays (1, 1, 2, 3, 5, 8, 13... minutes) and stop after 10 attempts, then the postback is marked FAILED.\n  Use Lookup to reconcile anything you missed.\n- The status in a postback is `APPROVED`, `DECLINED`, `ERROR` or `PENDING`; refunded and charged-back payments are reported as `APPROVED`.\n- The header `X-Idempotency-Key` carries the `transactionUuid`. **The same postback can arrive more than once** -\n  store that key and ignore repeats.\n- Always **confirm with Lookup** before shipping goods if you do not fully trust the caller.\n- `postbackUrl` must be a public `http`/`https` URL on port 80 or 443, without credentials (`user:pw@`). Anything else (including `localhost`\n  and private IPs) is refused with HTTP 400 `postbackUrl is not allowed: <reason>`. Redirects are never followed.\n  While developing, use a public test URL such as https://webhook.site.\n- The folder `4. Postback (your server)` has a request that simulates the gateway calling YOUR endpoint,\n  so you can test your handler.\n\n## Field rules (the usual source of 400 errors)\n\n| Field | Rule |\n|---|---|\n| `merchantIdentifier` | required, 20-40 chars |\n| `midName` | required in practice - the MID that exists for your account |\n| `currency` | required, exactly 3 letters (`USD`) |\n| `amount` | required, **a string** with a dot decimal: `\"10.00\"` |\n| `ccn` | required, 13-19 digits, no spaces |\n| `expMonth` | required, exactly 2 chars: `\"09\"` not `\"9\"` |\n| `expYear` | required, exactly 4 chars: `\"2030\"` |\n| `cvcCode` | required, 3-4 chars |\n| `referenceCode` | required, **unique per sale**, 8-30 chars. Reusing one returns 400 `Transaction reference code already exist!` |\n| `email`, `firstName`, `lastName`, `phoneNumber`, `address`, `city`, `state`, `zipCode`, `country`, `ip` | optional but strongly recommended (fraud checks, 3-D Secure). `ip` = the customer's IP address |\n| `redirectUrl` | accepted but **not used**: the customer is sent to the gateway's own 3-D Secure page, whose address comes back in the *response* `redirectUrl`. Do not rely on it to bring the customer back |\n| `postbackUrl` | optional, public URL for the result webhook |\n| `descriptor` | accepted but **not used**: the statement descriptor is the one configured on your MID |\n\n> The server's validation text says `referenceCode must be between 8 and 40 characters`, but the\n> limit that is actually enforced is **8 to 30**. Stay within 30.\n\n## Errors look like this\n\n```json\n{ \"timestamp\": \"2026-10-06T10:15:30Z\", \"status\": 400, \"error\": \"Bad Request\",\n  \"details\": \"ccn must be between 13 and 19 characters. \" }\n```\n\n| HTTP | Typical cause |\n|---|---|\n| 400 | Invalid field, duplicate `referenceCode`, unknown or inactive MID, refund of a non-approved or already refunded payment, unsafe `postbackUrl` |\n| 401 | Wrong/missing `x-api-key`, or the key does not belong to that `merchantIdentifier` |\n| 429 | Too many failed authentication attempts from your address; wait a few minutes |\n| 403 | The MID is shut down |\n| 404 | Lookup/refund could not find the payment |\n\nThe folder `5. Common errors` makes each of these happen on purpose so you can see the exact response.\n\n## Security checklist for juniors\n\n- Never put the API key or card numbers in front-end code, URLs, logs or git.\n- Never log full card numbers or CVC. Store only the last 4 digits.\n- Use HTTPS everywhere.\n- Use the **test** environment and test cards only while developing. Never test with real cards.\n- Generate `referenceCode` from YOUR order id so you can always find a payment again.\n- Do not retry a sale blindly after a timeout: first Lookup by `referenceCode`; if it exists, do not send it again.\n\n## Implementing it in your own code (Node.js example)\n\n```js\nconst res = await fetch(`${BASE_URL}/api/payment-process`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.PAYBRIDGE_API_KEY },\n  body: JSON.stringify({ merchantIdentifier, midName, currency: 'USD', amount: '10.00',\n    ccn, expMonth, expYear, cvcCode, referenceCode: `ORDER-${order.id}`, postbackUrl }),\n});\nconst out = await res.json();\nif (!res.ok) throw new Error(out.details);           // 4xx: fix the request\nif (out.redirectUrl) return redirectCustomer(out.redirectUrl); // 3-D Secure\nif (out.status === 'APPROVED') markPaid(order);\nelse if (out.status === 'IN PROGRESS') waitForPostback(order);\nelse markFailed(order, out.message);\n```\n\n## Out of scope\n\nThe `/cyb/*`, `/ng/*`, `/methodpay/*` and `/serverNotice` routes are used by the gateway's own\n3-D Secure pages and processor callbacks. **Merchants never call them.** Admin routes need staff login and are not part of this collection.\n",
      "item": [
        {
          "name": "Health check (is the gateway up?)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/health",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "health"
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "description": "No API key needed. Returns 200 when the gateway is running. Run this first if anything fails."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Gateway is up', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "response": []
        }
      ]
    },
    {
      "name": "1. Payments",
      "description": "Create a payment, either directly (**POST /api/payment-process**, card sent by your server) or through the hosted checkout (**POST /api/payment-session**, card typed by the shopper on the gateway's page). Always from your server, always with `x-api-key`.\n\nEach request builds a new unique `referenceCode` (8-30 characters) before it is sent and saves the ids from the answer for Lookup and Refund. Use the test cards on the documentation site's Testing page; results depend on the processor behind your MID.",
      "item": [
        {
          "name": "1.1 Create payment (full)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "Sends a complete sale with customer data and a webhook URL: **POST /api/payment-process**.\n\n**What happens next**\n- `APPROVED` / `DECLINED` / `ERROR`: final. Update your order.\n- `IN PROGRESS` + `redirectUrl`: redirect the customer to that URL (3-D Secure). The result arrives by postback.\n- `IN PROGRESS` without `redirectUrl`: wait for the postback or poll Lookup.\n\nThe pre-request script creates a unique `referenceCode`; the test script saves `transactionUuid` and `paymentUuid`. Run **2.1 Lookup** next.\n\n**Body fields**\n\n| Field | Required | Rule |\n|---|---|---|\n| `merchantIdentifier` | yes | 20-40 characters; must match the `x-api-key` |\n| `midName` | yes | The MID of your account; unknown or inactive MIDs return 400 |\n| `currency` | yes | Exactly 3 letters, e.g. `USD` |\n| `amount` | yes | **String** with a dot decimal and two decimals, e.g. `\"10.00\"` |\n| `ccn` | yes | 13-19 digits, no spaces |\n| `expMonth` / `expYear` | yes | `\"09\"` (2 chars) / `\"2030\"` (4 chars) |\n| `cvcCode` | yes | 3-4 characters |\n| `referenceCode` | yes | **Unique per sale**, 8-30 characters; reuse returns 400 |\n| `email`, `firstName`, `lastName`, `phoneNumber`, `address`, `city`, `state`, `zipCode`, `country`, `ip` | no | Recommended: used for fraud checks and 3-D Secure. `ip` is the customer's IP |\n| `postbackUrl` | no | Public http(s) URL on port 80/443, no credentials; localhost and private addresses are refused |\n| `redirectUrl`, `descriptor` | no | Accepted but **not used** |\n\nSandbox amounts: on the FTD stand-in a first deposit under 50.00 is approved (use a fresh `email` each time); 50.00 or more is declined.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"{{referenceCode}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Approved",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"5b1e6d52-0a4f-4c0e-9e86-2a4d8f1c7b11\",\n  \"paymentUuid\": \"pay_8841\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\",\n  \"descriptor\": \"MYSHOP*ORDER\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\",\n  \"referenceCode\": null,\n  \"redirectUrl\": null\n}"
            },
            {
              "name": "3-D Secure required",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"5b1e6d52-0a4f-4c0e-9e86-2a4d8f1c7b11\",\n  \"paymentUuid\": \"pay_8842\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"IN PROGRESS\",\n  \"amount\": \"10.00\",\n  \"currencyIso3\": \"USD\",\n  \"redirectUrl\": \"https://secure.example-processor.com/3ds/abc123\"\n}"
            },
            {
              "name": "Declined",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"9c3a...\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"DECLINED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Declined\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "// Every sale needs a UNIQUE referenceCode (8-30 characters). We build one for you each run.",
                  "const ref = 'PM-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 6).toUpperCase();",
                  "pm.collectionVariables.set('referenceCode', ref);",
                  "console.log('referenceCode for this sale:', ref);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 200 - the gateway accepted the request', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "// Save the ids so the Lookup and Refund requests work with one click.",
                  "if (b.transactionUuid) pm.collectionVariables.set('transactionUuid', b.transactionUuid);",
                  "if (b.paymentUuid)     pm.collectionVariables.set('paymentUuid', b.paymentUuid);",
                  "pm.test('status is one of IN PROGRESS / APPROVED / DECLINED / ERROR', () =>",
                  "  pm.expect(['IN PROGRESS','APPROVED','DECLINED','ERROR']).to.include(b.status));",
                  "pm.test('transactionUuid is present', () => pm.expect(b.transactionUuid).to.be.a('string'));",
                  "if (b.redirectUrl) console.log('3-D Secure needed. Send the customer to:', b.redirectUrl);"
                ]
              }
            }
          ]
        },
        {
          "name": "1.2 Create payment (minimum fields)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "The smallest valid sale: only the required fields, no customer data, no postback.\n\nUse it to see what is truly mandatory (see the field table in **1.1**). In real integrations send the customer fields too: they lower declines and are needed for 3-D Secure. Without `postbackUrl` you must poll **2.1 Lookup** for the final result.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"{{referenceCode}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Approved",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"5b1e6d52-0a4f-4c0e-9e86-2a4d8f1c7b11\",\n  \"paymentUuid\": \"pay_8843\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "// Every sale needs a UNIQUE referenceCode (8-30 characters). We build one for you each run.",
                  "const ref = 'PM-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 6).toUpperCase();",
                  "pm.collectionVariables.set('referenceCode', ref);",
                  "console.log('referenceCode for this sale:', ref);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 200 - the gateway accepted the request', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "// Save the ids so the Lookup and Refund requests work with one click.",
                  "if (b.transactionUuid) pm.collectionVariables.set('transactionUuid', b.transactionUuid);",
                  "if (b.paymentUuid)     pm.collectionVariables.set('paymentUuid', b.paymentUuid);",
                  "pm.test('status is one of IN PROGRESS / APPROVED / DECLINED / ERROR', () =>",
                  "  pm.expect(['IN PROGRESS','APPROVED','DECLINED','ERROR']).to.include(b.status));",
                  "pm.test('transactionUuid is present', () => pm.expect(b.transactionUuid).to.be.a('string'));",
                  "if (b.redirectUrl) console.log('3-D Secure needed. Send the customer to:', b.redirectUrl);"
                ]
              }
            }
          ]
        },
        {
          "name": "1.3 Create payment (3-D Secure flow)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "The sale flow that needs 3-D Secure, with an amount of 25.50.\n\nWhen the processor needs 3-D Secure the answer is `IN PROGRESS` with a `redirectUrl`. That is the **gateway's** 3-D Secure page: send the customer's browser there (the Console logs the address). The request fields `redirectUrl` and `descriptor` are accepted but ignored, so do not rely on them to bring the customer back.\n\nDo not trust the browser returning to tell you the payment succeeded: wait for the postback or call **2.1 Lookup**. A sale whose 3-D Secure authentication fails or is unavailable is declined before any charge, and the postback carries `DECLINED`.\n\nWhether a card is challenged depends on the processor and test card; see the Testing page of the documentation.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"25.50\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"{{referenceCode}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "3-D Secure required",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"7d2f9c10-3a5b-4e61-8c27-9b0e4d1a6f33\",\n  \"paymentUuid\": \"pay_8844\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"IN PROGRESS\",\n  \"amount\": \"25.50\",\n  \"currencyIso3\": \"USD\",\n  \"redirectUrl\": \"https://pg-api.blockchanger.io/...\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "// Every sale needs a UNIQUE referenceCode (8-30 characters). We build one for you each run.",
                  "const ref = 'PM-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 6).toUpperCase();",
                  "pm.collectionVariables.set('referenceCode', ref);",
                  "console.log('referenceCode for this sale:', ref);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 200 - the gateway accepted the request', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "// Save the ids so the Lookup and Refund requests work with one click.",
                  "if (b.transactionUuid) pm.collectionVariables.set('transactionUuid', b.transactionUuid);",
                  "if (b.paymentUuid)     pm.collectionVariables.set('paymentUuid', b.paymentUuid);",
                  "pm.test('status is one of IN PROGRESS / APPROVED / DECLINED / ERROR', () =>",
                  "  pm.expect(['IN PROGRESS','APPROVED','DECLINED','ERROR']).to.include(b.status));",
                  "pm.test('transactionUuid is present', () => pm.expect(b.transactionUuid).to.be.a('string'));",
                  "if (b.redirectUrl) console.log('3-D Secure needed. Send the customer to:', b.redirectUrl);"
                ]
              }
            }
          ]
        },
        {
          "name": "1.4 Open hosted checkout session",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{sessionIdempotencyKey}}",
                "description": "Required. Same key + same body returns the same session; same key + different body is 409."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-session",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-session"
              ]
            },
            "description": "Opens a hosted-checkout session for the embeddable widget: **POST /api/payment-session**. Same body as **1.1** **without the card fields** (`ccn`, `expMonth`, `expYear`, `cvcCode`): the shopper types the card on the gateway's checkout page, so card data never reaches your server or page.\n\n**Headers**: `x-api-key` (collection) and `Idempotency-Key` (required, set by the script). The same key with the same body returns the same session; the same key with a different body returns `409`.\n\n**Answer `201`**: `sessionId`, `checkoutUrl`, `status: CREATED`, `createdAt`, `expiresAt`. A session lives 30 minutes and starts at most one payment. The MID, the ban list, the postback URL and the reference code are checked when the session opens and again when the shopper pays.\n\n**Use it**\n1. Open `checkoutUrl` in a browser and pay with a test card, **or** give `sessionId` to `widget.open({ sessionId })` on your page (the page's origin must be allowed by the gateway).\n2. The result reaches your `postbackUrl`. Confirm with **2.1 Lookup** using the saved `referenceCode`.\n\n`amount` is a string greater than zero, e.g. `\"19.90\"`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"19.90\",\n  \"referenceCode\": \"{{referenceCode}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Created",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"sessionId\": \"3f0c9a52-7d1e-4b8a-9c44-0a1b2c3d4e5f\",\n  \"checkoutUrl\": \"https://pg-checkout.blockchanger.io/c/3f0c9a52-7d1e-4b8a-9c44-0a1b2c3d4e5f\",\n  \"status\": \"CREATED\",\n  \"createdAt\": \"2026-10-13T20:00:00+08:00\",\n  \"expiresAt\": \"2026-10-13T20:30:00+08:00\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "const ref = 'PM-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 6).toUpperCase();",
                  "pm.collectionVariables.set('referenceCode', ref);",
                  "pm.collectionVariables.set('sessionIdempotencyKey', 'sess-' + ref);"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('HTTP 201 - session created', () => pm.response.to.have.status(201));",
                  "const b = pm.response.json();",
                  "pm.test('sessionId and checkoutUrl are present', () => { pm.expect(b.sessionId).to.be.a('string'); pm.expect(b.checkoutUrl).to.be.a('string'); });",
                  "pm.test('status is CREATED', () => pm.expect(b.status).to.eql('CREATED'));",
                  "if (b.sessionId) pm.collectionVariables.set('sessionId', b.sessionId);",
                  "if (b.checkoutUrl) console.log('Open this page to pay with a test card:', b.checkoutUrl);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Lookup",
      "description": "Ask the gateway for the current state of a payment: **POST /payment-Lookup**. Use it after a timeout, to reconcile postbacks you may have missed, and before a refund.\n\nFind a payment by `referenceCode` (recommended: you always know it) or by the processor's `paymentUuid`. A payment that belongs to another merchant is reported exactly like a missing one (`404`). `status` here is `PENDING`, `APPROVED`, `DECLINED` or `ERROR`; an in-flight payment shows as `PENDING`, while the sale answer called it `IN PROGRESS`.",
      "item": [
        {
          "name": "2.1 Lookup by referenceCode",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payment-Lookup",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payment-Lookup"
              ]
            },
            "description": "**Recommended.** POST /payment-Lookup with your `referenceCode`. You always know it, even if the sale call timed out.\n\nThe answer has `transactionUuid`, `paymentUuid`, `type` (`DEPOSIT`, `REFUND` or `CHARGEBACK`), `status` (`PENDING`, `APPROVED`, `DECLINED` or `ERROR`), `amount`, `currencyIso3`, `message` and `referenceCode`. `404` means no such payment for your merchant.\n\nRun **1.1** (or **1.4** and pay) first so `referenceCode` is filled.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"referenceCode\": \"{{referenceCode}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Found",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"5b1e...\",\n  \"paymentUuid\": \"pay_8841\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\",\n  \"referenceCode\": \"PM-LX3K2-AB12\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "pm.test('status is PENDING / APPROVED / DECLINED / ERROR', () => pm.expect(['PENDING','APPROVED','DECLINED','ERROR']).to.include(b.status));",
                  "console.log('Payment status:', b.status, '-', b.message);"
                ]
              }
            }
          ]
        },
        {
          "name": "2.2 Lookup by paymentUuid",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payment-Lookup",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payment-Lookup"
              ]
            },
            "description": "POST /payment-Lookup with `paymentUuid`: the **processor's** transaction id returned by the sale, not the gateway's `transactionUuid`. Some processors return it only once the payment is final, and the FTD stand-in has none: if it is empty use **2.1**.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"paymentUuid\": \"{{paymentUuid}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Found",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"transactionUuid\": \"5b1e6d52-0a4f-4c0e-9e86-2a4d8f1c7b11\",\n  \"paymentUuid\": \"pay_8841\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\",\n  \"referenceCode\": \"PM-LX3K2-AB12\"\n}"
            },
            {
              "name": "Not found",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 404,\n  \"error\": \"Not Found\",\n  \"details\": \"Transaction with id pay_unknown not found!\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "// Some processors (for example FTD) return no usable paymentUuid.",
                  "if (!pm.collectionVariables.get('paymentUuid')) {",
                  "  console.warn('paymentUuid is empty: this processor did not return one. Use 2.1 (lookup by referenceCode) instead. Refund by paymentUuid is impossible for such a sale.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 404) { console.warn('404: the gateway has no payment stored under this paymentUuid (typical on the FTD processor). Use 2.1, lookup by referenceCode.'); pm.test.skip('Skipped: paymentUuid not searchable for this processor - use 2.1'); return; }",
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const b = pm.response.json();",
                  "pm.test('status is PENDING / APPROVED / DECLINED / ERROR', () => pm.expect(['PENDING','APPROVED','DECLINED','ERROR']).to.include(b.status));",
                  "console.log('Payment status:', b.status, '-', b.message);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Refund",
      "description": "Give the money back: **POST /payment-refund**, identified by the processor's `paymentUuid`.\n\n* Only **APPROVED** payments, for the **full** amount, **once**. A second try returns 400 `Transaction already refunded! ID ...`.\n* The answer is `{success, message, data}`:\n  * `success: true`, `message: \"OK\"`: refunded now.\n  * `success: true`, `message: \"PENDING\"`: the processor accepted it and will settle it later (CyberSource does this). Follow it with Lookup or the `REFUND` postback; a second refund is refused until it settles.\n  * `success: false`, `message: \"ERROR\"`: the processor gave no outcome or rejected the refund.\n* Not every processor supports refunds. The FTD stand-in does not, so test refunds on a MID that does.",
      "item": [
        {
          "name": "3.1 Refund payment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payment-refund",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payment-refund"
              ]
            },
            "description": "Refunds the payment identified by `paymentUuid` (saved automatically by the sale): **POST /payment-refund**. Full amount, once, only if it is `APPROVED`: check with **2.2 Lookup** first.\n\nUse the processor's `paymentUuid`, not `transactionUuid`. `success: true` with `message: \"PENDING\"` means the processor accepted the refund and will settle it later; confirm with Lookup or the `REFUND` postback. See the folder description for all outcomes.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"paymentUuid\": \"{{paymentUuid}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Refunded",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"OK\",\n  \"data\": {\n    \"paymentUuid\": \"7d2a...\",\n    \"refundUuid\": \"re_5521\"\n  }\n}"
            },
            {
              "name": "Already refunded",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-06T10:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"Transaction already refunded! ID 7d2a...\"\n}"
            },
            {
              "name": "Pending",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"message\": \"PENDING\",\n  \"data\": {\n    \"paymentUuid\": \"<refund record id>\",\n    \"refundUuid\": \"<processor refund id>\"\n  }\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Fail early with a readable message if the setup variables are still empty.",
                  "['baseUrl','merchantIdentifier','apiKey','midName'].forEach(function (v) {",
                  "  if (!pm.variables.get(v)) { throw new Error('Set the collection variable \"' + v + '\" first (see the README folder).'); }",
                  "});",
                  "// Some processors (for example FTD) return no usable paymentUuid.",
                  "if (!pm.collectionVariables.get('paymentUuid')) {",
                  "  console.warn('paymentUuid is empty: this processor did not return one. Use 2.1 (lookup by referenceCode) instead. Refund by paymentUuid is impossible for such a sale.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 404 || pm.response.code === 400) { console.warn('Refund not possible: ' + pm.response.text()); pm.test.skip('Skipped: sale not refundable by this paymentUuid (not APPROVED, already refunded)'); return; }",
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "const b = pm.response.json();",
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "pm.test('success flag present', () => pm.expect(b).to.have.property('success'));",
                  "if (b.data && b.data.refundUuid) pm.collectionVariables.set('refundUuid', b.data.refundUuid);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Postback (your server)",
      "description": "**This folder calls YOUR server, not the gateway.** The gateway sends this exact shape to your `postbackUrl`. Use it to test your handler. Set `yourServerUrl` to e.g. `https://your-server.example.com/webhooks/paybridge`.\n\nYour handler must: (1) answer 2xx fast, (2) be idempotent using `X-Idempotency-Key`, (3) update the order by `referenceCode`.\n\n`status` values: `APPROVED`, `DECLINED`, `ERROR`, `PENDING`. `type`: `DEPOSIT` or `REFUND`.",
      "item": [
        {
          "name": "4.1 Simulate an APPROVED postback",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Idempotency-Key",
                "value": "{{transactionUuid}}"
              }
            ],
            "url": {
              "raw": "{{yourServerUrl}}",
              "host": [
                "{{yourServerUrl}}"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"transactionUuid\": \"{{transactionUuid}}\",\n  \"paymentUuid\": \"{{paymentUuid}}\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"APPROVED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Approved\",\n  \"referenceCode\": \"{{referenceCode}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Simulates the gateway calling **your** endpoint (`yourServerUrl`) with an approved result. Expected: your server replies **2xx** quickly. Real postbacks also carry the header `X-Idempotency-Key` (= `transactionUuid`); the same postback can arrive more than once, so store the key and ignore repeats."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Your endpoint acknowledged with 2xx', () => pm.expect(pm.response.code).to.be.within(200, 299));"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "4.2 Simulate a DECLINED postback",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Idempotency-Key",
                "value": "{{transactionUuid}}-declined"
              }
            ],
            "url": {
              "raw": "{{yourServerUrl}}",
              "host": [
                "{{yourServerUrl}}"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"transactionUuid\": \"{{transactionUuid}}\",\n  \"paymentUuid\": \"{{paymentUuid}}\",\n  \"type\": \"DEPOSIT\",\n  \"status\": \"DECLINED\",\n  \"amount\": \"10.0\",\n  \"currencyIso3\": \"USD\",\n  \"message\": \"Declined\",\n  \"referenceCode\": \"{{referenceCode}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "description": "Simulates a declined result. Your server should mark the order as failed (by `referenceCode`) and reply **2xx**. Anything other than 2xx, or a timeout, makes the gateway retry with growing delays, up to 10 attempts."
          },
          "response": []
        }
      ]
    },
    {
      "name": "5. Common errors",
      "description": "Each request breaks one rule on purpose. Run them to see exactly what your code must handle. All error bodies share the shape `{timestamp, status, error, details}`.",
      "item": [
        {
          "name": "5.1 Wrong API key -> 401",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "Send a wrong or someone else's `x-api-key`. Expect **401**. Repeated failures from one address end in `429 Too many failed attempts` for a few minutes, so do not loop on this request.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "apikey",
              "apikey": [
                {
                  "key": "key",
                  "value": "x-api-key",
                  "type": "string"
                },
                {
                  "key": "value",
                  "value": "this-is-not-the-key",
                  "type": "string"
                },
                {
                  "key": "in",
                  "value": "header",
                  "type": "string"
                }
              ]
            }
          },
          "response": [
            {
              "name": "Wrong key",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 401,\n  \"error\": \"Unauthorized\",\n  \"details\": \"Invalid API key or merchant!\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 401 as documented', () => pm.response.to.have.status(401));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.2 Invalid card number -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "`ccn` is too short. The `details` text names the field and the rule that broke.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Bad card",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"ccn must be between 13 and 19 characters. \"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.3 Missing field -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "`currency` omitted. Required fields are listed in **1.1**.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Missing field",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"currency key cannot be null\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.4 Duplicate referenceCode -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "Re-sends the last `referenceCode` (run **1.1** first). A unique code per order is also what protects you from double charges.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"{{referenceCode}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Duplicate",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"Transaction reference code already exist!\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Intentionally reuses the referenceCode of the last sale (run 1.1 first)."
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.5 Unknown MID -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "`midName` that does not exist for your account. A MID that is shut down returns 403 instead.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"NOPE_MID\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Unknown MID",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"MID with name NOPE_MID does not exists in database\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.6 Unsafe postbackUrl -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-process",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-process"
              ]
            },
            "description": "`postbackUrl` must be a public http(s) URL on port 80 or 443 without credentials. This request uses `ftp://`; `localhost` and private addresses are refused the same way.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"10.00\",\n  \"ccn\": \"4111111111111111\",\n  \"expMonth\": \"12\",\n  \"expYear\": \"2030\",\n  \"cvcCode\": \"123\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"phoneNumber\": \"+15125550100\",\n  \"ip\": \"203.0.113.9\",\n  \"address\": \"1 Main St\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zipCode\": \"78701\",\n  \"country\": \"US\",\n  \"postbackUrl\": \"ftp://example.com/hook\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Unsafe URL",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"postbackUrl is not allowed: <reason>\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.7 Refund of unknown payment -> 404",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/payment-refund",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payment-refund"
              ]
            },
            "description": "Refunding something the gateway cannot find (or that belongs to another merchant).\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"paymentUuid\": \"does-not-exist\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "Unknown payment",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 404,\n  \"error\": \"Not Found\",\n  \"details\": \"Transaction with id does-not-exist not found!\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Responds in under 5 seconds', () => pm.expect(pm.response.responseTime).to.be.below(5000));",
                  "pm.test('Body is JSON', () => pm.response.to.be.json);",
                  "pm.test('HTTP 404 as documented', () => pm.response.to.have.status(404));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        },
        {
          "name": "5.8 Session without Idempotency-Key -> 400",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/payment-session",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "payment-session"
              ]
            },
            "description": "`Idempotency-Key` omitted on the session call. With the header present but a different body than before, the answer is `409`.\n\nAll error bodies share the shape `{timestamp, status, error, details}`; the exact `details` wording can differ slightly from the example.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchantIdentifier\": \"{{merchantIdentifier}}\",\n  \"midName\": \"{{midName}}\",\n  \"currency\": \"USD\",\n  \"amount\": \"19.90\",\n  \"referenceCode\": \"ERR-{{$randomInt}}{{$timestamp}}\",\n  \"firstName\": \"Test\",\n  \"lastName\": \"Buyer\",\n  \"email\": \"buyer@example.test\",\n  \"postbackUrl\": \"{{postbackUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "No Idempotency-Key",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    ""
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"timestamp\": \"2026-10-13T20:15:30Z\",\n  \"status\": 400,\n  \"error\": \"Bad Request\",\n  \"details\": \"Idempotency-Key header is required\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 400 as documented', () => pm.response.to.have.status(400));",
                  "pm.test('Error body has details', () => pm.expect(pm.response.json()).to.have.property('details'));"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
