Why Webhooks Are Better Than Polling
Polling the /orders/:id endpoint every few seconds to check if an order is complete creates unnecessary API load and introduces latency. Webhooks invert this relationship: instead of your system asking "is it done yet?", FoxReload notifies your server the instant an order status changes.
For instant-delivery products like Google Play Gift Cards, the webhook typically fires within 500ms of order placement. For delayed-delivery items (rare for standard gift cards), webhooks are even more critical.
Step 1 โ Create a Webhook Endpoint
Your webhook endpoint must be:
- Publicly accessible over HTTPS
- Able to respond with HTTP 200 within 5 seconds
- Idempotent (safe to call multiple times with the same payload)
// Express.js webhook handler
const express = require("express");
const crypto = require("crypto");
const app = express();
// IMPORTANT: Use raw body parser for webhook โ signature verification requires raw bytes
app.post("/webhooks/foxreload",
express.raw({ type: "application/json" }),
async (req, res) => {
// 1. Verify signature first
const isValid = verifyWebhookSignature(req);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
// 2. Parse payload
const event = JSON.parse(req.body.toString());
// 3. Acknowledge immediately
res.status(200).json({ received: true });
// 4. Process asynchronously
processWebhookEvent(event).catch(console.error);
}
);
Respond with 200 immediately, before any processing. If your handler takes too long and times out, FoxReload will retry โ you could end up processing the same event twice.
Step 2 โ Verify Webhook Signatures
Every webhook from FoxReload includes an X-FoxReload-Signature header. Always verify it before trusting the payload.
const WEBHOOK_SECRET = process.env.FOXRELOAD_WEBHOOK_SECRET;
function verifyWebhookSignature(req) {
const signature = req.headers["x-foxreload-signature"];
if (!signature) return false;
const [timestampPart, hashPart] = signature.split(",");
const timestamp = timestampPart.replace("t=", "");
const receivedHash = hashPart.replace("v1=", "");
// Reject webhooks older than 5 minutes (replay attack prevention)
const webhookAge = Date.now() / 1000 - parseInt(timestamp);
if (webhookAge > 300) return false;
// Compute expected signature
const payload = `${timestamp}.${req.body.toString()}`;
const expectedHash = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedHash, "hex"),
Buffer.from(expectedHash, "hex")
);
}
Step 3 โ Understand Webhook Event Types
| Event | Description |
|---|---|
order.completed |
Order fulfilled, codes available |
order.failed |
Order could not be fulfilled |
order.refunded |
Order refunded by supplier |
stock.low |
Product stock below threshold |
stock.out |
Product is now out of stock |
price.updated |
Wholesale price changed for a product |
Step 4 โ Process Order Events
async function processWebhookEvent(event) {
console.log(`Processing webhook: ${event.type} for ${event.data.order_id}`);
switch (event.type) {
case "order.completed":
await handleOrderCompleted(event.data);
break;
case "order.failed":
await handleOrderFailed(event.data);
break;
case "stock.out":
await handleStockOut(event.data);
break;
case "price.updated":
await handlePriceUpdate(event.data);
break;
default:
console.warn(`Unknown event type: ${event.type}`);
}
}
Handling order.completed
async function handleOrderCompleted(data) {
const { order_id, external_order_id, codes } = data;
const code = codes[0].code;
// Save code to database
await db.orders.update(
{ external_id: external_order_id },
{ code, status: "fulfilled", fulfilled_at: new Date() }
);
// Deliver to customer
const order = await db.orders.findOne({ external_id: external_order_id });
await displayCodeToCustomer(order.user_id, code);
await sendCodeEmail(order.user_email, code, order.product_name);
console.log(`Order ${external_order_id} fulfilled.`);
}
Handling order.failed
async function handleOrderFailed(data) {
const { external_order_id, reason } = data;
// Update order status
await db.orders.update(
{ external_id: external_order_id },
{ status: "failed", failed_reason: reason }
);
// Trigger refund
await issueCustomerRefund(external_order_id);
// Notify customer
await sendFailureEmail(external_order_id);
// Alert operations team
await sendSlackAlert(`Order failed: ${external_order_id} โ ${reason}`);
}
Step 5 โ Register Your Webhook URL
Register your endpoint in the FoxReload dashboard or via API:
POST /api/v1/webhooks
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"url": "https://yourshop.com/webhooks/foxreload",
"events": ["order.completed", "order.failed", "stock.out", "price.updated"],
"active": true
}
Response:
{
"webhook_id": "wh-abc123",
"url": "https://yourshop.com/webhooks/foxreload",
"secret": "whsec_xxxxxxxxxxxxxxxx",
"events": ["order.completed", "order.failed", "stock.out", "price.updated"],
"active": true
}
Save the secret as FOXRELOAD_WEBHOOK_SECRET in your environment. It is shown only once.
Step 6 โ Implement Idempotent Event Processing
FoxReload may deliver the same webhook multiple times (retries after timeouts). Use the event_id to deduplicate:
async function processWebhookEvent(event) {
// Check if already processed
const existing = await db.webhookEvents.findOne({ event_id: event.id });
if (existing) {
console.log(`Duplicate webhook ${event.id} โ skipping.`);
return;
}
// Mark as processed
await db.webhookEvents.create({
event_id: event.id,
type: event.type,
received_at: new Date()
});
// Process the event
await dispatch(event);
}
Step 7 โ Test Webhooks Locally
Use a tunnel tool to expose your local server during development:
# Using ngrok
ngrok http 3000
# Your webhook URL during development:
# https://abc123.ngrok.io/webhooks/foxreload
Register this temporary URL in the FoxReload sandbox dashboard while testing.
Webhook Reliability Checklist
- Endpoint responds with 200 within 5 seconds
- Signature verification implemented and enforced
- Timestamp validation prevents replay attacks (5-minute window)
- Event processing is idempotent (safe to process twice)
- Webhook events stored in database for audit trail
- Dead-letter queue for events that fail processing
- Monitoring alert if no webhooks received for > 30 minutes
See Also
- How to implement instant delivery for Google Play Gift Cards
- API for selling Google Play Gift Cards: key integration points
- How to manage Google Play code inventory
Real-time delivery starts with webhooks. Register your endpoint on the FoxReload platform and eliminate polling from your integration.

