Embed Widget Flow
From
<script>
tag to Lightning settlement — every step visualized.
Flow Overview
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).
Sequence Diagram
Step-by-Step Detail
Merchant Website Loads
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.
Script Initializes
pay.js
reads the
data-*
attributes from the script tag:
-
data-key— validatespk_live_orpk_test_prefix format -
data-product— product slug to fetch price from (ordata-amountfor 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.
Button Rendered
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.
Customer Clicks Button
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.
SatsRail Creates Checkout Session
The API server:
- Validates the publishable key against the merchant's API tokens
- Looks up the product — fetches name, description, price (USD)
- Converts USD price → sats using the real-time BTC/USD exchange rate
- Creates a CheckoutSession record with a 15-minute expiry
- Returns checkout_url, token, and expires_at
Rate limiting:
API requests are throttled per key. Exceeding limits returns
429 Too Many Requests
Checkout Opens
Based on
data-mode:
- iframe: Opens an embedded overlay on the merchant's page (500×700 on desktop, full-screen on mobile). No popup blockers.
- redirect: Navigates the customer's browser to the SatsRail hosted checkout page.
Invoice Generated (Non-Custodial)
SatsRail requests a Lightning invoice from the merchant's connected wallet/node:
- SatsRail sends the sats amount to the merchant's wallet connection
- The merchant's node generates a BOLT-11 invoice
- The invoice is returned to SatsRail and stored in the Invoice record
QR Code Displayed
The checkout page renders:
- Product name and description
- Price in USD and equivalent in sats
- QR code encoding the BOLT-11 invoice
- "Copy Invoice" button for desktop wallets
- 15-minute countdown timer for invoice expiry
WebLN-enabled browsers (e.g., Alby extension) are auto-detected — the wallet prompts to pay without scanning.
Customer Pays
Three payment paths:
- Path A — QR Scan: Customer opens their Lightning wallet app (Phoenix, Muun, etc.), scans the QR code, confirms the amount, and sends payment.
- Path B — Copy & Paste: Customer copies the BOLT-11 string, pastes into a desktop wallet, and confirms.
- Path C — WebLN: Browser extension wallet (Alby) auto-prompts. Customer clicks "Pay" in the extension popup.
Lightning Settlement (Instant)
Payment routes through Lightning Network channels and arrives at the merchant's wallet instantly. SatsRail detects settlement via:
- Primary: PaymentMonitorJob polls the merchant's wallet every 5 seconds
- Secondary: Webhook from the merchant's Lightning node (if configured)
Settlement is final and irreversible — no chargebacks.
Confirmation & Post-Payment
Once confirmed:
- Transaction logged: timestamp, amount_sats, USD value at payment time, product_id, payment_hash, preimage
- WebSocket broadcast: Checkout page receives real-time "payment confirmed" event
- UI updates: Modal/page shows "Payment Confirmed ✓" with transaction details
- Redirect: Customer sent to success_url (or shown confirmation if no URL configured)
- Webhook fired: Async POST to merchant's webhook URL with full payment payload
Error Paths
| 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 |
Security Architecture
Publishable Key Only
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.
Non-Custodial
SatsRail never holds merchant funds. Lightning invoices are generated by the merchant's own node. Payments flow directly from customer → merchant.
CORS Enabled
The
/api/v1/checkout_sessions
endpoint allows wildcard CORS origins, so the widget works from any domain without configuration.
HTTPS Only
All communication between pay.js and SatsRail API is over TLS. The CDN serves the script via HTTPS with SRI integrity hashes.
Add Bitcoin payments in 60 seconds
One script tag. No backend. Non-custodial Lightning payments.