Agent skill
FirstPromoter Webhooks Skill
Receive and authenticate FirstPromoter Webhooks v2. Use when setting up a FirstPromoter webhook endpoint, checking the custom secret header you add to the subscription, debugging 401s, or handling affiliate and referral events like promoter.created, referral.converted, commission.approved and payout.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill firstpromoter-webhooks
FirstPromoter (affiliate and referral management) Webhooks v2 sends a JSON POST when something happens in your account: a promoter signs up, a referral converts, a commission is approved, a payout completes. Requests are not signed. You authenticate them with a secret header that you add to the webhook subscription yourself, and your endpoint compares that header with the value you stored.
Covers Webhooks v2 only: add a custom header to the subscription and enter it here. Legacy webhooks cannot send custom headers.
When to Use This Skill
- How do I receive FirstPromoter webhooks?
- How do I authenticate a FirstPromoter webhook if it has no signature?
- Which header should my endpoint check? (The one you choose on the subscription)
- How do I handle
referral.converted,commission.approvedorpayout.completedevents? - Why does my FirstPromoter webhook endpoint return 401?
Verification (core)
The header name is user-chosen (default here: Authorization) and the expected value is the exact, whole header value, including any Bearer prefix you typed. Look the header up case-insensitively and compare the whole value; don't strip or parse a prefix.
const crypto = require('crypto');
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest(); // equal-length buffers
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyFirstPromoterRequest(headers, secret = process.env.FIRSTPROMOTER_WEBHOOK_SECRET,
name = process.env.FIRSTPROMOTER_WEBHOOK_HEADER || 'Authorization') {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // Node lowercases header names
return Boolean(received) && safeEqual(received, secret);
}
- Missing header or mismatch:
401. Bad JSON:400. Handled:200(any2xxacknowledges). - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, authenticate, then parse. - In Python use
hmac.compare_digeston bytes (encode both sides;strraises on non-ASCII).
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks. Deduplicate on event_id instead of relying on freshness.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Body: {event_. The X-Event-Id and X-Event-Type headers repeat event_id and event_type. Dispatch on event_type; dedupe on event_id.
| Event | Sent When |
|---|---|
promoter.created | A promoter signed up or was created via the API |
promoter.updated | A promoter's account details changed |
promoter.deleted | A promoter was deleted |
promoter.balance.updated | A promoter's earnings, current or paid balance changed |
referral.created | A new referral (lead or customer) was tracked |
referral.updated | A referral's details changed (state, plan, cancellation) |
referral.converted | A referral (lead) became a customer |
referral.cancelled | A referral was cancelled |
referral.deleted | A referral was deleted |
referral.moved | A referral moved to another campaign |
commission.created | A commission was created |
commission.updated | A commission changed (changes lists the fields) |
commission.approved | A commission was approved |
commission.pending | A commission was set to pending |
commission.denied | A commission was denied |
commission.deleted | A commission was deleted |
payout.created | A payout was created |
payout.completed | A payout completed successfully |
payout.failed | A payout failed |
contract_document.signed | A promoter signed a contract document |
More types exist (promoter_campaign.*, payout_method.*, payments_batch.*, other payout.*); see references/overview.md.
Environment Variables
FIRSTPROMOTER_WEBHOOK_HEADER=Authorization # header name you added to the subscription
FIRSTPROMOTER_WEBHOOK_SECRET=Bearer your-secret # exact, whole header value
Local Development
FirstPromoter can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 firstpromoter --path /webhooks/firstpromoter
Use the printed URL as the subscription URL in FirstPromoter.
Resources
- references/overview.md - What the webhooks are, events, payload and delivery
- references/setup.md - Add a Webhooks v2 subscription with a secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests