session.paid webhook. Every integration, whether it’s a custom storefront or a WooCommerce plugin, follows this same loop. By the end you’ll have made a real (or testnet) payment and verified it lands correctly in both your backend and the dashboard.
Prerequisites
Before starting, make sure you have:- A running Crypto Checkout instance (self-hosted or deployed to Vercel)
- A wallet configured in Dashboard → Settings → Wallet with a BIP39 mnemonic imported
- At least one API key created in Dashboard → API Keys — copy the raw
ck_test_...value shown at creation time, as it is never displayed again - (Optional) A publicly reachable webhook endpoint registered in Dashboard → Webhooks
End-to-end flow
Send a
POST /api/v1/checkout_sessions request from your backend. Always include an Idempotency-Key header so that a network retry cannot create a duplicate session.Never call this endpoint from the browser — your API key would be exposed. Create sessions server-side and redirect the customer to the returned
checkoutUrl.curl -X POST https://your-domain.com/api/v1/checkout_sessions \
-H "Authorization: Bearer ck_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-001-v1" \
-d '{
"amount": { "value": "50.00", "currency": "USD" },
"metadata": { "orderId": "001" },
"successUrl": "https://your-store.com/success"
}'
import { CheckoutClient } from "@your-org/checkout-sdk";
const client = new CheckoutClient({
apiKey: process.env.CHECKOUT_API_KEY!,
baseUrl: "https://your-domain.com",
});
const session = await client.checkoutSessions.create(
{
amount: { value: "50.00", currency: "USD" },
metadata: { orderId: "001" },
successUrl: "https://your-store.com/success",
},
{ idempotencyKey: "order-001-v1" },
);
{
"id": "cs_01HZ...",
"object": "checkout_session",
"status": "pending",
"checkoutUrl": "https://your-domain.com/checkout/cs_01HZ...",
"address": "0xAbCd...1234",
"amount": { "wei": "18473920000000000000", "eth": "18.47392" },
"fiat": { "amount": "50.00", "currency": "USD" },
"expiresAt": "2024-11-01T12:05:00.000Z",
"livemode": false,
"createdAt": "2024-11-01T12:00:00.000Z"
}
Redirect your customer’s browser to
session.checkoutUrl. This is a hosted page served by your Crypto Checkout instance — you do not need to build any payment UI yourself.The customer opens their wallet (MetaMask, Rabby, a mobile wallet, etc.), scans the QR code or copies the address, and sends exactly the displayed amount to the shown address on the correct chain. The page polls for status changes in real time and advances automatically once payment is detected.
The customer must send the exact amount shown. Sending less leaves the session in
underpaid; sending more moves it to overpaid. See the edge cases section below.As soon as the transaction is broadcast, Alchemy Notify POSTs to your Crypto Checkout instance. The session status transitions from
pending → detected almost immediately. The checkout page updates to show a confirmation counter.Crypto Checkout waits 60 seconds, then re-reads the on-chain receipt and counts confirmations. Once the confirmation count reaches the required number of confirmations (default: 3), the session transitions from
detected → paid and the session.paid event fires.You can increase the required number of confirmations in your environment for higher-value payments. For testnet development, lowering it to
1 speeds up the test cycle.Your backend receives a
POST from Crypto Checkout with the event type session.paid. Always verify the signature before fulfilling an order.// Express.js handler
import { verifyWebhook } from "@your-org/checkout-sdk/webhooks";
app.post("/webhooks/crypto", express.raw({ type: "*/*" }), (req, res) => {
const event = verifyWebhook({
payload: req.body.toString(), // raw string — do NOT JSON.parse first
signature: req.headers["x-webhook-signature"] as string,
secret: process.env.CHECKOUT_WEBHOOK_SECRET!,
});
if (event.type === "session.paid") {
const session = event.data as { metadata?: { orderId?: string } };
const orderId = session.metadata?.orderId;
// Mark the order paid in your database, send confirmation email, etc.
fulfillOrder(orderId);
}
res.sendStatus(200); // respond 2xx within the timeout window
});
Crypto Checkout retries failed deliveries up to 6 times over approximately 31 hours using exponential backoff. Returning a non-2xx status or timing out counts as a failure and triggers a retry.
Handling edge cases
| Scenario | Session status | Recommended action |
|---|---|---|
| Session expired before payment | expired | Create a new session and redirect the customer again |
| Customer sent too little | underpaid | Session stays underpaid; contact the customer. Create a new session for any remaining balance if needed. |
| Customer sent too much | overpaid | Session transitions to paid; Crypto Checkout does not auto-refund. Handle the overage manually. |
| Payment arrived just after expiry | paid_late | Within the late payment grace window (default: 10 minutes), the session is credited as paid_late. Treat as paid. |
Next steps
Webhooks overview
Learn about all available event types and delivery guarantees.
Session lifecycle
Understand every status and the transitions between them.
Troubleshooting
Fix common issues with payment detection and webhook delivery.