Skip to Content

Checkout Webhooks

Receive real-time notifications when a checkout status changes. Webhooks are sent as HTTP POST requests to the URL you specify when creating a checkout.

Webhook Payload

When a checkout status changes to a final state (COMPLETED, FAILED, or CANCELLED), BLOX sends a POST request to your webhookUrl:

{ "checkoutId": "8f14e45f-ceea-467f-a830-5e3e3c7e2b8a", "status": "COMPLETED", "amount": "15000", "timestamp": "2026-02-03T15:05:30.000Z" }

Payload Fields

FieldTypeDescription
checkoutIdstring (UUID)The checkout identifier
statusstringFinal status: COMPLETED, FAILED, or CANCELLED
amountstringPayment amount in cents
timestampstring (ISO 8601)When the status changed

Status Events

StatusDescriptionWhen Triggered
COMPLETEDPayment successfulFunds transferred to merchant wallet
FAILEDPayment failedInsufficient balance, network error, etc.
CANCELLEDCheckout cancelledCustomer cancelled, timeout (20 min), or error

Webhook Configuration

Set the webhookUrl when creating a checkout:

curl -X POST "https://api.blox.my/v1/checkout" \ -H "blox-api-key: $BLOX_API_KEY" \ -H "Content-Type: application/json" \ -H "Content-Digest: sha-256=:$DIGEST:" \ -H "Signature-Input: sig1=..." \ -H "Signature: sig1=:$SIG:" \ -d '{ "addressTo": "0x742d35Cc...", "amount": 15000, "tokenId": "550e8400-...", "redirectUrl": "https://yoursite.com/success", "webhookUrl": "https://yoursite.com/webhooks/blox", "title": "Order #123" }'

URL Requirements

  • Must be a valid HTTPS URL (HTTP not allowed in production)
  • Must be publicly accessible from the internet
  • Should respond within 10 seconds (timeout)
  • Should return a 2xx status code to acknowledge receipt

Handling Webhooks

Example: Node.js (Express)

