# CashTap Checkout API > Accept USD payments programmatically from your site or app through a > hosted, branded checkout page at pay.cashtap.cash. Stripe-Checkout > shape: your backend creates a Checkout Session, gets a hosted URL, > redirects the buyer there. CashTap handles payment collection, fee > deduction, and settlement to your CashTap wallet. This file is published at https://cashtap.cash/llms.txt and is intended for AI coding assistants (Claude, ChatGPT, Copilot, Cursor, etc.) to ingest as authoritative reference when writing integrations. Human-facing docs are at https://cashtap.cash/developers/docs. ## Quick facts - **Base URL:** `https://api.cashtap.cash/checkout/v1` - **Auth:** Bearer secret key. Header: `Authorization: Bearer sk_live_…` - **Format:** JSON request and response bodies - **Currency:** USD (settled on-chain as USDC; never appears in the API) - **Test mode:** none — live keys only. Use small amounts during integration. - **Ports of call:** - Merchant dashboard: `https://cashtap.cash/developers/api-settings` - Webhooks dashboard: `https://cashtap.cash/developers/webhooks` - Hosted checkout: `https://pay.cashtap.cash/c/{session_id}` - Documentation: `https://cashtap.cash/developers/docs` ## Authentication Send the secret key as a Bearer token. Keys are 32 random bytes, base64url-encoded, prefixed with `sk_live_`. Example: curl https://api.cashtap.cash/checkout/v1/sessions \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ ... }' Keys are hashed at rest. The raw key is shown to the merchant exactly once at creation time and is never recoverable. To rotate: mint a new key, deploy, then revoke the old. Both work simultaneously during cut-over. ## Error response envelope Every error uses this shape: { "error": { "code": "invalid_request", "message": "amount must be at least 0.50 USD.", "request_id": "req_2026053027abc" } } HTTP status mapping: | Status | Code | Meaning | |--------|-------------------------|----------------------------------------------------------------------------| | 400 | invalid_request | Missing or invalid parameters | | 401 | authentication_required | Missing or malformed Authorization header | | 403 | permission_denied | Key revoked, or redirect domain not on allowlist | | 404 | not_found | No session / merchant with that id | | 422 | unprocessable_entity | Semantically invalid (e.g. amount below minimum) | | 429 | rate_limited | Too many requests. Back off and retry. | | 503 | provider_unavailable | A downstream payment provider is degraded | | 500 | api_error | Unexpected server error. Include `request_id` when contacting support. | Every response also carries `X-Request-Id` for trace lookup. ## Endpoint: Create Checkout Session `POST /checkout/v1/sessions` Request body: { "amount": 50.00, "line_items": [ { "name": "Premium Plan", "description": "Monthly subscription", "quantity": 1, "unit_amount": 50.00 } ], "customer_email": "buyer@acme.com", "payment_methods": ["wallet", "cashapp"], "success_url": "https://acme.com/thanks?order=12345", "cancel_url": "https://acme.com/cart", "metadata": { "order_id": "12345" } } Parameters: - `amount` (number, required) — Total in USD. Min 0.50, max 100,000. - `line_items` (array, optional) — Displayed to the buyer on the checkout page. Each item: `{ name, description?, quantity, unit_amount }`. - `customer_email` (string, optional) — Pre-fills email on the page. - `payment_methods` (array, optional) — Subset of your merchant defaults. Valid ids: `coinbase`, `wallet`, `cashapp`, `peer`, `lightning-btc`, `card-bank`. Omit to use your merchant defaults. - `success_url` (string, required) — Where to send the buyer on successful payment. Domain must be on your merchant allowlist. - `cancel_url` (string, required) — Where to send the buyer on cancel or expiration. - `metadata` (object, optional) — Up to 20 string key/value pairs. Echoed back in webhooks and the retrieve endpoint. Response (201 Created): { "id": "cs_live_abc123…", "url": "https://pay.cashtap.cash/c/cs_live_abc123…", "status": "pending", "amount": 50.00, "expires_at": 1717104000, "created_at": 1717102200 } Sessions expire 60 minutes after creation. JavaScript example: const res = await fetch("https://api.cashtap.cash/checkout/v1/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.CASHTAP_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 50.0, line_items: [{ name: "Premium Plan", quantity: 1, unit_amount: 50.0 }], payment_methods: ["wallet", "cashapp"], success_url: "https://acme.com/thanks?order=12345", cancel_url: "https://acme.com/cart", metadata: { order_id: "12345" }, }), }); const session = await res.json(); // Redirect buyer: res.redirect(303, session.url); ## Endpoint: Retrieve Session Status `GET /checkout/v1/sessions/{session_id}` Prefer webhooks; poll only as a fallback. If you must poll, do it at most once every 5 seconds while the session is `pending` or `processing`, and stop at a terminal state (`completed`, `expired`, `failed`). Example (curl): curl https://api.cashtap.cash/checkout/v1/sessions/cs_live_abc123 \ -H "Authorization: Bearer sk_live_YOUR_KEY" Response (when status === "completed"): { "id": "cs_live_abc123…", "status": "completed", "amount": 50.00, "amount_received": 50.00, "fee_amount": 1.75, "net_amount": 48.25, "payment_method": "wallet", "customer_email": "buyer@acme.com", "paid_at": 1717102560, "incoming_tx_hash": "0x…", "settlement_tx_hash": "0x…", "metadata": { "order_id": "12345" } } The full response also carries `url`, `line_items`, `payment_methods`, `success_url`, `cancel_url`, `fee_sweep_tx_hash`, `expires_at` and `created_at`. Settlement fields (`amount_received`, `fee_amount`, `net_amount`, `settlement_tx_hash`, `fee_sweep_tx_hash`) are `null` until the session is `completed`. `amount_received` is what the buyer's payment actually delivered, before the platform fee. Compare it with `amount` before fulfilling: bridge and on-ramp fees can deliver slightly less (normally within 5%), and support may settle a larger underpayment after contacting the merchant. Session lifecycle states: - `pending` — Session created. Waiting for buyer to pay. - `processing` — Buyer's payment detected. Settling on-chain. - `completed` — Funds settled. `checkout.session.completed` webhook sent. - `expired` — 60 minutes passed without a confirmed payment. - `failed` — A payment arrived but settlement failed on CashTap's side. CashTap support resolves these. Do not fulfill. ## Payment methods | ID | Method | Best for | |-----------------|-----------------------|-----------------------------------------------------------| | `wallet` | Wallet Transfer | Crypto-native buyers paying from any wallet | | `cashapp` | CashApp Pay | US buyers, mobile-first via Lightning BTC | | `coinbase` | Coinbase Onramp | Buyers funding with USD card/bank | | `peer` | Peer Extension | Buyers with the CashTap browser extension | | `lightning-btc` | Lightning BTC | Sats payments, fastest for small amounts ≥ $35 | | `card-bank` | Card / Bank | Buyers paying with traditional rails | The merchant has a default list configured in the dashboard. Sessions can narrow that list further via `payment_methods` on session creation. ## Webhooks CashTap sends a signed HTTPS POST to the merchant's server when a checkout session reaches a final state. Webhooks replace polling. ### Configuration Endpoints are managed in the dashboard at `https://cashtap.cash/developers/webhooks` (there is no REST API for managing them). - Up to 5 endpoints per merchant. Each endpoint has its own URL, its own signing secret and its own event subscriptions (all events or a subset). - The URL must be `https://` on port 443 or 8443. Private, loopback and internal addresses are blocked; use a tunnel for local development. - The signing secret looks like `whsec_…` (49 characters). It is shown exactly once, when the endpoint is created or the secret is rotated. Store it in an environment variable, one secret per endpoint. - An endpoint can be paused and resumed by the merchant. Events for a paused or disabled endpoint are recorded as skipped; skipped events can be resent from the delivery history. ### Event types - `checkout.session.completed` — Payment received and settled. Compare `data.object.amount_received` with `amount` before fulfilling: bridge and on-ramp fees can deliver slightly less (normally within 5%), and support may settle a larger underpayment after contacting the merchant. - `checkout.session.expired` — The session timed out without a confirmed payment. - `checkout.session.failed` — A payment arrived but settlement failed on CashTap's side. CashTap support resolves these. Do not fulfill. `expired` or `failed` can later be followed by `completed` for the same session (late payment recovery). `completed` is final. There is exactly one event id per (session, event type). ### Payload Every event uses the same JSON envelope (UTF-8): { "id": "evt_FjhHMSLvI1sX4xUDxI4Wbw5h", "object": "event", "type": "checkout.session.completed", "api_version": "2026-09-01", "created": 1788998042, "livemode": true, "data": { "object": { "object": "checkout.session", "id": "cs_live_AAAAAAAAAAAAAAAAAAAAAA", "url": "https://pay.cashtap.cash/c/cs_live_AAAAAAAAAAAAAAAAAAAAAA", "status": "completed", "amount": 25, "amount_received": 24.91, "fee_amount": 0.75, "net_amount": 24.16, "line_items": [ { "name": "Pro plan", "description": null, "image_url": null, "quantity": 1, "unit_amount": 25 } ], "payment_method": "cashapp", "payment_methods": ["coinbase", "wallet", "cashapp"], "customer_email": "jane@example.com", "success_url": "https://acme-store.com/thanks", "cancel_url": "https://acme-store.com/cart", "metadata": { "order_id": "A-1042" }, "paid_at": 1788997990, "incoming_tx_hash": null, "settlement_tx_hash": "0x9f…c1", "fee_sweep_tx_hash": "0x1a…7e", "expires_at": 1789001590, "created_at": 1788997990 } } } - `id` — Unique event id (`evt_…`). The same id is sent on every retry, on every resend and to every endpoint. Deduplicate on it. - `type` — One of the three event types above. - `api_version` — Version of the payload format. Currently `2026-09-01`. - `created` — Unix timestamp (seconds) at which the event was created. - `livemode` — `true` for real sessions, `false` for test events. - `data.object` — The checkout session as it was when the event happened. Same fields as `GET /checkout/v1/sessions/{session_id}` plus `"object": "checkout.session"`. `status` always matches the event type. `metadata` is echoed. In `expired` and `failed` events the settlement fields (`amount_received`, `fee_amount`, `net_amount`, `settlement_tx_hash`, `fee_sweep_tx_hash`) are `null`. `expired` also has `payment_method`, `paid_at` and `incoming_tx_hash` set to `null`. ### Headers | Header | Value | |-------------------------|----------------------------------------------------------------------------| | `Content-Type` | `application/json; charset=utf-8` | | `User-Agent` | `CashTap-Webhooks/1.0 (+https://cashtap.cash/developers/docs#webhooks)` | | `X-CashTap-Signature` | `t=,v1=[,v1=]` | | `X-CashTap-Event-Id` | `evt_…`. Same across retries, resends and endpoints. Equals the body `id`. | | `X-CashTap-Event-Type` | e.g. `checkout.session.completed`. Equals the body `type`. | | `X-CashTap-Delivery-Id` | `whd_…`. One per delivery. A resend is a NEW delivery id. | | `X-CashTap-Attempt` | `1` to `6` for automatic deliveries. Always `1` for tests and resends. | CashTap never sends cookies, an `Authorization` header or custom headers. Only the body is authenticated: treat the `X-CashTap-*` headers as hints and dedupe on the event id, never on the delivery id. ### Signature verification Every request carries `X-CashTap-Signature: t=,v1=`. The signed payload is the timestamp `t` as ASCII digits, a literal `.`, then the exact raw bytes of the request body (`.`). Each `v1` is the lowercase hex HMAC-SHA256 of that payload, keyed with the whole `whsec_…` string as UTF-8 bytes: do NOT strip the prefix and do NOT base64-decode it (this differs from Svix). A receiver must read the raw body before any JSON parsing, reject the request when `t` is more than 300 seconds away from its own clock, and accept it when ANY `v1` matches under a constant-time comparison. `t` is fresh on every attempt. For 24 hours after a secret rotation the header carries two `v1` values (current secret first) so the new secret can be deployed without dropping events. Node.js (Express) — this exact code is tested against the vector below: const crypto = require("crypto"); // Register BEFORE express.json(): // app.post("/webhooks/cashtap", express.raw({ type: "application/json" }), handler) function verifyCashTapSignature(rawBody, header, secret, toleranceSec) { if (toleranceSec === undefined) toleranceSec = 300; if (typeof header !== "string" || header.length > 1024) return false; let t = null; const sigs = []; for (const part of header.split(",")) { const i = part.indexOf("="); if (i === -1) continue; const k = part.slice(0, i).trim(); const v = part.slice(i + 1).trim(); if (k === "t") t = v; else if (k === "v1") sigs.push(v); } if (!/^\d{1,12}$/.test(t || "")) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false; // replay window const expected = crypto.createHmac("sha256", secret).update(t + ".").update(rawBody).digest(); return sigs.slice(0, 5).some(function (s) { return /^[0-9a-f]{64}$/i.test(s) && crypto.timingSafeEqual(Buffer.from(s, "hex"), expected); }); } function handler(req, res) { const ok = verifyCashTapSignature(req.body, req.get("X-CashTap-Signature"), process.env.CASHTAP_WEBHOOK_SECRET); if (!ok) return res.sendStatus(400); const event = JSON.parse(req.body.toString("utf8")); // 1) dedupe on event.id (unique index) 2) respond 2xx fast 3) process asynchronously if (event.type === "checkout.session.completed") { const s = event.data.object; if (s.amount_received < s.amount) { /* underpaid within tolerance: decide before fulfilling */ } } res.sendStatus(200); } Test vector (the timestamp is fixed at 2026-01-01, so call the verifier with the tolerance disabled, or freeze the clock): secret : whsec_test_secret_do_not_use t : 1767225600 body : {"id":"evt_123","type":"checkout.session.completed"} v1 : 66a710e3cc2913be941031d9e7691c085570a75feb121fa1cc59070421dd44a2 header : t=1767225600,v1=66a710e3cc2913be941031d9e7691c085570a75feb121fa1cc59070421dd44a2 Common causes of a signature mismatch: hashing re-serialized JSON instead of the raw bytes; a global JSON body parser (`app.use(express.json())`) registered before the webhook route, which has already consumed the body; using another endpoint's secret; decoding the `whsec_…` string; an unsynced server clock; checking only the first `v1` during a rotation. ### Responding, retries and auto-disable - Any 2xx status marks the delivery successful. Everything else is a failed attempt. Redirects are never followed. - Respond within 10 seconds. Acknowledge first, do slow work afterwards. - The first 2,000 characters of the response body are stored for debugging and shown in the merchant's delivery history. - A failed delivery is retried: 6 attempts over about 12 hours. Attempt 1 is immediate; attempts 2 to 6 follow about 1 min, 5 min, 30 min, 2 h and 9 h after the previous attempt (±10% jitter). - `Retry-After` on a 429 or 503 response is honored up to 1 hour. - After the sixth failed attempt the delivery is marked failed. The merchant can resend it from the delivery history (kept for 30 days). - CashTap emails the merchant on the first failure of a streak, when a delivery exhausts its retries, and on recovery. - An endpoint is disabled automatically after 72 hours of consecutive failures. The merchant re-enables it from the dashboard and can resend the skipped events from the delivery history. ### Delivery guarantees - At-least-once: the same event can arrive more than once. Dedupe on `event.id` (for example a unique index); retries and resends reuse it. - No ordering guarantee: events can arrive out of order. - For high-value fulfillment, confirm the session with `GET /checkout/v1/sessions/{session_id}` before shipping. ### Test events The dashboard can send a test event to any endpoint. Test events use one of the three real event types with `"livemode": false`, an id starting with `evt_test_`, a session id starting with `cs_test_` (it cannot be retrieved through the API) and `"metadata": { "cashtap_test": "true" }`. They are signed exactly like real events and are sent once, without retries. Check `livemode` to ignore them in production logic. ### Idempotency Use `event.id` for idempotency on your side. The API does not accept idempotency keys yet, so do not send an `Idempotency-Key` header and expect it to be honored. ## Settlement & fees When a buyer pays: 1. Funds first land in a CashTap-controlled dedicated wallet. 2. The platform fee is swept to CashTap's admin wallet. 3. The net amount is swept to the merchant's CashTap wallet. 4. The session is marked `completed` and the `checkout.session.completed` webhook fires. Default platform fee: **2.9% + $0.30** per successful checkout. The fee is automatically deducted before settlement. The merchant receives `net_amount` in their wallet. The full breakdown is in the retrieve-session response (`amount`, `fee_amount`, `net_amount`). Custom pricing is available for higher volume. Contact support. ## Status Status: everything in this file is live in production. Merchant accounts are approved manually; an approved merchant can mint API keys, create sessions and receive webhooks today. ## Versioning The REST base path is `/checkout/v1`. Webhook payloads carry `api_version` (currently `2026-09-01`). Additive fields may appear at any time; breaking changes ship under a new base path or api_version with 12 months of overlap.