Affihub
DocsAPI ReferenceWebhooks

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_ID

Subscribe 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/paddle

Pass 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.