Use the Idempotency-Key header to safely retry requests. If a request with the same key was already processed, the original response is returned. Keys expire after 24 hours.
3
Handle the webhook
When the payment is confirmed on-chain, billing.io sends a checkout.completed event to your registered webhook endpoint.First, register a webhook endpoint:
The webhook signing secret (whsec_ prefix) is only returned once at creation time. Store it securely — you will need it to verify incoming webhook signatures.
Then handle incoming webhooks in your application:
Node.js
import crypto from "crypto";export async function POST(request) { const body = await request.text(); const signature = request.headers.get("X-Billing-Signature"); // Parse the signature header const parts = Object.fromEntries( signature.split(",").map((p) => p.split("=")) ); const timestamp = parts.t; const receivedHmac = parts.v1; // Compute expected HMAC const payload = `${timestamp}.${body}`; const expectedHmac = crypto .createHmac("sha256", process.env.WEBHOOK_SECRET) .update(payload) .digest("hex"); if (receivedHmac !== expectedHmac) { return new Response("Invalid signature", { status: 401 }); } // Process the event const event = JSON.parse(body); switch (event.type) { case "checkout.completed": const checkout = event.data; console.log(`Payment confirmed: ${checkout.checkout_id}`); console.log(`Amount: $${checkout.amount_usd}`); console.log(`Tx: ${checkout.tx_hash}`); // Fulfill the order in your system break; case "checkout.expired": console.log(`Checkout expired: ${event.data.checkout_id}`); // Mark the order as expired break; } return new Response("OK", { status: 200 });}
A checkout.completed webhook payload looks like this:
Use your sk_test_ API key and the sandbox environment to test the full flow without real crypto. You can simulate payment confirmations from the Sandbox page in the dashboard.
Sandbox Testing Guide
Learn how to simulate payments, test webhooks, and validate your integration in sandbox mode.