Skip to content

Webhook events

Vauchflow delivers events to your endpoint via HTTP POST using the transactional outbox pattern — events are guaranteed to be delivered at least once, even across API restarts.

Terminal window
curl -X POST https://api.vauchflow.com/v1/webhooks \
-H "Authorization: Bearer vf_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/vauchflow",
"events": ["voucher.redeemed", "campaign.budget_reached"]
}'
# Response includes your signing secret — save it now, it won't be shown again.
EventTrigger
voucher.redeemedA redemption completed successfully
voucher.validatedA validation check was performed
voucher.expiredA voucher passed its expiry date
campaign.startedCampaign start time reached
campaign.endedCampaign end time reached
campaign.budget_reachedmax_redemptions limit hit
redemption.rolled_backA redemption was reversed
fraud.alertVelocity anomaly detected
agent.action.pending_approvalHigh-risk agent action queued for operator review
agent.action.approvedOperator approved a queued agent action
agent.action.rejectedOperator rejected a queued agent action
agent.action.executedApproved agent action successfully executed
agent.action.expiredPending approval expired without operator decision (default 15 min TTL)
{
"id": "evt_01HX...",
"type": "voucher.redeemed",
"timestamp": "2026-04-17T14:30:00Z",
"data": {
"voucher_code": "SAVE20",
"customer_id": "cust_xyz",
"discount_applied": 18.00,
"remaining_redemptions": 498
}
}

Every payload is signed with HMAC-SHA256. Verify to prevent replay attacks:

const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const [timestamp, signature] = signatureHeader.split(',');
const ts = timestamp.replace('t=', '');
const sig = signature.replace('v1=', '');
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
const diff = Math.abs(Date.now() / 1000 - Number(ts));
if (diff > 300) { throw new Error('Timestamp too old — possible replay attack'); }
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

immediate → 5 min → 30 min → 2h → 5h → 10h → every 12h → 72h max

After 72 hours without successful delivery, the event moves to the dead-letter queue, visible in your dashboard for manual replay.

  • Encrypted at rest: signing secrets are encrypted with AES-256-GCM (VF-062) before storage. Plaintext is never persisted.
  • SSRF protection: webhook delivery blocks internal and private network addresses (link-local, loopback, RFC1918) to prevent server-side request forgery (VF-059).
  • Zero-downtime rotation (Starter plans and above): rotate the signing secret without dropping in-flight events. The previous secret remains valid for a configurable overlap window after rotation, giving you time to redeploy receivers (VF-066).