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
| Field | Type | Description |
|---|---|---|
checkoutId | string (UUID) | The checkout identifier |
status | string | Final status: COMPLETED, FAILED, or CANCELLED |
amount | string | Payment amount in cents |
timestamp | string (ISO 8601) | When the status changed |
Status Events
| Status | Description | When Triggered |
|---|---|---|
COMPLETED | Payment successful | Funds transferred to merchant wallet |
FAILED | Payment failed | Insufficient balance, network error, etc. |
CANCELLED | Checkout cancelled | Customer 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
2xxstatus 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
passIdempotency
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
| Property | Value |
|---|---|
| Timeout | 10 seconds |
| Retries | None (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
- Respond quickly — Acknowledge the webhook immediately, then process asynchronously
- Use a queue — Push webhook data to a job queue for reliable processing
- Log everything — Store raw webhook payloads for debugging
- 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:
- Use the Status API — After receiving a webhook, query
GET /v1/checkout/:idto verify the status - Use secret URL paths — Add a secret token to your webhook URL (e.g.,
/webhooks/blox/secret-token-here) - Validate checkout ID — Ensure the
checkoutIdbelongs 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/bloxSandbox Environment
Create test checkouts in the sandbox environment to trigger webhook events:
- Create a checkout with your ngrok webhook URL
- Complete the checkout flow in the sandbox UI
- 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");
}