From
<script>
tag to Lightning settlement — every step visualized.
This diagram traces the complete lifecycle when a merchant adds the SatsRail embed widget to their website. The customer never leaves the merchant's page — checkout happens in an embedded overlay or redirect, with funds settling directly to the merchant's Lightning wallet (non-custodial).
The merchant's HTML page includes a script tag:
<script src="https://satsrail.com/js/pay.js"
data-product="prod_abc123"
data-key="pk_live_..."
data-mode="iframe"></script>
The browser sends a GET request to the SatsRail CDN to fetch the
pay.js
bundle.
pay.js
reads the
data-*
attributes from the script tag:
data-key
— validates
pk_live_
or
pk_test_
prefix format
data-product
— product slug to fetch price from (or
data-amount
for direct amount)
data-mode
— "iframe", "new_tab", or "redirect" (default: uses merchant setting)
If the API key format is invalid, the script stops and renders an error state.
The script injects a styled
"Pay with Bitcoin ⚡"
button into the DOM at the script tag's position. The button class is
.satsrail-pay-btn
and can be styled with custom CSS.
pay.js
sends a
POST
request to
/api/v1/checkout_sessions
with:
Payload:
{ product_id: "prod_abc123", publishable_key: "pk_live_...", success_url, cancel_url }
The CORS endpoint allows wildcard origins, so this works from any merchant domain.
The API server:
Rate limiting:
API requests are throttled per key. Exceeding limits returns
429 Too Many Requests
Based on
data-mode:
SatsRail requests a Lightning invoice from the merchant's connected wallet/node:
The checkout page renders:
WebLN-enabled browsers (e.g., Alby extension) are auto-detected — the wallet prompts to pay without scanning.
Three payment paths:
Payment routes through Lightning Network channels and arrives at the merchant's wallet instantly. SatsRail detects settlement via:
Settlement is final and irreversible — no chargebacks.
Once confirmed:
| Error | Trigger | User Experience | Recovery |
|---|---|---|---|
| Invalid API Key | pk_live_ format check fails or key not found in database |
Script renders error state — no checkout button shown | Merchant must fix the data-key attribute |
| Product Not Found | Product slug doesn't match any active product | Button shows error message for 3 seconds, then reverts | Merchant verifies product slug in dashboard |
| Invoice Expired | 15-minute countdown reaches zero before payment | Modal shows "Expired" message | Customer clicks "Generate New Invoice" — new invoice at updated BTC rate |
| Lightning Routing Failure | No route found to merchant's node, insufficient channel capacity | Error message shown on checkout page | Customer retries (may succeed with different routing) |
| Network Timeout | API request from pay.js fails (DNS, connectivity) | Button shows "Connection error" briefly | pay.js retries with exponential backoff: 1s → 2s → 4s → 8s (max 3 retries) |
| Session Creation Failed | Server error (500) or validation error (422) | Button shows error message | Customer clicks button again to retry |
The embed widget uses
pk_live_
(publishable key) — safe to expose in client-side HTML. The secret key
sk_live_
is never used in the widget.
SatsRail never holds merchant funds. Lightning invoices are generated by the merchant's own node. Payments flow directly from customer → merchant.
The
/api/v1/checkout_sessions
endpoint allows wildcard CORS origins, so the widget works from any domain without configuration.
All communication between pay.js and SatsRail API is over TLS. The CDN serves the script via HTTPS with SRI integrity hashes.
One script tag. No backend. Non-custodial Lightning payments.