Event webhooks
Configure an event webhook in your workspace console to receive subscribed events. This is an outgoing event delivery; it is separate from payment-provider webhooks and advertising-network postbacks.
Endpoint requirements
Use a publicly reachable HTTPS endpoint on port 443, with a valid certificate and no embedded credentials or URL fragment. Private, loopback, and reserved addresses are rejected. Redirects are not followed.
Store the webhook signing secret in your backend secret manager. Read the exact raw request body before JSON parsing. Verify the signature and timestamp, durably record the delivery, then return a 2xx response promptly. Process slow work asynchronously.
Headers and signature
| Header | Meaning |
|---|---|
x-linkrunway-id | Stable delivery ID for deduplication |
x-linkrunway-timestamp | Unix timestamp in seconds for this attempt |
x-linkrunway-signature | v1= followed by lowercase hex HMAC-SHA256 |
The signed bytes are timestamp + "." + rawBody. The delivery ID is not part of the signature. Use a timing-safe comparison. Reject stale timestamps using a bounded tolerance; the example uses five minutes and requires a synchronized clock.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook(rawBody, timestamp, signature, secret) {
if (!secret || !/^\d+$/.test(timestamp ?? '')) return false;
if (!/^v1=[a-f0-9]{64}$/.test(signature ?? '')) return false;
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds)) return false;
if (Math.abs(Date.now() / 1000 - seconds) > 300) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
const received = Buffer.from(signature.slice(3), 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}Pass a Buffer containing the original bytes, not JSON.stringify() of a parsed object. Verify before trusting any event fields. Apply a reasonable request body limit in your receiver.
Payload
The envelope has version: 1, type: "event.created", and event. The event includes internal id, caller eventId (client events use the stored app: prefix), event type, name, amount, currency, verified, occurredAt, clickId, customerId, externalUserId, attribution, originalEventId, and metadata.
Nullable fields may be null. Attribution, when present, includes link, partner, share, and UTM information. Do not infer verified revenue merely from an event name: inspect verified and the event type, and retain your payment provider as the source of payment truth.
Deduplicate and acknowledge
After signature verification, save the delivery ID in a durable table with a unique constraint and enqueue processing in the same transaction. Return 2xx for an already accepted duplicate. For business-event deduplication across endpoints or replays, also use the event's internal ID and the operation you are performing.
Delivery is at least once and ordering is not guaranteed. A timeout can lead to a retry after your endpoint has committed. Never issue a payout merely because a webhook handler ran again.
Retries and recovery
Any 2xx marks delivery successful. Network failures, 5xx, 408, and 429 are retried with exponential delay and jitter. Other 4xx responses are terminal. A delivery becomes dead after eight attempts. Transport time is bounded to 20 seconds, with a 10-second socket timeout; respond well before that.
Each retry uses a fresh timestamp and signature but the same delivery ID. Monitor deliveries in the console, correct endpoint or signature issues, and retry failed deliveries through the available console controls. Keep your receiver idempotent when replaying.
Test invalid signatures, changed body bytes, stale timestamps, repeated deliveries, and transient endpoint failures before relying on live webhooks.