Webhooks
An HMAC-signed POST to your URL whenever an offer is listed, verified, changed, updated or expired. Needs a free API key.
How it works
- Register up to 5 HTTPS endpoints per key, each subscribed to any of:
listedverifiedchangedterms_updatedexpiredrelistedincidentincident_resolved(none selected = all). - Every event is one POST per endpoint with
Content-Type: application/json,X-Aifree-Event: offer.<kind>,X-Aifree-Delivery(the event id) andX-Aifree-Signature: t=<unix seconds>,v1=<hex>wherev1 = HMAC_SHA256(secret, t + "." + rawBody). - Answer 2xx within 10 seconds. Anything else is retried 5 more times (1 min, 5 min, 30 min, 2 h, 12 h); 20 failures in a row pause the endpoint. Every attempt is visible below, and any delivery can be replayed.
- Verify the signature and ignore timestamps older than 5 minutes. Events can arrive more than once (replays, retries): key on
id.
Payload
{
"id": "evt_6f1c…",
"type": "offer.verified",
"createdAt": "2026-10-13T09:12:44.120Z",
"data": {
"slug": "groq-free-tier",
"kind": "verified",
"source": "human",
"note": null,
"diff": null,
"offer": { "name": "Groq Free Tier", "provider": "Groq", "status": "verified", "url": "https://aifree.dev/offer/groq-free-tier" },
"offerStatus": "verified",
"alternativesUrl": null
}
}diff is set for terms_updated: { field: { from, to } } over the public facts (headline, what you get, the catch, amount, card and signup requirements, eligibility, limits, expiry, free type, category, vendor URL).
offerStatus is the offer's status when the event was sent. When it is expired, alternativesUrl is the category hub its /offer page now redirects to (else null). New fields are only ever added, never renamed or removed, so ignore keys you do not know; deliveries queued before a field existed (and their replays) do not carry it.
Verify the signature
Node
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyAifreeSignature(header, secret, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'))
}
// Express: app.post('/hooks/aifree', express.raw({ type: '*/*' }), (req, res) => {
// if (!verifyAifreeSignature(req.get('x-aifree-signature'), process.env.AIFREE_WEBHOOK_SECRET, req.body.toString())) return res.sendStatus(401)
// const event = JSON.parse(req.body); /* event.type === 'offer.verified' … */ res.sendStatus(204)
// })Python
import hmac, hashlib, time
def verify_aifree_signature(header: str, secret: str, raw_body: bytes, tolerance=300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))These examples are exercised by our test suite against the same signer that sends your deliveries.
Console
Manage endpoints and inspect deliveries with your key. The same calls are available as GET/POST/DELETE /api/v1/webhooks, GET /api/v1/webhooks/deliveries and POST /api/v1/webhooks/deliveries/{id}/replay (see the API page).