Agent-readable docs index: /docs/llms.txt. Full docs in one file: /docs/llms-full.txt. Download /docs/docs.zip to grep all markdown files locally.

Configuring Webhook Endpoints & Signature Verification

Webhooks allow your application to receive real-time HTTP POST notifications when events occur in your Shaf workspace — enabling tight integration with automation platforms, analytics pipelines, and custom backends.

Available Webhook Events

EventTrigger
link.createdA new short link is created in the workspace
link.updatedA link's destination URL, slug, or settings are modified
link.clickedA visitor clicks and resolves a short link (batched delivery)
link.expiredA link reaches its expiration timestamp
domain.verifiedA custom domain passes DNS CNAME verification
quota.warningWorkspace usage reaches 80% or 100% of plan limits

Registering an Endpoint

    In the sidebar, navigate to Settings → Webhooks. Click Add Endpoint.

    Configure the Endpoint

    FieldDescription
    Endpoint URLThe public HTTPS URL of your server that will receive events
    Event TypesSelect one or more events to subscribe to
    DescriptionOptional label to identify this endpoint
    Example endpoint:
    https://api.yourcompany.com/webhooks/shaf

    Copy the Signing Secret

    After saving, Shaf generates a shared secret key starting with whsec_.... Copy and store it securely in your server environment.
    The whsec_ signing secret is displayed once at creation. Use it to verify HMAC signatures on incoming payloads.

    Test the Endpoint

    Click Send Test Event to dispatch a sample link.created payload to your endpoint URL. Verify your server logs a 200 OK response.

Verifying Webhook Signatures

Every webhook payload is signed using an HMAC-SHA256 signature passed in the X-Shaf-Signature HTTP header. Always verify signatures before processing events to prevent spoofed requests.
import { Buffer } from 'node:buffer' import crypto from 'node:crypto' export function verifyShafWebhook( payloadString: string, signatureHeader: string, secret: string ): boolean { const expectedSignature = crypto .createHmac('sha256', secret) .update(payloadString) .digest('hex') // Use timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signatureHeader, 'hex'), Buffer.from(expectedSignature, 'hex') ) } // Express.js handler example app.post('/webhooks/shaf', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-shaf-signature'] as string const isValid = verifyShafWebhook(req.body.toString(), signature, process.env.SHAF_WEBHOOK_SECRET!) if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }) } const event = JSON.parse(req.body.toString()) console.log('Received event:', event.type) res.status(200).json({ received: true }) })

Retry Policy

If your server responds with any HTTP status outside the 2xx range (e.g. 500 Internal Error, connection timeout):
Rendering diagram...
Your endpoint must respond within 10 seconds. For slow processing jobs, immediately return 200 OK and handle the payload asynchronously using a queue (e.g. BullMQ, AWS SQS).

Troubleshooting

My endpoint shows as Degraded in the console
This means 5 consecutive delivery attempts failed. Check your server logs for the exact error (non-2xx response or timeout). Once resolved, click Re-enable Endpoint in Settings → Webhooks to resume delivery.
Webhook signature verification fails (HMAC hash mismatch)
Ensure you are computing the HMAC over the raw request body bytes — not a JSON-parsed and re-serialized version. Parsing and re-serializing can alter whitespace or key ordering, producing a different hash. Use req.body as a raw Buffer (Node.js) or request.get_data() (Python Flask) before JSON parsing.
Why do I receive multiple click events in a single webhook payload?
Click events are delivered in micro-batches (groups of clicks within a short window) to prevent high-traffic links from overwhelming your endpoint. Each batch is a single POST request containing an array of click events. Parse the events array in the payload, not just the top-level type field.