| Event | Trigger |
link.created | A new short link is created in the workspace |
link.updated | A link's destination URL, slug, or settings are modified |
link.clicked | A visitor clicks and resolves a short link (batched delivery) |
link.expired | A link reaches its expiration timestamp |
domain.verified | A custom domain passes DNS CNAME verification |
quota.warning | Workspace usage reaches 80% or 100% of plan limits |
| Field | Description |
| Endpoint URL | The public HTTPS URL of your server that will receive events |
| Event Types | Select one or more events to subscribe to |
| Description | Optional label to identify this endpoint |
1https://api.yourcompany.com/webhooks/shaf
whsec_.... Copy and store it securely in your server environment.whsec_ signing secret is displayed once at creation. Use it to verify HMAC signatures on incoming payloads.link.created payload to your endpoint URL. Verify your server logs a 200 OK response.X-Shaf-Signature HTTP header. Always verify signatures before processing events to prevent spoofed requests.12345678910111213141516171819202122232425262728293031323334import { 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 }) })
2xx range (e.g. 500 Internal Error, connection timeout):200 OK and handle the payload asynchronously using a queue (e.g. BullMQ, AWS SQS).req.body as a raw Buffer (Node.js) or request.get_data() (Python Flask) before JSON parsing.events array in the payload, not just the top-level type field.