CashPay API
Base btc-lightning.com Version v1
Merchant integration guide

Accept Lightning payments with the CashPay API

Create package-based invoices from your server, show the Lightning invoice to your customer, then credit them when CashPay notifies your webhook. All business calls require HMAC signatures.

https://btc-lightning.com/v1 HTTPS only JSON

Overview

Typical flow for a merchant backend.

1

List packages

Fetch enabled packages and amounts configured for your account.

2

Create a payment

Send package_id + your order number. Receive a BOLT11 invoice.

3

Customer pays

Display the invoice (QR / wallet). Amount is fixed by the package.

4

Webhook + verify

On payment.paid, verify the signature, then credit your user idempotently.

Credentials: Create an API Key / Secret and Webhook Secret in the CashPay merchant dashboard. The Secret is shown only once — store it on your server, never in a browser or mobile app.

API Explorer

Verify connectivity and call live endpoints from this page. Signing runs only in your browser — the API Secret is never uploaded as plaintext and never written to our logs from this UI.

Security: Use credentials only on a trusted device. Prefer a dedicated key. If your account has an IP allowlist, your current public IP must be allowed (browser requests use your IP). Production apps must sign on your backend — do not embed secrets in client apps.

Ready
{}

Authentication

Every business endpoint requires these headers. GET /v1/health is public and does not require signing.

HeaderDescription
X-Api-KeyYour public API key (e.g. mk_live_…)
X-TimestampUnix timestamp in seconds (string of digits)
X-NonceRandom string, 8–64 characters. Must be unique per request
X-SignatureLowercase hex HMAC-SHA256 of the canonical string (see below)
Content-Typeapplication/json for requests with a body
Acceptapplication/json (recommended)
  • Timestamp must be within ±300 seconds of server time.
  • Reusing the same nonce with the same API key returns 401.
  • If an IP allowlist is configured on your account, requests must originate from an allowed IP.

Two secrets — do not mix

SecretUsed forHeader
API Secret Outbound API calls you send to CashPay X-Signature
Webhook Secret Inbound webhooks CashPay sends to your notify_url X-CashPay-Signature
Do not mix secrets. Signing an API request with the Webhook Secret (or verifying a webhook with the API Secret) always fails. Each secret is shown only once when created or rotated — store it on your backend immediately.

Request signing

Build a canonical string, then compute HMAC-SHA256 with your API Secret.

Canonical string

Join the five parts with a real newline character (\n), not spaces and not an empty separator.

{timestamp}\n{nonce}\n{METHOD}\n{pathWithQuery}\n{sha256Hex(body)}
PartRules
timestampSame value as X-Timestamp
nonceSame value as X-Nonce
METHODUppercase HTTP method: GET, POST, …
pathWithQuery Request path starting with /, plus query string if present. Examples: /v1/packages, /v1/payments?merchant_order_no=ORD-1. Do not include the scheme or host.
sha256Hex(body) Hex SHA-256 of the raw request body bytes. For empty body (typical GET), hash the empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Signature

X-Signature = hex( HMAC_SHA256( api_secret, canonical_string ) )

Compare signatures using a constant-time equality check. Send the hex digest in lowercase.

Important: Sign the exact bytes you send. For JSON, do not re-serialize after signing. Query parameter order in the signed path must match the URL you request.

Common signing mistakes

  • Concatenating the five parts without \nInvalid signature.
  • GET with a body (e.g. HTTP clients serializing null as "null") while hashing an empty body → mismatch. For GET, send no body.
  • Signing /v1/payments but calling /v1/payments?merchant_order_no=… (or the reverse) → mismatch. Path + query must match exactly.
  • Using Webhook Secret instead of API Secret (or trailing spaces when pasting the secret) → mismatch.

List packages

GET /v1/packages

Returns enabled packages for the authenticated merchant. Amounts are authoritative — never invent or override them client-side.

Response 200

{
  "items": [
    {
      "id": "a038f7a9-f20f-4f9d-8aa4-6b53662139c2",
      "name": "Starter",
      "amount_usd": "100.00",
      "sort_order": 0
    }
  ]
}