import express from "express"; const app = express(); app.use(express.json()); app.post("/webhooks/blox", async (req, res) => { const { checkoutId, status, amount, timestamp } = req.body; console.log(`Checkout ${checkoutId} status: ${status}`); try { switch (status) { case "COMPLETED": // Payment successful - fulfill the order await fulfillOrder(checkoutId, amount); break; case "FAILED": // Payment failed - notify customer await notifyPaymentFailed(checkoutId); break; case "CANCELLED": // Checkout cancelled - clean up await handleCancellation(checkoutId); break; } // Acknowledge receipt res.status(200).json({ received: true }); } catch (error) { console.error("Webhook processing error:", error); // Return 500 to trigger retry res.status(500).json({ error: "Processing failed" }); } }); async function fulfillOrder(checkoutId: string, amount: string) { // Mark order as paid in your database // Send confirmation email // Trigger fulfillment process } async function notifyPaymentFailed(checkoutId: string) { // Send failure notification to customer // Log for support investigation } async function handleCancellation(checkoutId: string) { // Release any reserved inventory // Update order status }

Example: Python (Flask)

from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/blox", methods=["POST"]) def handle_webhook(): data = request.json checkout_id = data["checkoutId"] status = data["status"] amount = data["amount"] print(f"Checkout {checkout_id} status: {status}") try: if status == "COMPLETED": fulfill_order(checkout_id, amount) elif status == "FAILED": notify_payment_failed(checkout_id) elif status == "CANCELLED": handle_cancellation(checkout_id) return jsonify({"received": True}), 200 except Exception as e: print(f"Webhook error: {e}") return jsonify({"error": "Processing failed"}), 500 def fulfill_order(checkout_id, amount): # Mark order as paid, send confirmation, etc. pass def notify_payment_failed(checkout_id): # Notify customer of failure pass def handle_cancellation(checkout_id): # Clean up cancelled checkout pass

Idempotency

Webhooks may be delivered more than once in rare cases (network issues, retries). Always implement idempotent webhook handlers:

// Store processed webhook IDs to prevent duplicate processing const processedWebhooks = new Set<string>(); app.post("/webhooks/blox", async (req, res) => { const { checkoutId, status, timestamp } = req.body; // Create a unique key for this webhook event const webhookKey = `${checkoutId}:${status}:${timestamp}`; // Check if already processed if (processedWebhooks.has(webhookKey)) { console.log(`Duplicate webhook ignored: ${webhookKey}`); return res.status(200).json({ received: true }); } // Mark as processed before handling (prevents race conditions) processedWebhooks.add(webhookKey); try { await processWebhook(checkoutId, status); res.status(200).json({ received: true }); } catch (error) { // Remove from processed set so it can be retried processedWebhooks.delete(webhookKey); res.status(500).json({ error: "Processing failed" }); } });

For production, use a database instead of an in-memory set:

CREATE TABLE processed_webhooks ( webhook_key VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

Timeout & Retries

PropertyValue
Timeout10 seconds
RetriesNone (currently)

Important: If your webhook endpoint doesn’t respond within 10 seconds, the request is aborted. The webhook is not automatically retried. Use the status endpoint to query missed updates.

Best Practices

  1. Respond quickly — Acknowledge the webhook immediately, then process asynchronously
  2. Use a queue — Push webhook data to a job queue for reliable processing
  3. Log everything — Store raw webhook payloads for debugging
  4. Verify checkout — Always verify the checkout status via API for critical operations
// Fast acknowledgment with async processing app.post("/webhooks/blox", async (req, res) => { // Immediately acknowledge res.status(200).json({ received: true }); // Process asynchronously setImmediate(async () => { try { await processWebhook(req.body); } catch (error) { console.error("Async processing failed:", error); // Queue for retry or alert } }); });

Security Considerations

Verify Webhook Origin

Currently, BLOX webhooks do not include a signature header. To verify authenticity:

  1. Use the Status API — After receiving a webhook, query GET /v1/checkout/:id to verify the status
  2. Use secret URL paths — Add a secret token to your webhook URL (e.g., /webhooks/blox/secret-token-here)
  3. Validate checkout ID — Ensure the checkoutId belongs to your account
app.post("/webhooks/blox/:secret", async (req, res) => { // Verify secret token if (req.params.secret !== process.env.WEBHOOK_SECRET) { return res.status(401).json({ error: "Invalid secret" }); } const { checkoutId, status } = req.body; // Verify with BLOX API const checkout = await getCheckoutFromAPI(checkoutId); if (checkout.status !== status) { console.warn("Status mismatch - possible spoofing attempt"); return res.status(400).json({ error: "Status mismatch" }); } // Process verified webhook await processWebhook(checkoutId, status); res.status(200).json({ received: true }); });

Testing Webhooks

Local Development

Use a tunneling service like ngrok  to expose your local server:

# Start your local server npm run dev # Running on http://localhost:3000 # In another terminal, start ngrok ngrok http 3000 # Use the ngrok URL as your webhookUrl # https://abc123.ngrok.io/webhooks/blox

Sandbox Environment

Create test checkouts in the sandbox environment to trigger webhook events:

  1. Create a checkout with your ngrok webhook URL
  2. Complete the checkout flow in the sandbox UI
  3. Observe the webhook payload in your server logs

Fallback: Polling

If webhooks are not suitable for your use case, you can poll the status endpoint:

async function waitForCompletion(checkoutId: string, timeoutMs = 1200000) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const checkout = await getCheckout(checkoutId); if (["COMPLETED", "FAILED", "CANCELLED"].includes(checkout.status)) { return checkout; } // Wait 5 seconds before next poll await new Promise(resolve => setTimeout(resolve, 5000)); } throw new Error("Checkout polling timeout"); }

Learn more about the status endpoint →

Last updated