Webhooks

Subscribe to real-time HTTP callbacks for link lifecycle events. Flyn signs every webhook delivery with HMAC-SHA256 so you can verify authenticity.

Available Events

EventDescription
link.clickA short link was clicked. Includes geo, device, browser, and referrer data.
link.createA new link was created. Includes the full link object.
link.updateA link was updated. Includes the updated link object.
link.deleteA link was deleted. Includes the link data as it was before deletion.
link.expiredA link has expired based on its expiresAt date.
domain.verifiedA custom domain was successfully verified.

Webhook Payload

Every webhook delivery is an HTTP POST with a JSON body:

Payload Structure
{
  "event": "link.click",
  "timestamp": 1711929600000,
  "data": {
    // Event-specific payload (link object, click data, etc.)
  }
}

Delivery Headers

FieldTypeDescription
Content-TypestringAlways "application/json"
X-Flyn-SignaturestringHMAC-SHA256 signature: t={timestamp},v1={hex_signature}
X-Flyn-EventstringThe event type (e.g. "link.click")
User-AgentstringAlways "Flyn-Webhook/1.0"

Verifying Signatures

Always verify webhook signatures to ensure the request came from Flyn and was not tampered with. The signature is computed as:

Signature Algorithm
HMAC-SHA256(secret, "{timestamp}.{body}")

// Where:
// - secret = your webhook secret (whsec_...)
// - timestamp = the "t" value from X-Flyn-Signature header
// - body = the raw JSON request body
Node.js Verification Example
import { createHmac } from 'crypto';

function verifyWebhook(req, secret) {
  const sigHeader = req.headers['x-flyn-signature'];
  const [tPart, vPart] = sigHeader.split(',');
  const timestamp = tPart.replace('t=', '');
  const receivedSig = vPart.replace('v1=', '');

  // Reject requests older than 5 minutes (replay protection)
  const age = Date.now() - parseInt(timestamp);
  if (age > 5 * 60 * 1000) {
    throw new Error('Webhook timestamp too old');
  }

  const body = JSON.stringify(req.body);
  const expectedSig = createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  if (receivedSig !== expectedSig) {
    throw new Error('Invalid webhook signature');
  }

  return req.body; // Verified!
}

Limits & Reliability

  • Maximum 10 webhooks per account
  • Delivery timeout: 10 seconds, your endpoint must respond within 10s
  • On failure, the failure_count is incremented
  • Webhooks are automatically disabled after repeated failures
  • Reactivating a webhook resets its failure count
  • Webhook URLs must be public HTTPS/HTTP endpoints (no private/internal IPs)

Create a Webhook

POST/api/webhooks

Request Body

ParameterTypeRequiredDescription
urlstringYesThe HTTPS endpoint URL to receive webhook events
eventsstring[]YesArray of event types to subscribe to
Request Example
curl -X POST https://www.flyn.to/api/webhooks \
  -H "Authorization: Bearer flyn_sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yoursite.com/webhooks/flyn",
    "events": ["link.click", "link.create", "link.delete"]
  }'

Response (201)

Response
{
  "webhook": {
    "id": "webhook-uuid",
    "url": "https://api.yoursite.com/webhooks/flyn",
    "events": ["link.click", "link.create", "link.delete"],
    "secret": "whsec_a1b2c3d4e5f6...",
    "status": "active",
    "createdAt": 1711929600000,
    "failureCount": 0
  }
}

Save the secret immediately. It is shown in full only on creation. Use it to verify webhook signatures.

List Webhooks

GET/api/webhooks

Response Fields

FieldTypeDescription
webhooks[].idstringWebhook UUID
webhooks[].urlstringEndpoint URL
webhooks[].eventsstring[]Subscribed events
webhooks[].secretstringMasked secret (first 8 chars visible)
webhooks[].statusstring"active" or "inactive"
webhooks[].createdAtnumberUnix timestamp (ms)
webhooks[].lastTriggeredAtnumber | nullLast successful delivery timestamp
webhooks[].failureCountnumberConsecutive delivery failures

Update a Webhook

PATCH/api/webhooks/:id

Request Body

ParameterTypeRequiredDescription
urlstringNoNew endpoint URL
eventsstring[]NoReplace subscribed events
statusstringNo"active" or "inactive". Reactivating resets failure count.

Send Test Ping

Sends a test webhook.test event to your endpoint to verify it is receiving and processing deliveries correctly.

POST/api/webhooks/:id

Response (200)

{ "success": true, "status": 200 }

If delivery fails:

{ "success": false, "status": 500, "statusText": "Internal Server Error" }

Delete a Webhook

DELETE/api/webhooks/:id

Response (200)

{ "success": true }

Was this page helpful? Spotted something wrong?