Create payment

POST /v1/payments

Request body

FieldRequiredDescription
package_idYesPackage id from GET /v1/packages
merchant_order_noYesYour unique order id (max 64 chars). Field name is exactly merchant_order_no — not order_no / out_trade_no. Used for idempotency and echoed in webhooks as data.merchant_order_no
notify_urlNoHTTPS URL for this payment’s webhook. Falls back to your default notify URL. If the URL includes your order id in the path, webhook signing must use that full path
metadataNoJSON object echoed back on reads (max ~2KB)
Do not send amount_usd. Amounts come only from the selected package. Sending amount_usd returns 422.

Example request

{
  "package_id": "a038f7a9-f20f-4f9d-8aa4-6b53662139c2",
  "merchant_order_no": "ORD-10086",
  "notify_url": "https://merchant.example.com/hooks/cashpay",
  "metadata": { "user_id": "u10086" }
}

Response 201 (new) or 200 (idempotent replay)

{
  "id": "9236f138-07b5-4d1c-9fe6-bea17a29bc06",
  "merchant_order_no": "ORD-10086",
  "package_id": "a038f7a9-f20f-4f9d-8aa4-6b53662139c2",
  "package_name": "Starter",
  "amount_usd": "100.00",
  "status": "pending",
  "bolt11": "lnbc…",
  "pay_url": "https://www.example.com/pay/invoice/…",
  "expires_at": "2026-07-18 16:20:00",
  "paid_at": null,
  "created_at": "2026-07-18 16:10:00",
  "metadata": { "user_id": "u10086" }
}
  • Repeating the same merchant_order_no returns the existing payment (idempotent).
  • Redirect the payer to pay_url (hosted checkout), and/or present bolt11 (Lightning invoice string), until status becomes paid or the invoice expires.
  • Payment statuses you may see: pending, paid, expired.
  • IDs: response id is the CashPay payment id; webhook data.invoice_id is the invoice id used in pay_url. Do not confuse them with merchant_order_no.
  • pay_url may be null if the hosted checkout base URL is not configured on the CashPay side — you can still use bolt11.

Get payment

GET /v1/payments/{id}

Lookup by CashPay payment id (UUID). Same response shape as create.

GET /v1/payments?merchant_order_no={order}

Lookup by your order number. Path used for signing must include the query string, e.g. /v1/payments?merchant_order_no=ORD-10086.

Webhooks are the primary success signal. Polling is optional for UX (e.g. waiting screens). Always treat webhook delivery as authoritative after signature verification.

Webhooks

When a payment is confirmed, CashPay sends POST to your notify_url (HTTPS only) with event payment.paid.

Log the X-CashPay-* headers on your notify endpoint while integrating. Signature verification requires those headers plus the raw body — body-only logs are not enough to debug.

Headers

HeaderDescription
Content-Typeapplication/json
X-CashPay-TimestampUnix timestamp (seconds). Same role as X-Timestamp on API requests
X-CashPay-NonceUnique nonce. Same role as X-Nonce on API requests
X-CashPay-SignatureLowercase hex HMAC-SHA256 of the canonical string (Webhook Secret)

Verify exactly like inbound API signing

{X-CashPay-Timestamp}\n{X-CashPay-Nonce}\nPOST\n{pathWithQuery}\n{sha256Hex(rawBody)}
  • Use your Webhook Secret (not the API Secret) as the HMAC key.
  • pathWithQuery is the path (and query, if any) of your notify URL — the full path CashPay called. Examples: https://merchant.example.com/hooks/cashpay/hooks/cashpay; https://merchant.example.com/pay/notify/ORD-10086/pay/notify/ORD-10086 (include the order segment).
  • Hash the raw request body bytes; do not parse-then-restringify before verifying.
  • Reject if timestamp is outside a reasonable window (e.g. ±300 seconds).
  • After you accept the event for processing, respond with HTTP 2xx quickly. Non-2xx responses are retried by CashPay.

