Agent skill
Affonso Webhooks Skill
Receive and verify Affonso affiliate-program webhooks. Use when setting up Affonso webhook handlers, debugging X-Affonso-Signature verification, or handling events like affiliate.created, referral.converted, transaction.created, transaction.paid, and payout.paid.
Install this skill
npx skills add hookdeck/webhook-skills --skill affonso-webhooks
When to Use This Skill
- How do I receive Affonso webhooks?
- How do I verify the
X-Affonso-Signatureheader (andX-Affonso-Timestamp)? - How do I handle
referral.converted,transaction.created, orpayout.paidevents? - Why is my Affonso webhook signature verification failing?
- Why does verification fail when I hex-decode the Affonso signing secret?
Essential Code (USE THIS)
Affonso sends two headers with every webhook:
X-Affonso-Timestamp: 1704067200
X-Affonso-Signature: 4f2c8a1b9d3e7f0a6c5b8d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
X-Affonso-Signatureis the bare lowercase hex HMAC-SHA256 (64 characters, nosha256=prefix) of"<X-Affonso-Timestamp>.<raw body>".- The key is the endpoint's signing secret (a 64-character hex string), used as a plain string. Do not hex-decode it. It is shown once when you create the endpoint.
- Sign the header value exactly as received, then
., then the raw body bytes. NeverJSON.parse/ re-serialise first. - The event name is the
typefield in the JSON body (affiliate.created,transaction.paid, ...).
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyAffonso(rawBody, timestampHeader, signatureHeader, secret) {
if (!secret || !timestampHeader || !signatureHeader) return false;
if (!/^[0-9a-fA-F]{64}$/.test(signatureHeader)) return false; // validate before decoding
// Optional replay window omitted here (off by default): see "Replay window (opt-in)" below and the examples
const signed = Buffer.concat([Buffer.from(timestampHeader, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
const received = Buffer.from(signatureHeader, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Verification (Python)
import hashlib, hmac, re
def verify_affonso(raw_body: bytes, timestamp_header, signature_header, secret: str) -> bool:
if not secret or not timestamp_header or not signature_header:
return False
if not re.fullmatch(r"[0-9a-fA-F]{64}", signature_header): # validate before decoding
return False
signed = timestamp_header.encode("latin-1") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(signature_header), expected)
Respond 401 when verification fails (missing or empty header, malformed digest, mismatch).
Replay window (opt-in)
Affonso's docs sample rejects timestamps more than 300 seconds from now. The examples leave that off by default: Affonso's own retries (11 attempts over 48 hours) and Hookdeck retries and replays resend the original X-Affonso-Timestamp, so a fixed window would reject legitimate redeliveries. Set AFFONSO_ to a positive number (the timestamp is Unix seconds) to opt in; unset or 0 is off. Use the event id for idempotency instead.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Dispatch on the body's type. Choose the events to send when you create the endpoint.
| Event | Description |
|---|---|
affiliate.created | A new affiliate signs up (usually PENDING) |
affiliate.confirmed | An affiliate is confirmed (ACTIVE) |
referral.lead | A visitor referred by an affiliate becomes a lead |
referral.converted | A referral converts to a customer |
transaction.created | A sale and its commission are recorded |
transaction.approved | A commission is approved |
transaction.paid | A commission is paid |
transaction.rejected | A commission is rejected (for example after a refund) |
payout.paid | A payout to an affiliate completes |
payout.failed | A payout fails |
Others: affiliate.updated, affiliate.deleted, referral.updated, referral.deleted, transaction.updated, transaction.deleted, payout.created, payout.updated, payout.deleted, coupon.created, coupon.deleted. See references/overview.md.
For the full event reference, see Affonso's webhook events.
Important Headers
| Header | Description |
|---|---|
X-Affonso-Signature | Lowercase hex HMAC-SHA256 of <timestamp>.<raw body>, no prefix |
X-Affonso-Timestamp | Unix timestamp in seconds, part of the signed content |
Environment Variables
AFFONSO_WEBHOOK_SECRET=your_64_character_hex_signing_secret # Settings → Webhooks, shown once
AFFONSO_WEBHOOK_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 affonso --path /webhooks/affonso
Reference Materials
- references/overview.md - Affonso webhook concepts, events, payloads, retries
- references/setup.md - Creating an endpoint and finding the signing secret
- references/verification.md - Signature verification details and gotchas