{
  "info": {
    "name": "Frag — FragmentPay API",
    "description": "Accept payment from any token on any chain. Settle in one asset.\n\nBase URL is set via the `baseUrl` collection variable (default `https://api.frag.dev`). Authenticate every `/api/public/v1/*` request with your secret key via the `Authorization: Bearer {{secretKey}}` header — `sk_test_…` for test mode, `sk_live_…` for live.\n\nAll `POST` requests support `Idempotency-Key`. Retries with the same key and body return the original response; a same-key/different-body retry returns `409 conflict`.\n\nImport into Postman or Bruno (both read v2.1). Every request ships with an example response and a test script that pins status code + response shape and stores useful ids into collection variables so you can chain requests without editing them.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "f8a1c9f0-1a2b-4c3d-9e0f-fragmentpay0001"
  },
  "variable": [
    { "key": "baseUrl", "value": "https://api.frag.dev", "type": "string" },
    { "key": "secretKey", "value": "sk_test_replace_me", "type": "string" },
    { "key": "merchantWallet", "value": "MERCHANT_SOLANA_WALLET", "type": "string" },
    { "key": "intentId", "value": "", "type": "string" },
    { "key": "refundId", "value": "", "type": "string" },
    { "key": "payoutId", "value": "", "type": "string" },
    { "key": "webhookEndpointId", "value": "", "type": "string" },
    { "key": "webhookSigningSecret", "value": "", "type": "string" }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [{ "key": "token", "value": "{{secretKey}}", "type": "string" }]
  },
  "event": [
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Global sanity checks applied to every request.",
          "pm.test('response has JSON content-type', function () {",
          "  const ct = pm.response.headers.get('Content-Type') || '';",
          "  pm.expect(ct.toLowerCase()).to.include('application/json');",
          "});",
          "pm.test('response body is valid JSON', function () {",
          "  pm.response.json();",
          "});"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Payment Intents",
      "description": "Create, retrieve, list, and cancel invoices. Every POST is idempotent when you send an `Idempotency-Key`.",
      "item": [
        {
          "name": "Create PaymentIntent",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{$guid}}", "description": "Same key + same body ⇒ cached response. Different body ⇒ 409." }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 99,\n  \"currency\": \"USD\",\n  \"settlement_chain\": \"solana\",\n  \"settlement_token\": \"USDC\",\n  \"settlement_address\": \"{{merchantWallet}}\",\n  \"customer_email\": \"buyer@example.com\",\n  \"metadata\": { \"orderId\": \"order_8841\" },\n  \"expires_in_seconds\": 1800\n}"
            },
            "description": "Create a new PaymentIntent. `settlement_*` fields default to the merchant's dashboard settings when omitted."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is 200 or 201', () => pm.expect([200, 201]).to.include(pm.response.code));",
                  "const body = pm.response.json();",
                  "pm.test('has intent id', () => pm.expect(body.id, 'body.id').to.be.a('string'));",
                  "pm.test('status is created/quoted', () => pm.expect(['created','quoted']).to.include(body.status));",
                  "pm.test('has checkout_url', () => pm.expect(body.checkout_url).to.match(/^https?:\\/\\//));",
                  "if (body.id) pm.collectionVariables.set('intentId', body.id);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "201 Created",
              "originalRequest": {
                "method": "POST",
                "header": [{ "key": "Content-Type", "value": "application/json" }],
                "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents" },
                "body": { "mode": "raw", "raw": "{ \"amount\": 99, \"currency\": \"USD\" }" }
              },
              "status": "Created",
              "code": 201,
              "header": [{ "key": "Content-Type", "value": "application/json" }],
              "body": "{\n  \"id\": \"pi_11111111-1111-1111-1111-111111111111\",\n  \"merchant_id\": \"m_00000000-0000-0000-0000-000000000000\",\n  \"amount\": \"99.00\",\n  \"currency\": \"USD\",\n  \"status\": \"created\",\n  \"settlement_chain\": \"solana\",\n  \"settlement_token\": \"USDC\",\n  \"settlement_address\": \"MERCHANT_SOLANA_WALLET\",\n  \"checkout_url\": \"https://frag.dev/checkout/pi_11111111-1111-1111-1111-111111111111\",\n  \"expires_at\": \"2026-07-18T18:52:00Z\",\n  \"created_at\": \"2026-07-18T18:22:00Z\",\n  \"metadata\": { \"orderId\": \"order_8841\" }\n}"
            },
            {
              "name": "400 invalid input",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents" }, "body": { "mode": "raw", "raw": "{}" } },
              "status": "Bad Request",
              "code": 400,
              "header": [{ "key": "Content-Type", "value": "application/json" }],
              "body": "{ \"error\": \"invalid input\", \"details\": { \"fieldErrors\": { \"amount\": [\"Required\"] } } }"
            },
            {
              "name": "401 unauthorized",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents" } },
              "status": "Unauthorized",
              "code": 401,
              "body": "{ \"error\": \"unauthorized\" }"
            },
            {
              "name": "409 idempotency reused",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents" } },
              "status": "Conflict",
              "code": 409,
              "body": "{ \"error\": \"idempotency key reused with different body\" }"
            }
          ]
        },
        {
          "name": "List PaymentIntents",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents?limit=25&status=settled",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents"],
              "query": [
                { "key": "limit", "value": "25", "description": "1–200, default 50" },
                { "key": "status", "value": "settled", "description": "Optional filter — created|quoted|executing|settled|failed|expired|cancelled|refunded" }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents" } },
              "status": "OK",
              "code": 200,
              "header": [{ "key": "Content-Type", "value": "application/json" }],
              "body": "{ \"data\": [ { \"id\": \"pi_11111111-1111-1111-1111-111111111111\", \"amount\": \"99.00\", \"currency\": \"USD\", \"status\": \"settled\", \"created_at\": \"2026-07-18T18:22:00Z\" } ] }"
            }
          ]
        },
        {
          "name": "Retrieve PaymentIntent",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents", "{{intentId}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('id matches request', () => pm.expect(body.id).to.eql(pm.collectionVariables.get('intentId')));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{\n  \"id\": \"pi_11111111-1111-1111-1111-111111111111\",\n  \"amount\": \"99.00\",\n  \"currency\": \"USD\",\n  \"status\": \"settled\",\n  \"settlement_chain\": \"solana\",\n  \"settlement_token\": \"USDC\",\n  \"amount_received_usd\": \"100.00\",\n  \"platform_fee_usd\": \"1.00\",\n  \"net_settlement_usd\": \"99.00\",\n  \"settled_at\": \"2026-07-18T18:34:00Z\",\n  \"metadata\": { \"orderId\": \"order_8841\" }\n}"
            },
            {
              "name": "404 not found",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/pi_missing" } },
              "status": "Not Found",
              "code": 404,
              "body": "{ \"error\": \"not found\" }"
            }
          ]
        },
        {
          "name": "Cancel PaymentIntent",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/cancel",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents", "{{intentId}}", "cancel"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('status is cancelled', () => pm.expect(body.status).to.eql('cancelled'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 cancelled",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/cancel" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"pi_11111111-1111-1111-1111-111111111111\", \"status\": \"cancelled\" }"
            },
            {
              "name": "409 cannot cancel",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/cancel" } },
              "status": "Conflict",
              "code": 409,
              "body": "{ \"error\": \"cannot cancel intent in status settled\" }"
            }
          ]
        },
        {
          "name": "Routing candidates",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/routing",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents", "{{intentId}}", "routing"]
            },
            "description": "Audit trail — every quote the router considered, which one was chosen, which one the AI picked, and the estimated savings."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/routing" } },
              "status": "OK",
              "code": 200,
              "body": "{\n  \"data\": [\n    { \"id\": \"qc_1\", \"provider\": \"lifi\", \"chosen\": true, \"ai_pick\": true, \"score\": 0.94, \"from_chain\": \"base\", \"to_chain\": \"solana\", \"from_token\": \"USDC\", \"to_token\": \"USDC\", \"to_amount_usd\": \"99.10\", \"fee_usd\": \"0.14\", \"gas_usd\": \"0.06\", \"duration_sec\": 42, \"reason\": \"best net after fees\" },\n    { \"id\": \"qc_2\", \"provider\": \"jupiter-direct\", \"chosen\": false, \"ai_pick\": false, \"score\": 0.87, \"from_token\": \"SOL\", \"to_token\": \"USDC\", \"to_amount_usd\": \"98.72\", \"fee_usd\": \"0.19\", \"gas_usd\": \"0.00\", \"duration_sec\": 12 }\n  ]\n}"
            }
          ]
        },
        {
          "name": "Intent transactions",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/transactions",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payment-intents", "{{intentId}}", "transactions"]
            },
            "description": "On-chain legs (swaps, bridges, settlement transfer) for this intent, in execution order."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payment-intents/{{intentId}}/transactions" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"data\": [ { \"id\": \"tx_1\", \"kind\": \"swap\", \"chain\": \"base\", \"tx_hash\": \"0xabc…\", \"amount_usd\": \"99.10\", \"status\": \"confirmed\", \"created_at\": \"2026-07-18T18:33:00Z\" } ] }"
            }
          ]
        }
      ]
    },
    {
      "name": "Refunds",
      "description": "Issue partial or full refunds against a settled intent. Balance-capped — creating a refund that would exceed the settled amount returns 422.",
      "item": [
        {
          "name": "Create Refund",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Idempotency-Key", "value": "{{$guid}}" }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/refunds",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "refunds"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"intent_id\": \"{{intentId}}\",\n  \"amount_usd\": 25.00,\n  \"destination\": \"CUSTOMER_WALLET_ADDRESS\",\n  \"reason\": \"customer_request\"\n}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 201', () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test('has refund id', () => pm.expect(body.id).to.be.a('string'));",
                  "pm.test('status pending', () => pm.expect(body.status).to.eql('pending'));",
                  "if (body.id) pm.collectionVariables.set('refundId', body.id);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "201 Created",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/refunds" } },
              "status": "Created",
              "code": 201,
              "body": "{\n  \"id\": \"rf_22222222-2222-2222-2222-222222222222\",\n  \"payment_intent_id\": \"pi_11111111-1111-1111-1111-111111111111\",\n  \"amount_usd\": \"25.00\",\n  \"chain\": \"solana\",\n  \"token\": \"USDC\",\n  \"destination\": \"CUSTOMER_WALLET_ADDRESS\",\n  \"reason\": \"customer_request\",\n  \"status\": \"pending\",\n  \"created_at\": \"2026-07-18T18:40:00Z\"\n}"
            },
            {
              "name": "422 exceeds refundable balance",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/refunds" } },
              "status": "Unprocessable Entity",
              "code": 422,
              "body": "{ \"error\": \"refund exceeds refundable balance\", \"remaining\": 12.5 }"
            },
            {
              "name": "409 intent not refundable",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/refunds" } },
              "status": "Conflict",
              "code": 409,
              "body": "{ \"error\": \"cannot refund intent in status=executing\" }"
            }
          ]
        },
        {
          "name": "List Refunds",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/refunds?limit=50&payment_intent_id={{intentId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "refunds"],
              "query": [
                { "key": "limit", "value": "50" },
                { "key": "payment_intent_id", "value": "{{intentId}}", "description": "Optional — filter to one intent" }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/refunds" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"data\": [ { \"id\": \"rf_22222222-2222-2222-2222-222222222222\", \"amount_usd\": \"25.00\", \"status\": \"paid\", \"tx_hash\": \"5s4…\", \"paid_at\": \"2026-07-18T18:42:00Z\" } ] }"
            }
          ]
        },
        {
          "name": "Retrieve Refund",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/refunds/{{refundId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "refunds", "{{refundId}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('id matches', () => pm.expect(body.id).to.eql(pm.collectionVariables.get('refundId')));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/refunds/{{refundId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"rf_22222222-2222-2222-2222-222222222222\", \"payment_intent_id\": \"pi_11111111-1111-1111-1111-111111111111\", \"amount_usd\": \"25.00\", \"chain\": \"solana\", \"token\": \"USDC\", \"destination\": \"CUSTOMER_WALLET_ADDRESS\", \"reason\": \"customer_request\", \"status\": \"paid\", \"tx_hash\": \"5s4…\", \"paid_at\": \"2026-07-18T18:42:00Z\", \"failure_reason\": null }"
            }
          ]
        }
      ]
    },
    {
      "name": "Payouts",
      "description": "Merchant off-ramp / settlement rail. Payouts are enqueued automatically when an intent settles. Use these endpoints to audit status or replay a failed dispatch.",
      "item": [
        {
          "name": "List Payouts",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payouts?limit=50&status=paid",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payouts"],
              "query": [
                { "key": "limit", "value": "50" },
                { "key": "status", "value": "paid", "description": "pending|dispatched|paid|failed|cancelled" },
                { "key": "payment_intent_id", "value": "", "disabled": true }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));",
                  "if (Array.isArray(body.data) && body.data.length) {",
                  "  pm.collectionVariables.set('payoutId', body.data[0].id);",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payouts" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"data\": [ { \"id\": \"po_33333333-3333-3333-3333-333333333333\", \"payment_intent_id\": \"pi_11111111-1111-1111-1111-111111111111\", \"rail\": \"self_custody\", \"chain\": \"solana\", \"token\": \"USDC\", \"amount\": \"99.00\", \"currency\": \"USD\", \"status\": \"paid\", \"tx_hash\": \"5s4…\", \"attempts\": 1, \"paid_at\": \"2026-07-18T18:44:00Z\", \"created_at\": \"2026-07-18T18:34:00Z\" } ] }"
            }
          ]
        },
        {
          "name": "Retrieve Payout",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payouts/{{payoutId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payouts", "{{payoutId}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('id matches', () => pm.expect(body.id).to.eql(pm.collectionVariables.get('payoutId')));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/payouts/{{payoutId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"po_33333333-3333-3333-3333-333333333333\", \"status\": \"paid\", \"tx_hash\": \"5s4…\", \"attempts\": 1, \"failure_reason\": null }"
            }
          ]
        },
        {
          "name": "Replay Payout dispatch",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/payouts/{{payoutId}}/replay",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "payouts", "{{payoutId}}", "replay"]
            },
            "description": "Manually re-run the payout dispatcher. Only useful for `failed` or stuck `pending` payouts — the engine already retries with exponential backoff."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('replayed flag', () => pm.expect(body.replayed).to.eql(true));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 replayed",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/payouts/{{payoutId}}/replay" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"replayed\": true, \"result\": { \"status\": \"paid\", \"tx_hash\": \"5s4…\" } }"
            }
          ]
        }
      ]
    },
    {
      "name": "Webhook Endpoints",
      "description": "Configure the URLs Frag POSTs signed events to. The `signing_secret` returned on Create / Rotate is shown exactly once — store it immediately.",
      "item": [
        {
          "name": "Create Webhook Endpoint",
          "request": {
            "method": "POST",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://your.app/api/frag/webhook\",\n  \"events\": [\n    \"payment_intent.settled\",\n    \"payment_intent.failed\",\n    \"payment_intent.refunded\",\n    \"payout.paid\",\n    \"payout.failed\"\n  ],\n  \"enabled\": true\n}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 201', () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test('has id + secret', () => {",
                  "  pm.expect(body.id).to.be.a('string');",
                  "  pm.expect(body.signing_secret).to.match(/^whsec_/);",
                  "});",
                  "if (body.id) pm.collectionVariables.set('webhookEndpointId', body.id);",
                  "if (body.signing_secret) pm.collectionVariables.set('webhookSigningSecret', body.signing_secret);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "201 Created",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints" } },
              "status": "Created",
              "code": 201,
              "body": "{ \"id\": \"we_44444444-4444-4444-4444-444444444444\", \"url\": \"https://your.app/api/frag/webhook\", \"events\": [\"payment_intent.settled\"], \"enabled\": true, \"signing_secret\": \"whsec_0123456789abcdef0123456789abcdef0123456789abcdef\", \"created_at\": \"2026-07-18T18:22:00Z\" }"
            }
          ]
        },
        {
          "name": "List Webhook Endpoints",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "pm.test('data is array', () => pm.expect(pm.response.json().data).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"data\": [ { \"id\": \"we_44444444-4444-4444-4444-444444444444\", \"url\": \"https://your.app/api/frag/webhook\", \"events\": [\"payment_intent.settled\"], \"enabled\": true, \"created_at\": \"2026-07-18T18:22:00Z\" } ] }"
            }
          ]
        },
        {
          "name": "Retrieve Webhook Endpoint",
          "request": {
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints", "{{webhookEndpointId}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "pm.test('id matches', () => pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('webhookEndpointId')));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"we_44444444-4444-4444-4444-444444444444\", \"url\": \"https://your.app/api/frag/webhook\", \"events\": [\"payment_intent.settled\"], \"enabled\": true, \"created_at\": \"2026-07-18T18:22:00Z\" }"
            }
          ]
        },
        {
          "name": "Update Webhook Endpoint",
          "request": {
            "method": "PATCH",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints", "{{webhookEndpointId}}"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"events\": [\"payment_intent.settled\", \"payment_intent.failed\", \"payout.paid\"],\n  \"enabled\": true\n}"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('events updated', () => pm.expect(body.events).to.be.an('array'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "PATCH", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"we_44444444-4444-4444-4444-444444444444\", \"url\": \"https://your.app/api/frag/webhook\", \"events\": [\"payment_intent.settled\", \"payment_intent.failed\", \"payout.paid\"], \"enabled\": true, \"created_at\": \"2026-07-18T18:22:00Z\" }"
            }
          ]
        },
        {
          "name": "Rotate Signing Secret",
          "request": {
            "method": "POST",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}/rotate-secret",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints", "{{webhookEndpointId}}", "rotate-secret"]
            },
            "description": "Rotates the endpoint's signing secret. The new plaintext secret is returned exactly once — store it, then update your verifier."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('rotated secret returned', () => pm.expect(body.signing_secret).to.match(/^whsec_/));",
                  "if (body.signing_secret) pm.collectionVariables.set('webhookSigningSecret', body.signing_secret);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "POST", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}/rotate-secret" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"we_44444444-4444-4444-4444-444444444444\", \"url\": \"https://your.app/api/frag/webhook\", \"events\": [\"payment_intent.settled\"], \"enabled\": true, \"signing_secret\": \"whsec_new0secretHexvalue0here0000000000000000000000000000\", \"created_at\": \"2026-07-18T18:22:00Z\" }"
            }
          ]
        },
        {
          "name": "Delete Webhook Endpoint",
          "request": {
            "method": "DELETE",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "webhook-endpoints", "{{webhookEndpointId}}"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "pm.test('deleted flag', () => pm.expect(pm.response.json().deleted).to.eql(true));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "DELETE", "url": { "raw": "{{baseUrl}}/api/public/v1/webhook-endpoints/{{webhookEndpointId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"deleted\": true }"
            }
          ]
        }
      ]
    },
    {
      "name": "Public / Unauthenticated",
      "description": "Endpoints that don't require a secret key. Safe to call from browsers.",
      "item": [
        {
          "name": "Public Checkout state (poll)",
          "request": {
            "auth": { "type": "noauth" },
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/checkout/{{intentId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "checkout", "{{intentId}}"]
            },
            "description": "Used by the browser SDK to poll intent status. Returns only safe fields — no merchant PII, no keys."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('has status field', () => pm.expect(body.status).to.be.a('string'));",
                  "pm.test('no sensitive fields', () => {",
                  "  pm.expect(body).to.not.have.property('merchant_id');",
                  "  pm.expect(body).to.not.have.property('secret_hash');",
                  "});"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/checkout/{{intentId}}" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"id\": \"pi_11111111-1111-1111-1111-111111111111\", \"amount\": \"99.00\", \"currency\": \"USD\", \"status\": \"executing\", \"settlement_chain\": \"solana\", \"settlement_token\": \"USDC\", \"expires_at\": \"2026-07-18T18:52:00Z\", \"metadata\": { \"orderId\": \"order_8841\" } }"
            }
          ]
        },
        {
          "name": "Supported Tokens",
          "request": {
            "auth": { "type": "noauth" },
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/v1/tokens?chain=solana",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "v1", "tokens"],
              "query": [{ "key": "chain", "value": "solana", "description": "Optional — filter to a single chain" }]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('data is array', () => pm.expect(body.data).to.be.an('array'));",
                  "if (body.data.length) {",
                  "  pm.test('tokens are unblocked', () => pm.expect(body.data.every(t => t.blocked === false)).to.eql(true));",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/v1/tokens?chain=solana" } },
              "status": "OK",
              "code": 200,
              "body": "{ \"data\": [ { \"chain\": \"solana\", \"address\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\", \"symbol\": \"USDC\", \"name\": \"USD Coin\", \"decimals\": 6, \"verified\": true, \"min_liquidity_usd\": 1000000, \"blocked\": false } ] }"
            }
          ]
        },
        {
          "name": "Health Check",
          "request": {
            "auth": { "type": "noauth" },
            "method": "GET",
            "url": {
              "raw": "{{baseUrl}}/api/public/health",
              "host": ["{{baseUrl}}"],
              "path": ["api", "public", "health"]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status 200 or 503', () => pm.expect([200, 503]).to.include(pm.response.code));",
                  "const body = pm.response.json();",
                  "pm.test('service is frag', () => pm.expect(body.service).to.eql('frag'));",
                  "pm.test('reports checks', () => pm.expect(body.checks).to.be.an('object'));"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "200 OK",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/health" } },
              "status": "OK",
              "code": 200,
              "body": "{\n  \"status\": \"ok\",\n  \"service\": \"frag\",\n  \"version\": \"2026.07.18\",\n  \"time\": \"2026-07-18T18:22:00Z\",\n  \"latency_ms\": 12,\n  \"checks\": {\n    \"env\": { \"supabase_url\": true, \"supabase_publishable\": true, \"cron_secret\": true },\n    \"database\": \"ok\"\n  }\n}"
            },
            {
              "name": "503 database down",
              "originalRequest": { "method": "GET", "url": { "raw": "{{baseUrl}}/api/public/health" } },
              "status": "Service Unavailable",
              "code": 503,
              "body": "{ \"status\": \"down\", \"service\": \"frag\", \"checks\": { \"database\": \"down\" } }"
            }
          ]
        }
      ]
    }
  ]
}