Payload

Read the order number from data.merchant_order_no (nested under data). CashPay does not send a top-level order_no field. Map to your internal name only inside your own code.
{
  "id": "d5a63767-7f4d-4c21-8e2a-f887f4a8152e",
  "event": "payment.paid",
  "created_at": "2026-07-18T08:16:33Z",
  "data": {
    "id": "9236f138-07b5-4d1c-9fe6-bea17a29bc06",
    "merchant_order_no": "ORD-10086",
    "package_id": "a038f7a9-f20f-4f9d-8aa4-6b53662139c2",
    "package_name": "Starter",
    "amount_usd": "100.00",
    "invoice_id": "142bd039-5c60-4a6f-9cc0-e8583d7b91d9",
    "paid_at": "2026-07-18 16:11:54",
    "status": "paid"
  }
}

Merchant responsibilities

  1. Verify X-CashPay-Signature with Webhook Secret before trusting the body.
  2. Confirm event === "payment.paid" and data.amount_usd matches your order expectation.
  3. Credit using data.id or data.merchant_order_no as an idempotency key — webhooks may be delivered more than once.
  4. Never trust browser redirects or unpaid invoice display as payment success.
  5. HTTP 2xx means CashPay will stop retrying that delivery attempt. Your JSON business result (if any) is separate — handle failures in your own system after a valid signed event.

Errors

Errors return JSON: {"message":"…"}.

HTTPMeaning
401Missing/invalid headers, bad signature, expired timestamp, or reused nonce
403API disabled for the merchant, or IP not allowlisted
404Payment not found (or not owned by this API key)
422Validation error (missing fields, invalid package, amount_usd sent, etc.)
429API key temporarily locked
500Unexpected server error
GET /v1/health

Public health check. Example: {"ok":true,"service":"cashpay-merchant-api"}

Troubleshooting

Self-check guide for the most common integration failures. No secrets are shown here.

SymptomWhat to check
401 / Invalid signature Canonical parts joined with \n; correct secret type; path+query matches the URL; GET has empty body; no extra spaces in the secret
401 / Timestamp expired Server clock skew; send Unix seconds (not milliseconds)
401 / Nonce already used Generate a new nonce for every request
Webhook accepted by CashPay but your app rejects the order Read data.merchant_order_no (not a renamed field); ensure your local order is still payable; log X-CashPay-* headers while debugging
Webhook signature always fails on your side Use Webhook Secret; method is always POST; path includes every path segment of your notify URL; hash raw body bytes

Code examples

Copy-paste helpers that list packages (smoke test). Keep secrets on your server. Code samples stay in English. For an interactive demo, use the API Explorer above.

Health needs no auth. For signed calls, generate headers with your backend (or the Explorer), then:

# 1) Connectivity (no credentials)
curl -sS https://btc-lightning.com/v1/health

# 2) List packages — replace the four auth headers from your signer
curl -sS https://btc-lightning.com/v1/packages \
  -H "Accept: application/json" \
  -H "X-Api-Key: mk_live_..." \
  -H "X-Timestamp: 1710000000" \
  -H "X-Nonce: $(openssl rand -hex 16)" \
  -H "X-Signature: <hmac_hex>"

OpenAPI

Machine-readable contract for Postman, Insomnia, codegen, and API gateways: /docs/openapi.json

  • Import into Postman → run the same smoke tests as the Explorer.
  • Does not include internal implementation details — only public request/response shapes.

Go-live checklist

  • Store API Secret and Webhook Secret only on your backend.
  • Create at least one enabled package and note its package_id.
  • Set a default HTTPS notify_url (or pass one per payment).
  • Implement signature verification before any balance credit.
  • Make credits idempotent on payment id / merchant_order_no.
  • Optional: configure an IP allowlist for your API servers.
  • Test: create payment → pay invoice → confirm webhook arrives and verifies.
  • While integrating, log webhook X-CashPay-* headers and verify against data.merchant_order_no.
Need credentials or packages? Open the CashPay merchant dashboard → Open API.