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-Signature header (and X-Affonso-Timestamp)?
  • How do I handle referral.converted, transaction.created, or payout.paid events?
  • 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-Signature is the bare lowercase hex HMAC-SHA256 (64 characters, no sha256= 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. Never JSON.parse / re-serialise first.
  • The event name is the type field 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_WEBHOOK_TOLERANCE_SECONDS 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.

EventDescription
affiliate.createdA new affiliate signs up (usually PENDING)
affiliate.confirmedAn affiliate is confirmed (ACTIVE)
referral.leadA visitor referred by an affiliate becomes a lead
referral.convertedA referral converts to a customer
transaction.createdA sale and its commission are recorded
transaction.approvedA commission is approved
transaction.paidA commission is paid
transaction.rejectedA commission is rejected (for example after a refund)
payout.paidA payout to an affiliate completes
payout.failedA 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

HeaderDescription
X-Affonso-SignatureLowercase hex HMAC-SHA256 of <timestamp>.<raw body>, no prefix
X-Affonso-TimestampUnix 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →