Appearance
Webhooks
Fyralynx webhooks deliver real-time event notifications to your server as HTTP POST requests. Webhooks are available on the Enterprise tier ($299+/month).
This guide covers: registering an endpoint, the four event types, verifying signatures, handling retries, and testing your integration.
Plan Requirement
Webhooks are available on Enterprise tier only.
Attempting to register a webhook endpoint on a Publisher plan returns HTTP 403 with code ENTERPRISE_TIER_REQUIRED.
Event Types
Fyralynx fires webhooks for four event types:
| Event | When it fires |
|---|---|
link.clicked | Every time a real visitor follows one of your routing URLs. One event per click. |
link.health_changed | When a link's health status changes (e.g. ok → warning, valid → invalid). |
commission_rate.changed | When the Fyralynx ops team publishes a verified commission rate change for a merchant/network. |
account.usage_warning | When your monthly click usage reaches 80% or 100% of your included limit. |
Registering a Webhook Endpoint
Webhook endpoints are managed in the Fyralynx dashboard:
- Go to Settings → Webhooks.
- Click "+ New Endpoint".
- Enter your HTTPS endpoint URL.
- Select which event types to subscribe to.
- Click "Save".
Fyralynx generates a signing secret for the endpoint on creation. Copy and store it securely - it is shown only once. You use this secret to verify that webhook requests come from Fyralynx and not from an attacker.
Requirements for webhook endpoints
- Must accept HTTPS POST requests. HTTP endpoints are rejected.
- Must respond with HTTP 2xx within 10 seconds. Connections that time out or return non-2xx are treated as failures.
- Must be idempotent on
webhookId- Fyralynx delivers with at-least-once semantics. You may receive the same event more than once (see Retry Policy).
Webhook Payload Structure
Every webhook delivery is an HTTP POST with Content-Type: application/json. The request body follows this envelope structure:
json
{
"event": "link.clicked",
"webhookId": "01HX4Z9Y3ABCDEF1234567890AB",
"creatorId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-05-16T14:23:00Z",
"signature": "sha256=8b45e2f1a3c9d7e4b6f8a2c1d5e9f3a7b1c4d8e2f6a0b3c7d1e5f9a2b4c8d0e3",
"data": {
/* event-specific payload - see below */
}
}| Field | Type | Description |
|---|---|---|
event | string | Event type identifier. |
webhookId | UUID | Unique delivery ID. Use this for idempotency checks. |
creatorId | UUID | Your creator account ID. |
deliveredAt | ISO 8601 | When Fyralynx dispatched this delivery. |
signature | string | HMAC-SHA256 signature for verification (see below). |
data | object | Event-specific payload. |
Signature Verification
Every webhook request includes a signature header containing an HMAC-SHA256 digest of the raw request body, using your endpoint's signing secret.
Always verify the signature before processing a webhook payload. Skip signature verification only in local development.
Algorithm
signature = "sha256=" + HMAC-SHA256(signingSecret, rawRequestBodyBytes)The signature is computed over the raw request body bytes - do not parse or normalise the JSON before computing the signature. Use a constant-time comparison to prevent timing attacks.
TypeScript / Node.js
typescript
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyFyralynxSignature(
rawBody: Buffer | string,
signatureHeader: string | null,
signingSecret: string,
): boolean {
if (!signatureHeader) return false;
const expected = 'sha256=' + createHmac('sha256', signingSecret)
.update(typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf8') : rawBody)
.digest('hex');
const actual = signatureHeader;
// Constant-time comparison - prevents timing attacks
if (expected.length !== actual.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(actual));
}Express.js handler example:
typescript
import express from 'express';
const app = express();
// Use express.raw() to get the raw body before JSON parsing
app.post('/webhooks/fyralynx', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-fyralynx-signature'] as string | undefined ?? null;
const isValid = verifyFyralynxSignature(req.body, signature, process.env.FYRALYNX_WEBHOOK_SECRET!);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString('utf8'));
// Idempotency check
if (await isAlreadyProcessed(payload.webhookId)) {
return res.sendStatus(200); // Acknowledge without reprocessing
}
switch (payload.event) {
case 'link.clicked':
await handleLinkClicked(payload.data);
break;
case 'link.health_changed':
await handleHealthChanged(payload.data);
break;
case 'commission_rate.changed':
await handleCommissionRateChanged(payload.data);
break;
case 'account.usage_warning':
await handleUsageWarning(payload.data);
break;
default:
console.warn(`Unknown Fyralynx event: ${payload.event}`);
}
res.sendStatus(200);
});Python (FastAPI)
python
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
def verify_signature(body: bytes, signature: str | None, secret: str) -> bool:
if not signature:
return False
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhooks/fyralynx")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("x-fyralynx-signature")
if not verify_signature(body, signature, FYRALYNX_WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = json.loads(body)
event = payload["event"]
# Idempotency
if await is_already_processed(payload["webhookId"]):
return {"status": "ok"}
if event == "link.clicked":
await handle_link_clicked(payload["data"])
elif event == "link.health_changed":
await handle_health_changed(payload["data"])
elif event == "commission_rate.changed":
await handle_commission_rate_changed(payload["data"])
elif event == "account.usage_warning":
await handle_usage_warning(payload["data"])
return {"status": "ok"}PHP
php
function verify_fyralynx_signature(string $rawBody, ?string $signature, string $secret): bool {
if ($signature === null) return false;
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
// In your webhook handler:
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_FYRALYNX_SIGNATURE'] ?? null;
if (!verify_fyralynx_signature($rawBody, $signature, FYRALYNX_WEBHOOK_SECRET)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);HTTP Headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Fyralynx-Signature | HMAC-SHA256 signature: sha256={hex} |
X-Fyralynx-Event | Event type (e.g. link.clicked) |
X-Fyralynx-Delivery-ID | UUID of this delivery attempt (same as webhookId in body). Use for deduplication. |
X-Fyralynx-Version | Webhook payload version (v1). |
User-Agent | Fyralynx-Webhooks/1.0 |
Event Payloads
link.clicked
Fires for every click tracked on any of your routing URLs. The data object mirrors the ClickRecord analytics schema.
json
{
"event": "link.clicked",
"webhookId": "01HX4Z9Y3ABCDEF1234567890AB",
"creatorId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-05-16T14:23:01Z",
"signature": "sha256=...",
"data": {
"clickId": "7f3a1b2c-9d4e-4f5a-8b6c-1d2e3f4a5b6c",
"linkId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-05-16T14:23:00Z",
"merchantId": "amazon",
"networkId": "Impact",
"region": "US",
"country": "NG",
"device": "android",
"preset": "auto",
"confidence": 87,
"deepLinkUsed": false,
"normalizedEpcUsd": 0.037
}
}Common uses:
- Real-time click logging to your analytics database.
- Triggering CMS post updates when affiliate links are clicked.
- Real-time commission estimation dashboards.
- Alerting on unusual click patterns (potential click fraud).
link.health_changed
Fires when the deep-link validator or redirect-chain tracer detects a change in link health status.
json
{
"event": "link.health_changed",
"webhookId": "01HX4Z9Y3BCDEF1234567890AB",
"creatorId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-05-16T18:00:05Z",
"signature": "sha256=...",
"data": {
"linkId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"previousStatus": "ok",
"currentStatus": "warning",
"deepLinkStatus": "invalid",
"redirectDepth": 3,
"changedAt": "2026-05-16T17:59:50Z"
}
}Possible status transitions:
ok → warning- redirect depth increased, or deep-link TTL expired.ok → critical- 404 detected on destination, or loop detected.warning → ok- health probe resolved successfully.critical → ok- merchant fixed the issue.unknown → validated- first deep-link probe succeeded.
Common uses:
- Alert your team when important affiliate links break.
- Automatically update or flag broken links in your CMS.
- Track deep-link availability trends over time.
commission_rate.changed
Fires when the Fyralynx ops team verifies and publishes a commission rate change for a merchant/network combination.
json
{
"event": "commission_rate.changed",
"webhookId": "01HX4Z9Y3CDEF1234567890ABC",
"creatorId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-05-16T09:00:00Z",
"signature": "sha256=...",
"data": {
"merchantId": "amazon",
"networkId": "CJ",
"regionCode": "US",
"previousRate": 0.03,
"newRate": 0.025,
"effectiveDate": "2026-05-01",
"impactSeverity": "medium",
"notes": "Amazon US reduced Books commission from 3% to 2.5% across all networks.",
"verifiedAt": "2026-04-30T18:00:00Z"
}
}impactSeverity values:
| Value | Meaning |
|---|---|
low | ≤ 0.5% rate change. Minimal EPC impact. |
medium | 0.5%-1% rate change. Noticeable EPC impact. |
high | > 1% rate change or widely-used merchant/network. Significant EPC impact. |
Common uses:
- Alert your content team to review posts promoting the affected merchant.
- Trigger a CMS batch-update to re-route affected links.
- Update internal commission tracking spreadsheets.
- Flag content for disclosure update if commission structure significantly changed.
account.usage_warning
Fires at 80% and again at 100% of your monthly included click limit.
json
{
"event": "account.usage_warning",
"webhookId": "01HX4Z9Y3DEF1234567890ABCD",
"creatorId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-05-20T11:45:00Z",
"signature": "sha256=...",
"data": {
"clicksUsed": 40000,
"clickLimit": 50000,
"pctUsed": 0.80,
"threshold": 0.80,
"billingPeriodEnd": "2026-06-01"
}
}threshold values:
0.80- first warning. 20% of included clicks remain.1.00- limit reached. Free tier: routing is stopped. Paid tier: overage charges begin.
Common uses:
- Alert your ops team to review traffic volumes.
- Trigger an automatic plan upgrade or overage budget approval workflow.
- Pause non-critical link creation to conserve quota.
Retry Policy
Fyralynx retries failed deliveries (non-2xx response or connection timeout) with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (original) | Immediate |
| 2 | 10 seconds |
| 3 | 30 seconds |
| 4 | 2 minutes |
| 5 | 10 minutes |
After 5 failed attempts, the delivery is abandoned and marked as failed. Failed deliveries are visible in your webhook delivery log at Settings → Webhooks → Delivery Log.
Your endpoint must respond within 10 seconds. If your processing logic takes longer, return HTTP 200 immediately and process the event asynchronously:
typescript
app.post('/webhooks/fyralynx', async (req, res) => {
// Verify signature
const isValid = verifyFyralynxSignature(req.body, req.headers['x-fyralynx-signature'], secret);
if (!isValid) return res.sendStatus(401);
// Acknowledge immediately - do not wait for processing
res.sendStatus(200);
// Process asynchronously
const payload = JSON.parse(req.body.toString());
await jobQueue.enqueue({ type: 'fyralynx_webhook', payload });
});Idempotency
Webhooks are delivered at-least-once. You may receive the same event more than once if:
- Your endpoint returned a non-2xx on a previous attempt.
- A network error prevented Fyralynx from receiving your 2xx acknowledgement.
- Cloudflare's edge had a temporary issue.
Every webhook payload includes a webhookId UUID. Store processed webhookId values in your database and skip reprocessing if the ID has been seen:
typescript
async function isAlreadyProcessed(webhookId: string): Promise<boolean> {
return db.exists('processed_webhooks', { webhook_id: webhookId });
}
async function markProcessed(webhookId: string): Promise<void> {
await db.insert('processed_webhooks', {
webhook_id: webhookId,
processed_at: new Date(),
});
}A 24-hour TTL on your processed_webhooks store is sufficient - Fyralynx does not retry failed deliveries beyond ~15 minutes.
Testing Webhooks
Local Development with ngrok or cloudflared
Expose your local server to receive test webhook deliveries:
bash
# Using ngrok
ngrok http 3000
# → Forwarding https://abc123.ngrok.io → http://localhost:3000
# Using cloudflared
cloudflared tunnel --url http://localhost:3000
# → Your quick Tunnel has been created at https://xxx.trycloudflare.comRegister the public URL as a webhook endpoint in the Fyralynx dashboard for testing.
Trigger a Test Event
From Settings → Webhooks, click "Send Test" next to any registered endpoint. Fyralynx sends a synthetic link.clicked event to the endpoint immediately. The synthetic event uses real schema but placeholder data and is labelled with "_test": true in the payload.
Replay Failed Deliveries
From Settings → Webhooks → Delivery Log, find any failed delivery and click "Retry" to re-dispatch it immediately.
Viewing Delivery Logs
The delivery log shows:
- Delivery timestamp.
- HTTP status code returned by your endpoint.
- Response time.
- Request and response bodies.
- Retry count.
Logs are retained for 30 days.
Disabling Webhooks
To temporarily disable an endpoint without deleting it:
- Go to Settings → Webhooks.
- Toggle the endpoint's Active switch to off.
Fyralynx will not deliver events to disabled endpoints. Deliveries that fail while the endpoint is disabled are not retried when you re-enable it - only new events generate new deliveries.
To permanently remove an endpoint:
- Click the endpoint's Delete button.
- Confirm deletion.
The signing secret for a deleted endpoint is immediately invalidated.
Webhook IP Addresses
Fyralynx webhook deliveries originate from Cloudflare Workers. The source IP addresses are dynamic and cannot be allowlisted reliably. Instead, use signature verification (as described above) to validate that webhooks come from Fyralynx.
If your firewall requires explicit source IP allowlisting, contact enterprise@fyralynx.com to discuss dedicated egress IP options for Enterprise contracts.
Webhook Payload Versioning
Webhook payloads are versioned. The current version is v1, indicated by the X-Fyralynx-Version: v1 header.
When Fyralynx adds new optional fields to a payload, the version does not increment - you should write your handlers to ignore unknown fields rather than fail.
When Fyralynx makes a breaking change (removing or renaming a required field, or changing a field's type), the version is incremented to v2. You will receive advance notice of at least 90 days before a breaking version increment.
Best practice: Always use a JSON schema validator that accepts additional unknown properties:
typescript
// Good - will not break if Fyralynx adds new optional fields
const { clickId, linkId, timestamp, merchantId } = payload.data;
// Risky - will break if Fyralynx adds fields not in your strict schema
const data = ClickRecordSchema.strict().parse(payload.data); // throws on unknown fieldsSee Also
- Authentication - API key generation and security
- Rate Limits - API and edge redirector limits
- Getting Started - complete setup walkthrough
- OpenAPI Specification - complete webhook payload schemas in the
webhookssection