Webhooks
Webhooks let you receive real-time HTTP notifications when events happen in your Refuel account. Configure a webhook endpoint URL in your dashboard, and Refuel will send POST requests with event payloads whenever something important occurs.
Available Events
| Event | Description |
|---|---|
| conversion.created | A new conversion has been recorded. Fires immediately when a conversion event is received. |
| conversion.approved | A conversion has been approved (either automatically by fraud checks or manually by the company). |
| conversion.rejected | A conversion has been rejected due to fraud flags or manual review. |
| affiliate.created | A new affiliate has signed up through the widget or API. |
| affiliate.link_created | An affiliate has generated a new referral link. |
| payout.processing | A payout has been initiated and is being processed via Stripe. |
| payout.completed | A payout has been successfully transferred to the affiliate. |
| payout.failed | A payout transfer has failed. |
| click.recorded | A new click event has been tracked (high volume -- use sparingly). |
Payload Format
Every webhook request is a POST with a JSON body following this envelope format:
{
"id": "evt_abc123",
"type": "conversion.created",
"createdAt": "2026-03-30T14:22:00Z",
"data": {
// Event-specific payload
}
}conversion.created
{
"id": "evt_abc123",
"type": "conversion.created",
"createdAt": "2026-03-30T14:22:00Z",
"data": {
"conversionId": "conv_xyz",
"orderId": "order_12345",
"orderValue": 9999,
"currency": "USD",
"commissionAmount": 1000,
"affiliatePayout": 800,
"platformFee": 200,
"status": "pending",
"affiliateId": "aff_abc",
"linkCode": "abc123",
"fraudScore": 0.05
}
}conversion.approved
{
"id": "evt_def456",
"type": "conversion.approved",
"createdAt": "2026-03-30T14:25:00Z",
"data": {
"conversionId": "conv_xyz",
"orderId": "order_12345",
"orderValue": 9999,
"affiliatePayout": 800,
"affiliateId": "aff_abc",
"approvedAt": "2026-03-30T14:25:00Z"
}
}payout.completed
{
"id": "evt_ghi789",
"type": "payout.completed",
"createdAt": "2026-03-30T16:00:00Z",
"data": {
"payoutId": "pay_abc",
"affiliateId": "aff_abc",
"amount": 5000,
"currency": "USD",
"stripeTransferId": "tr_...",
"paidAt": "2026-03-30T16:00:00Z"
}
}affiliate.created
{
"id": "evt_jkl012",
"type": "affiliate.created",
"createdAt": "2026-03-30T10:00:00Z",
"data": {
"affiliateId": "aff_new",
"email": "newaff@example.com",
"companyId": "cmp_abc123",
"linkCode": "xyz789"
}
}Signature Verification
Every webhook request includes a signature in the X-Refuel-Signature header. You should always verify this signature to ensure the request is authentic and has not been tampered with.
The signature is an HMAC-SHA256 hash of the raw request body, using your webhook signing secret as the key. Your signing secret is available in the dashboard under Settings → Webhooks.
How it works
- Refuel computes
HMAC-SHA256(webhook_secret, raw_body) - The hex-encoded result is sent in the
X-Refuel-Signatureheader - Your server computes the same HMAC and compares the two values
- If they match, the request is authentic
Verification in Node.js
import crypto from "crypto";
function verifyWebhookSignature(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Verification in Python
import hmac
import hashlib
def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Example Webhook Handler
Here is a complete Express.js webhook handler that verifies the signature and processes different event types.
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = process.env.REFUEL_WEBHOOK_SECRET!;
// Important: use raw body for signature verification
app.post(
"/webhooks/refuel",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-refuel-signature"] as string;
const rawBody = req.body.toString();
// Verify signature
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
console.error("Invalid webhook signature");
return res.status(401).json({ error: "Invalid signature" });
}
// Parse the verified payload
const event = JSON.parse(rawBody);
switch (event.type) {
case "conversion.created":
console.log("New conversion:", event.data.conversionId);
// e.g., send internal Slack notification
break;
case "conversion.approved":
console.log("Conversion approved:", event.data.conversionId);
// e.g., update your internal order records
break;
case "payout.completed":
console.log("Payout sent:", event.data.payoutId);
// e.g., send receipt email to affiliate
break;
case "affiliate.created":
console.log("New affiliate:", event.data.email);
// e.g., add to your CRM
break;
default:
console.log("Unhandled event:", event.type);
}
// Always respond with 200 to acknowledge receipt
res.json({ received: true });
}
);
app.listen(3000, () => console.log("Webhook server running on port 3000"));Best Practices
- 1.Always verify signatures. Never trust a webhook payload without verifying the HMAC signature first.
- 2.Respond quickly. Return a 200 status within 5 seconds. Process heavy work asynchronously with a queue.
- 3.Handle duplicates. Webhook delivery is at-least-once. Use the event
idto deduplicate. - 4.Use HTTPS. Always use an HTTPS endpoint URL for your webhook receiver.
- 5.Monitor failures. Refuel retries failed deliveries with exponential backoff (up to 3 retries over 1 hour). Check your dashboard for delivery logs.