{
  "info": {
    "_postman_id": "8c1f0a52-3d9e-4b7a-9c61-2f4e5d6a7b80",
    "name": "Caravel Pay API",
    "description": "Caravel Pay creates hosted card, Apple Pay and Google Pay checkouts and pays out to your Polygon wallet, minus a flat 2%. There are no accounts or dashboards: \"Connect\" returns a merchant id and secret, and every other request is signed with them.\n\n1. Set `site_url` (your HTTPS site) and `payout_address` (your Polygon wallet) in the collection variables.\n2. Run **Connect: get your API keys** once. Its test script stores `merchant_id` and `secret` in the collection variables. Every run creates a NEW merchant.\n3. Every other request is signed by the collection pre-request script: `Caravel-Signature` = lowercase hex HMAC-SHA256(secret, `timestamp\\nMETHOD\\npath+query\\nrawBody`).\n\nKeep the secret out of shared workspaces: store it as a secret-type or current-value-only variable. Docs: https://caravelpay.app/docs/ · Reference for AI coding agents: caravel-pay-api-llm.md",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Caravel Pay request signing (docs: Authentication and request signing).",
          "// signature = lowercase hex HMAC-SHA256(secret as ASCII, timestamp + \"\\n\" + METHOD + \"\\n\" + path + \"\\n\" + rawBody)",
          "// path = the request path plus its query string, if any (never the full URL); rawBody = \"\" for GET.",
          "(function () {",
          "    var resolved = pm.variables.replaceIn(pm.request.url.toString());",
          "    var match = /^[a-z][a-z0-9+.-]*:\\/\\/[^\\/?#]+([^#]*)/i.exec(resolved);",
          "    var path = match ? match[1] : resolved;",
          "    if (path === '' || path.charAt(0) === '?') { path = '/' + path; }",
          "    path = path.replace(/\\?$/, '');",
          "    // Unsigned: POST /v1/connect, POST /v1/reports, and requests marked with a Caravel-Unsigned header",
          "    // (removed here, never sent), such as creating a payment link without API keys.",
          "    if (pm.request.headers.has('Caravel-Unsigned')) {",
          "        pm.request.headers.remove('Caravel-Unsigned');",
          "        return;",
          "    }",
          "    if (/^\\/v1\\/(connect|reports)(\\?|$)/.test(path)) {",
          "        return;",
          "    }",
          "    var merchant = pm.variables.get('merchant_id');",
          "    var secret = pm.variables.get('secret');",
          "    if (!merchant || !secret) {",
          "        throw new Error('Set the merchant_id and secret collection variables first (run \"Connect: get your API keys\").');",
          "    }",
          "    var method = pm.request.method.toUpperCase();",
          "    var body = '';",
          "    if (method !== 'GET' && pm.request.body && pm.request.body.mode === 'raw') {",
          "        body = pm.variables.replaceIn(pm.request.body.raw || '');",
          "    }",
          "    var timestamp = String(Math.floor(Date.now() / 1000));",
          "    var signature = CryptoJS.HmacSHA256(timestamp + '\\n' + method + '\\n' + path + '\\n' + body, secret).toString(CryptoJS.enc.Hex);",
          "    pm.request.headers.upsert({ key: 'Caravel-Merchant', value: merchant });",
          "    pm.request.headers.upsert({ key: 'Caravel-Timestamp', value: timestamp });",
          "    pm.request.headers.upsert({ key: 'Caravel-Signature', value: signature });",
          "})();"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.caravelpay.app",
      "type": "string",
      "description": "API origin. Production placeholder host until the final domain is announced."
    },
    {
      "key": "site_url",
      "value": "https://shop.example",
      "type": "string",
      "description": "Your HTTPS site. Return, cancel and callback URLs must be on it (and its www. twin)."
    },
    {
      "key": "payout_address",
      "value": "",
      "type": "string",
      "description": "Your Polygon wallet (0x + 40 hex). Every payout is sent here."
    },
    {
      "key": "merchant_id",
      "value": "",
      "type": "string",
      "description": "Set by Connect."
    },
    {
      "key": "secret",
      "value": "",
      "type": "secret",
      "description": "Set by Connect. Shown only once by the API: keep it private."
    },
    {
      "key": "order_id",
      "value": "test-1",
      "type": "string",
      "description": "Your order reference for Create a checkout; unique per merchant."
    },
    {
      "key": "checkout_id",
      "value": "",
      "type": "string",
      "description": "Set by Create a checkout."
    },
    {
      "key": "next_cursor",
      "value": "",
      "type": "string",
      "description": "Set by List checkouts."
    },
    {
      "key": "webhook_url",
      "value": "https://shop.example/webhooks/caravel",
      "type": "string",
      "description": "Your webhook endpoint, for the signed test webhook."
    },
    {
      "key": "link_id",
      "value": "",
      "type": "string",
      "description": "Set by Create a payment link (signed)."
    }
  ],
  "item": [
    {
      "name": "Connect: get your API keys",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "    var data = pm.response.json();",
              "    if (data.merchant_id !== undefined && data.merchant_id !== null) { pm.collectionVariables.set('merchant_id', data.merchant_id); }",
              "    if (data.secret !== undefined && data.secret !== null) { pm.collectionVariables.set('secret', data.secret); }",
              "}"
            ]
          }
        }
      ],
      "request": {
        "auth": {
          "type": "noauth"
        },
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"site_url\": \"{{site_url}}\",\n  \"payout_address\": \"{{payout_address}}\",\n  \"platform\": \"api\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/v1/connect",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "connect"
          ]
        },
        "description": "Unsigned. Creates a NEW merchant every time and returns its id and secret once. To change the wallet or rotate the secret, connect again and keep the old credentials until their open checkouts have finished. Limited to 10 per hour per IP."
      },
      "response": [
        {
          "name": "201 Connected",
          "status": "Created",
          "code": 201,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"merchant_id\": \"mch_7Q2KZX4PRB\",\n  \"secret\": \"9b2e7c4a1f6d8e3b5a0c9d2f7e4b1a6c3d8f5e2b9a7c4d1e6f3b8a5c2d9e7f4a\",\n  \"status\": \"active\",\n  \"fee_rate\": \"2%\"\n}"
        },
        {
          "name": "409 Wallet limit",
          "status": "Conflict",
          "code": 409,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"error\": {\n    \"code\": \"limit_exceeded\",\n    \"message\": \"This payout wallet can't be connected to more merchants.\",\n    \"param\": \"payout_address\"\n  }\n}"
        }
      ]
    },
    {
      "name": "Retrieve your merchant",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{base_url}}/v1/merchant",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "merchant"
          ]
        },
        "description": "The quickest way to test your credentials."
      },
      "response": [
        {
          "name": "200 Merchant",
          "status": "OK",
          "code": 200,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"id\": \"mch_7Q2KZX4PRB\",\n  \"site_url\": \"https://shop.example\",\n  \"origins\": [\n    \"https://shop.example\",\n    \"https://www.shop.example\"\n  ],\n  \"payout_address\": \"0x52908400098527886E0F7030069857D2E4169EE7\",\n  \"status\": \"active\",\n  \"fee_rate\": \"2%\"\n}"
        },
        {
          "name": "401 Bad signature",
          "status": "Unauthorized",
          "code": 401,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"error\": {\n    \"code\": \"invalid_signature\",\n    \"message\": \"Caravel-Signature doesn't match. Sign the exact path and body you send.\",\n    \"param\": \"Caravel-Signature\"\n  }\n}"
        }
      ]
    },
    {
      "name": "Create a checkout",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "    var data = pm.response.json();",
              "    if (data.id !== undefined && data.id !== null) { pm.collectionVariables.set('checkout_id', data.id); }",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"order_id\": \"{{order_id}}\",\n  \"amount\": \"136.41\",\n  \"currency\": \"USD\",\n  \"customer_email\": \"buyer@example.com\",\n  \"return_url\": \"{{site_url}}/thanks\",\n  \"cancel_url\": \"{{site_url}}/cart\",\n  \"callback_url\": \"{{site_url}}/webhooks/caravel\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/v1/checkouts",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "checkouts"
          ]
        },
        "description": "amount is a string with exactly two decimals; currency is \"USD\". return_url, cancel_url and callback_url must be HTTPS on your merchant origins. The same order_id with the same details returns the same checkout (200); different details give 409 order_id_conflict. Checkouts expire after 60 minutes: retry with a new order_id such as 1042-2."
      },
      "response": [
        {
          "name": "201 Created",
          "status": "Created",
          "code": 201,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"id\": \"chk_8Qe2TnV4kR\",\n  \"checkout_url\": \"https://pay.caravelpay.app/c/chk_8Qe2TnV4kR\",\n  \"status\": \"awaiting_payment\",\n  \"amount\": \"136.41\",\n  \"currency\": \"USD\",\n  \"expires_at\": \"2026-11-09T15:04:12Z\"\n}"
        },
        {
          "name": "409 order_id used with other details",
          "status": "Conflict",
          "code": 409,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"error\": {\n    \"code\": \"order_id_conflict\",\n    \"message\": \"This order_id already belongs to a checkout with different details. Use a new order_id.\",\n    \"param\": \"order_id\"\n  }\n}"
        },
        {
          "name": "400 Bad amount",
          "status": "Bad Request",
          "code": 400,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"error\": {\n    \"code\": \"invalid_amount\",\n    \"message\": \"amount must be a string with two decimals, like \\\"136.41\\\"\",\n    \"param\": \"amount\"\n  }\n}"
        }
      ]
    },
    {
      "name": "Retrieve a checkout",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{base_url}}/v1/checkouts/{{checkout_id}}",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "checkouts",
            "{{checkout_id}}"
          ]
        },
        "description": "The current status, paid_amount and, once paid, the payout. Decide whether an order is paid from this response, not from a webhook body."
      },
      "response": [
        {
          "name": "200 Paid",
          "status": "OK",
          "code": 200,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"id\": \"chk_8Qe2TnV4kR\",\n  \"checkout_url\": \"https://pay.caravelpay.app/c/chk_8Qe2TnV4kR\",\n  \"status\": \"paid\",\n  \"amount\": \"136.41\",\n  \"currency\": \"USD\",\n  \"expires_at\": \"2026-11-09T15:04:12Z\",\n  \"order_id\": \"1042\",\n  \"created_at\": \"2026-11-09T14:04:12Z\",\n  \"paid_amount\": \"136.41\",\n  \"payout\": {\n    \"amount\": \"133.68\",\n    \"asset\": \"USDC\",\n    \"tx_hash\": \"0x5d1e8c0b7a4f2e19c6d3b8a0f5e7c2d1a9b4e6f8c0d2a7b5e3f1c9d8a6b4a07b\"\n  }\n}"
        },
        {
          "name": "404 Not yours",
          "status": "Not Found",
          "code": 404,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"error\": {\n    \"code\": \"not_found\",\n    \"message\": \"No checkout with that id belongs to this merchant.\",\n    \"param\": \"id\"\n  }\n}"
        }
      ]
    },
    {
      "name": "List checkouts",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (pm.response.code >= 200 && pm.response.code < 300) {",
              "    var data = pm.response.json();",
              "    if (data.next_cursor !== undefined && data.next_cursor !== null) { pm.collectionVariables.set('next_cursor', data.next_cursor); }",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{base_url}}/v1/checkouts?limit=20",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "checkouts"
          ],
          "query": [
            {
              "key": "limit",
              "value": "20"
            }
          ]
        },
        "description": "Newest first. The query string is part of the signed path."
      },
      "response": [
        {
          "name": "200 Page",
          "status": "OK",
          "code": 200,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"data\": [\n    {\n      \"id\": \"chk_8Qe2TnV4kR\",\n      \"checkout_url\": \"https://pay.caravelpay.app/c/chk_8Qe2TnV4kR\",\n      \"status\": \"paid\",\n      \"amount\": \"136.41\",\n      \"currency\": \"USD\",\n      \"expires_at\": \"2026-11-09T15:04:12Z\",\n      \"order_id\": \"1042\",\n      \"created_at\": \"2026-11-09T14:04:12Z\",\n      \"paid_amount\": \"136.41\",\n      \"payout\": {\n        \"amount\": \"133.68\",\n        \"asset\": \"USDC\",\n        \"tx_hash\": \"0x5d1e8c0b7a4f2e19c6d3b8a0f5e7c2d1a9b4e6f8c0d2a7b5e3f1c9d8a6b4a07b\"\n      }\n    }\n  ],\n  \"next_cursor\": null\n}"
        }
      ]
    },
    {
      "name": "List checkouts: next page",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{base_url}}/v1/checkouts?limit=20&cursor={{next_cursor}}",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "checkouts"
          ],
          "query": [
            {
              "key": "limit",
              "value": "20"
            },
            {
              "key": "cursor",
              "value": "{{next_cursor}}"
            }
          ]
        },
        "description": "Pass next_cursor from the previous page as cursor; it is null on the last page."
      },
      "response": []
    },
    {
      "name": "Payment links",
      "item": [
        {
          "name": "Create a payment link without API keys",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Caravel-Unsigned",
                "value": "1",
                "description": "Tells the collection script not to sign this request; removed before sending."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"payout_address\": \"{{payout_address}}\",\n  \"amount\": \"25.00\",\n  \"currency\": \"USD\",\n  \"description\": \"Logo design, 2 rounds\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/v1/payment-links",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "payment-links"
              ]
            },
            "description": "No signature: send payout_address. The answer is only id and url; no credentials are returned and these links send no webhooks. amount null lets the buyer enter it (min_amount/max_amount). 30 links an hour per address."
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "_postman_previewlanguage": "json",
              "body": "{\n  \"id\": \"lnk_K7Q2ZX4PRBM5TW3NHJ6D\",\n  \"url\": \"https://pay.caravelpay.app/l/lnk_K7Q2ZX4PRBM5TW3NHJ6D\"\n}"
            }
          ]
        },
        {
          "name": "Create a payment link (signed)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    var data = pm.response.json();",
                  "    if (data.id !== undefined && data.id !== null) { pm.collectionVariables.set('link_id', data.id); }",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": \"49.00\",\n  \"currency\": \"USD\",\n  \"description\": \"Workshop seat\",\n  \"reusable\": false,\n  \"callback_url\": \"{{site_url}}/webhooks/caravel\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/v1/payment-links",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "payment-links"
              ]
            },
            "description": "Signed: the link belongs to your merchant and pays out to its wallet; its checkouts appear in GET /v1/checkouts and send webhooks to callback_url. Don't send payout_address."
          },
          "response": []
        },
        {
          "name": "Retrieve a payment link",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/payment-links/{{link_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{link_id}}"
              ]
            },
            "description": "Signed. Status (active, completed, deactivated) and how many of its checkouts are paid."
          },
          "response": []
        },
        {
          "name": "Deactivate a payment link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/v1/payment-links/{{link_id}}/deactivate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{link_id}}",
                "deactivate"
              ]
            },
            "description": "Signed. New checkouts stop at once; open ones can still be paid. Permanent."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Report a seller",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"url\": \"https://shop.example/product/123\",\n  \"category\": \"scam_fraud\",\n  \"details\": \"Describe what is wrong. No card numbers or passwords.\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/v1/reports",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "v1",
            "reports"
          ]
        },
        "description": "Unsigned. Reports a site or checkout that sells something illegal, defrauds buyers or impersonates someone. category: illegal_goods, scam_fraud, impersonation or other. 5 reports an hour per address."
      },
      "response": [
        {
          "name": "202 Received",
          "status": "Accepted",
          "code": 202,
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "_postman_previewlanguage": "json",
          "body": "{\n  \"id\": \"rpt_K7Q2ZX4PRBM3N5T6\"\n}"
        }
      ]
    },
    {
      "name": "Send a signed test webhook to your endpoint",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Accept",
            "value": "application/json"
          },
          {
            "key": "Caravel-Event",
            "value": "evt_TEST{{$timestamp}}"
          },
          {
            "key": "User-Agent",
            "value": "CaravelPay-Webhooks/1"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"id\": \"evt_TEST{{$timestamp}}\",\n  \"type\": \"checkout.paid\",\n  \"created\": \"2026-11-09T14:09:37Z\",\n  \"data\": {\n    \"id\": \"chk_8Qe2TnV4kR\",\n    \"checkout_url\": \"https://pay.caravelpay.app/c/chk_8Qe2TnV4kR\",\n    \"status\": \"paid\",\n    \"amount\": \"136.41\",\n    \"currency\": \"USD\",\n    \"expires_at\": \"2026-11-09T15:04:12Z\",\n    \"order_id\": \"1042\",\n    \"created_at\": \"2026-11-09T14:04:12Z\",\n    \"paid_amount\": \"136.41\",\n    \"payout\": {\n      \"amount\": \"133.68\",\n      \"asset\": \"USDC\",\n      \"tx_hash\": \"0x5d1e8c0b7a4f2e19c6d3b8a0f5e7c2d1a9b4e6f8c0d2a7b5e3f1c9d8a6b4a07b\"\n    }\n  }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{webhook_url}}"
        },
        "description": "Posts a checkout.paid event to your own webhook_url, signed with your secret over the path of webhook_url, the way Caravel Pay does. Use it to test your verification code. A correct handler re-fetches the checkout with GET /v1/checkouts/{id} and, because this checkout id is only an example, must NOT mark anything paid."
      },
      "response": []
    }
  ]
}
