Webhooks
Two directions
Webhooks flow both ways.
Inbound. Stripe and Paddle tell Affihub about payments. Affihub creates commissions.
Outbound. Affihub tells your app about commissions, partners, and payouts. Your app does whatever it needs to.
No third-party services. No Svix. Native delivery with HMAC-SHA256 signing, automatic retries, and delivery logs built in.
Inbound webhooks
Stripe
Point Stripe at this URL:
https://api.affihub.com/webhooks/stripe?program_id=YOUR_PROGRAM_IDSubscribe to two events:
| Event | What Affihub does |
|---|---|
checkout.session.completed |
Creates a commission from client_reference_id or metadata.affihub_ref |
invoice.paid |
Creates a commission from subscription or line item metadata |
Affihub verifies the Stripe-Signature header against your stripe_webhook_secret. No secret, no processing.
Full setup: Stripe Integration
Paddle
Point Paddle at this URL:
https://api.affihub.com/webhooks/paddlePass custom_data.affihub_program with your program ID in each transaction. Subscribe to:
| Event | What Affihub does |
|---|---|
transaction.completed |
Creates a commission from custom_data.affihub_ref |
subscription.canceled |
Flags the commission for review |
adjustment.updated |
Recalculates the commission amount |
Affihub verifies the Paddle-Signature header using HMAC-SHA256. Unverified requests get rejected.
Full setup: Paddle Integration
Outbound webhooks
Your app needs to know when commissions are created, partners sign up, or payouts land. Outbound webhooks handle that.
Create an endpoint
curl -X POST "https://api.affihub.com/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/affihub",
"events": ["*"],
"description": "Production webhook"
}'Response:
{
"id": "whe_a1b2c3d4e5f6",
"url": "https://your-app.com/webhooks/affihub",
"secret": "4f8a2c6e9d1b3f5a7c0e2d4b6a8f1c3e5d7b9a0c2e4f6a8d1b3c5e7f9a0b2d",
"events": ["*"],
"active": true,
"created_at": "2026-04-12T10:00:00Z"
}The secret is shown once. Save it. You will use it to verify every delivery.
Event types
| Event | When it fires |
|---|---|
commission.created |
New commission from a Stripe or Paddle webhook |
commission.updated |
Commission approved, rejected, or recalculated |
partner.created |
New partner added to your program |
partner.updated |
Partner details or status changed |
payout.generated |
Payout records created for a period |
payout.paid |
Payout executed via Stripe Connect |
Subscribe to all with ["*"] or pick specific events.
Payload format
Every delivery is a signed POST request with this body:
{
"event": "commission.created",
"timestamp": "2026-04-12T15:30:00.000Z",
"data": {
"id": "com_abc123",
"partner_id": "par_def456",
"amount": 1980,
"revenue": 9900,
"status": "pending"
}
}Headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Affihub-Signature |
HMAC-SHA256 hex digest of the request body |
X-Affihub-Event |
The event type (e.g. commission.created) |
Verify the signature
Compute HMAC-SHA256 of the raw request body using your endpoint secret. Compare it to the X-Affihub-Signature header.
Node.js:
import crypto from "crypto";
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post("/webhooks/affihub", (req, res) => {
const sig = req.headers["x-affihub-signature"];
if (!verifySignature(req.rawBody, sig, WEBHOOK_SECRET)) {
return res.status(401).send("Bad signature");
}
const event = req.body;
console.log(event.event, event.data);
res.status(200).send("OK");
});Python:
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Retry policy
Failed deliveries retry automatically with exponential backoff.
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | Final attempt. Marked as failed. |
Your endpoint must return a 2xx status code within 5 seconds. Anything else triggers a retry.
Manage endpoints
List all endpoints:
curl "https://api.affihub.com/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY"View endpoint with delivery history:
curl "https://api.affihub.com/api/v1/webhooks/whe_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"Update an endpoint:
curl -X PATCH "https://api.affihub.com/api/v1/webhooks/whe_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"events": ["commission.created", "payout.paid"]}'Disable an endpoint:
curl -X PATCH "https://api.affihub.com/api/v1/webhooks/whe_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'Delete an endpoint:
curl -X DELETE "https://api.affihub.com/api/v1/webhooks/whe_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"SSRF protection
Endpoint URLs must use HTTPS. Affihub blocks delivery to localhost, private IP ranges, and link-local addresses. This is not configurable.
Best practices
Verify every signature. The signature proves the request came from Affihub. Without verification, anyone can POST to your endpoint and fabricate events.
Respond fast. Return 200 immediately. Process the event asynchronously. A slow handler looks like a failed delivery.
Handle duplicates. Use the data.id field for idempotency. Retries can deliver the same event more than once.
Use HTTPS. Required. Plain HTTP endpoints are rejected at creation time.
Keep secrets out of code. Store your signing secret and API key in environment variables.