Skip to main content

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:

CategoryEvents
Affiliateaffiliate.registered, affiliate.verified, affiliate.suspended, affiliate.activated, affiliate.profile.updated, affiliate.tier.changed
Campaigncampaign.created, campaign.updated, campaign.activated, campaign.paused, campaign.ended, campaign.affiliate.joined, campaign.affiliate.left
Linklink.created, link.clicked, link.deactivated, link.performance.threshold
Conversionconversion.tracked, conversion.approved, conversion.rejected, conversion.disputed
Commissioncommission.created, commission.earned, commission.pending, commission.approved, commission.paid, commission.adjusted, commission.bulk_updated
Paymentpayment.initiated, payment.processing, payment.completed, payment.failed, payment.refunded
Payoutpayout.batch.created, payout.batch.processing, payout.batch.completed, payout.batch.failed, payout.completed, payout.failed
Applicationapplication.submitted, application.approved, application.rejected
Brandbrand.registered, brand.verified, brand.suspended, brand.product.added, brand.product.updated
Fraudfraud.detected, fraud.confirmed, fraud.resolved, suspicious.activity
Analyticsanalytics.report.ready, performance.milestone, revenue.threshold.reached
Integrationintegration.connected, integration.disconnected, integration.error, integration.sync.completed
Systemsystem.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 OK quickly — 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 offset
  • limit (int, default 50, max 100) — Results per page
  • active_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/endpoints and select the one you need by id.

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 by success, failed, or retrying

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-Signature header 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 delivered
  • 3xx — Redirect not followed (security)
  • 4xx — Client error (URL invalid, auth failed) — not retried
  • 5xx — 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}

Next Steps