Webhooks
Webhooks send real-time HTTP POST notifications to your server when events occur in Affilync. Requires the Pro tier or higher. Webhooks are managed via the /api/webhooks endpoints.
Create a Webhook
curl -X POST https://api.affilync.com/api/webhooks/endpoints \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/affilync",
"events": ["conversion.tracked", "commission.earned"],
"description": "My webhook"
}'
Response (200):
{
"success": true,
"data": {
"id": "wh_e4f5g6",
"url": "https://example.com/webhooks/affilync",
"events": ["conversion.tracked", "commission.earned"],
"active": true,
"secret": "whsec_your_signing_secret",
"description": "My webhook",
"created_at": "2026-03-21T15:00:00Z",
"updated_at": null,
"last_triggered": null,
"failure_count": 0,
"success_count": 0
}
}
Available Events
Affilync supports 40+ webhook event types across multiple categories:
| Category | Events |
|---|---|
| Affiliate | affiliate.registered, affiliate.verified, affiliate.suspended, affiliate.activated, affiliate.profile.updated, affiliate.tier.changed |
| Campaign | campaign.created, campaign.updated, campaign.activated, campaign.paused, campaign.ended, campaign.affiliate.joined, campaign.affiliate.left |
| Link | link.created, link.clicked, link.deactivated, link.performance.threshold |
| Conversion | conversion.tracked, conversion.approved, conversion.rejected, conversion.disputed |
| Commission | commission.created, commission.earned, commission.pending, commission.approved, commission.paid, commission.adjusted, commission.bulk_updated |
| Payment | payment.initiated, payment.processing, payment.completed, payment.failed, payment.refunded |
| Payout | payout.batch.created, payout.batch.processing, payout.batch.completed, payout.batch.failed, payout.completed, payout.failed |
| Application | application.submitted, application.approved, application.rejected |
| Brand | brand.registered, brand.verified, brand.suspended, brand.product.added, brand.product.updated |
| Fraud | fraud.detected, fraud.confirmed, fraud.resolved, suspicious.activity |
| Analytics | analytics.report.ready, performance.milestone, revenue.threshold.reached |
| Integration | integration.connected, integration.disconnected, integration.error, integration.sync.completed |
| System | system.maintenance, system.update, api.limit.warning, api.limit.exceeded |
Get the full catalog of subscribable events via GET /api/webhooks/event-types.
Payload Format
All webhook payloads follow this structure:
{
"event": "conversion.tracked",
"timestamp": "2026-03-21T15:05:00Z",
"data": {
"conversion_id": "conv_j3k4l5",
"affiliate_id": "aff_m6n7o8",
"campaign_id": "camp_abc123",
"amount": "99.99",
"currency": "USD",
"product_id": "prod_xyz789"
}
}
Payloads include event-specific fields in the data object. Each event type has a documented sample payload available via the event-types endpoint.
Signature Verification
Every webhook request includes an X-Webhook-Signature header. Verify it to confirm the request is from Affilync:
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
Compute the HMAC-SHA256 of the raw request body using your webhook secret and compare:
import hmac
import hashlib
import json
def verify_webhook(request_body, signature_header, webhook_secret):
"""Verify webhook signature from X-Webhook-Signature header"""
# signature_header format: "sha256=<hex>"
expected_sig = hmac.new(
key=webhook_secret.encode('utf-8'),
msg=request_body, # Raw body bytes
digestmod=hashlib.sha256
).hexdigest()
expected_header = f"sha256={expected_sig}"
return hmac.compare_digest(expected_header, signature_header)
# In your webhook handler:
# raw_body = await request.body() # FastAPI
# verify_webhook(raw_body, request.headers["X-Webhook-Signature"], your_secret)
Retry Policy
If your endpoint returns a non-2xx status code or times out (30 second limit), Affilync retries with exponential backoff:
Retry Schedule:
- Attempt 1: Immediate
- Attempt 2: ~60 seconds
- Attempt 3: ~120 seconds
- Attempt 4: ~240 seconds
- Attempt 5: ~480 seconds
- Attempt 6: ~960 seconds
- Attempt 7: ~1920 seconds
- Attempt 8+: Up to 1 hour max delay
Maximum 10 total attempts per event. After all retries fail, the event is marked as failed and moved to the dead letter queue. You can manually retry failed deliveries via the API.
Best Practices:
- Respond with
200 OKquickly — process the payload asynchronously - Use the event's timestamp or ID to deduplicate (retries send the same event)
- Monitor failed deliveries in the dashboard and fix endpoint issues promptly
Manage Webhooks
Create Endpoint
POST /api/webhooks/endpoints (Pro tier+)
Create a new webhook endpoint.
Request:
{
"url": "https://example.com/webhooks",
"events": ["conversion.tracked", "commission.earned"],
"description": "Optional description",
"active": true,
"headers": {"X-Custom-Header": "value"}
}
Response: Returns the webhook endpoint with id and secret.
List Endpoints
GET /api/webhooks/endpoints
List all webhook endpoints for your account.
Query Parameters:
skip(int, default 0) — Pagination offsetlimit(int, default 50, max 100) — Results per pageactive_only(bool, default false) — Filter to active endpoints only
Response:
{
"success": true,
"data": {
"endpoints": [
{
"id": "wh_xyz",
"url": "https://example.com/webhooks",
"description": "My webhook",
"events": ["conversion.tracked"],
"active": true,
"secret": "whsec_****1234",
"headers": {},
"created_at": "2026-03-21T15:00:00Z",
"updated_at": null,
"last_triggered": null,
"failure_count": 0,
"success_count": 5
}
],
"total": 1,
"page": 1,
"per_page": 50
}
}
There is no single-endpoint GET. To retrieve the details of a specific webhook endpoint, fetch the full list from
GET /api/webhooks/endpointsand select the one you need byid.
Update Endpoint
PUT /api/webhooks/endpoints/{endpoint_id}
Update the URL, events, description, or status of a webhook endpoint.
Request:
{
"url": "https://example.com/webhooks/v2",
"events": ["conversion.tracked"],
"active": true,
"description": "Updated description"
}
Delete Endpoint
DELETE /api/webhooks/endpoints/{endpoint_id}
Remove a webhook endpoint. Stopped deliveries are not retried.
Get Event Deliveries
GET /api/webhooks/endpoints/{endpoint_id}/events
View the delivery history for a webhook endpoint.
Query Parameters:
skip(int, default 0)limit(int, default 50, max 100)status(string) — Filter bysuccess,failed, orretrying
Response:
{
"success": true,
"data": {
"events": [
{
"id": "del_abc123",
"endpoint_id": "wh_xyz",
"event_type": "conversion.tracked",
"payload": { "conversion_id": "conv_123", ... },
"status": "success",
"attempts": 1,
"response_code": 200,
"response_body": "OK",
"error_message": null,
"created_at": "2026-03-21T15:05:00Z",
"delivered_at": "2026-03-21T15:05:01Z"
}
],
"total": 42,
"page": 1,
"per_page": 50
}
}
Retry Failed Delivery
POST /api/webhooks/endpoints/{endpoint_id}/events/{event_id}/retry
Manually retry a failed webhook delivery.
Response:
{
"success": true,
"message": "Webhook event retry queued",
"data": {
"delivery_id": "del_new456"
}
}
Test Endpoint
POST /api/webhooks/endpoints/{endpoint_id}/test
Send a test webhook to validate your endpoint.
Request:
{
"event_type": "conversion.tracked",
"payload": {
"conversion_id": "test_123",
"amount": "99.99"
}
}
Get Available Event Types
GET /api/webhooks/event-types
Retrieve the full catalog of subscribable webhook events with descriptions and sample payloads.
Response:
{
"success": true,
"data": [
{
"name": "conversion.tracked",
"description": "New conversion has been tracked",
"category": "Conversion",
"sample_payload": {
"conversion_id": "conv_123",
"affiliate_id": "aff_123",
"amount": 99.99
}
},
...
]
}
Get Analytics
GET /api/webhooks/analytics
Get webhook delivery statistics and performance metrics.
Query Parameters:
days(int, default 30, max 365) — Time period for metrics
Response:
{
"success": true,
"data": {
"total_endpoints": 5,
"active_endpoints": 4,
"total_events_sent": 1250,
"successful_deliveries": 1200,
"failed_deliveries": 50,
"success_rate": 96.0,
"average_response_time": 245,
"events_by_type": [
{"event_type": "conversion.tracked", "count": 600},
{"event_type": "commission.earned", "count": 400}
],
"daily_stats": []
}
}
Zapier Integration
Affilync supports Zapier REST Hooks for native Zapier integration.
Subscribe to Event (Zapier)
POST /api/webhooks/subscribe
Called by Zapier when a user creates a Zap trigger. Creates a webhook subscription and returns the subscription ID.
Request:
{
"hookUrl": "https://hooks.zapier.com/hooks/catch/...",
"event": "commission.earned"
}
Response:
{
"id": "wh_zapier123"
}
Unsubscribe (Zapier)
DELETE /api/webhooks/subscribe/{webhook_id}
Called by Zapier when a user disables a Zap. Removes the webhook subscription.
Security Considerations
- Always verify the signature — Check the
X-Webhook-Signatureheader before processing - Use HTTPS only — Webhook URLs must use HTTPS (public domains only)
- Fail fast — Return 200 OK immediately; process asynchronously
- Handle duplicates — Webhook retries may send the same event ID multiple times
- SSRF protection — Webhook URLs are validated to prevent Server-Side Request Forgery attacks
- Secrets — Store your webhook secret securely; never commit it to version control
Error Handling
Common HTTP status codes from webhooks:
200–299— Success, event marked delivered3xx— Redirect not followed (security)4xx— Client error (URL invalid, auth failed) — not retried5xx— Server error — retried with exponential backoff- Timeout (>30s) — Treated as failure, retried
Example: Validating a Conversion Webhook
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
WEBHOOK_SECRET = "whsec_your_secret_here"
@app.post("/webhooks/affilync")
async def handle_webhook(request: Request):
# Get signature from header
signature = request.headers.get("X-Webhook-Signature")
if not signature:
raise HTTPException(status_code=401, detail="Missing signature")
# Get raw body
body = await request.body()
# Verify signature
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
body,
hashlib.sha256
).hexdigest()
expected_header = f"sha256={expected_sig}"
if not hmac.compare_digest(signature, expected_header):
raise HTTPException(status_code=403, detail="Invalid signature")
# Parse payload
import json
payload = json.loads(body)
# Process based on event type
if payload["event"] == "conversion.tracked":
conversion_data = payload["data"]
# Store conversion in your database
print(f"Conversion: {conversion_data['conversion_id']}")
return {"success": True}