Webhooks
Receive real-time HTTP POST notifications when events happen in your workspace. Configure endpoints, choose which events to subscribe to, and verify signatures for secure delivery.
Available events
Subscribe to one or more of the following event types. Each event is delivered as an HTTP POST request to your configured endpoint URL.
| Event | Description |
|---|---|
| link.created | A new short link was created |
| link.clicked | A short link was clicked |
| link.updated | A link's destination or settings changed |
| link.deleted | A link was permanently deleted |
| page.viewed | A bio page was viewed |
| page.published | A bio page was published |
| qr.scanned | A QR code was scanned |
| domain.verified | A custom domain DNS verification completed |
Creating webhook subscriptions
Register a webhook endpoint by providing a URL and the events you want to subscribe to. You must also provide a signing secret that MASK uses to sign each delivery so you can verify authenticity.
/api/v1/webhookscurl -X POST https://mask.pk/api/v1/webhooks \
-H "Authorization: Bearer mk_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/mask",
"events": ["link.created", "link.clicked", "page.viewed"],
"secret": "whsec_your_signing_secret_here"
}'{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/mask",
"events": ["link.created", "link.clicked", "page.viewed"],
"active": true,
"created_at": "2026-03-24T14:00:00Z"
}You can list and delete webhooks using the following endpoints:
/api/v1/webhooks— list all webhook subscriptions/api/v1/webhooks/:id— remove a webhook subscriptionPayload format
Every webhook delivery sends a JSON POST request to your endpoint. The payload includes an event ID, the event type, a timestamp, and event-specific data.
{
"id": "evt_7f3a2b1c4d5e",
"type": "link.clicked",
"timestamp": "2026-03-24T12:34:56Z",
"data": {
"link_id": "lnk_abc123",
"slug": "my-link",
"url": "https://example.com",
"short_url": "https://mask.pk/my-link",
"referrer": "https://twitter.com",
"country": "US",
"city": "San Francisco",
"device": "mobile",
"browser": "Chrome",
"os": "iOS"
}
}{
"id": "evt_8g4b3c2d5f6a",
"type": "link.created",
"timestamp": "2026-03-24T10:00:00Z",
"data": {
"link_id": "lnk_def456",
"slug": "new-campaign",
"url": "https://example.com/campaign",
"short_url": "https://mask.pk/new-campaign",
"domain": "mask.pk",
"tags": ["campaign-q1"],
"created_by": "user_abc123"
}
}{
"id": "evt_9h5c4d3e6g7b",
"type": "page.viewed",
"timestamp": "2026-03-24T15:22:10Z",
"data": {
"page_id": "bp_abc123",
"slug": "jane",
"referrer": "https://instagram.com",
"country": "GB",
"device": "mobile",
"browser": "Safari"
}
}Signature verification
Every webhook delivery includes an X-Mask-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook secret. Always verify this signature before processing the payload to ensure the request is authentic and hasn't been tampered with.
Delivery headers
| Header | Description |
|---|---|
| X-Mask-Signature | HMAC-SHA256 hex digest of the request body |
| X-Mask-Event | The event type (e.g. link.clicked) |
| X-Mask-Delivery-Id | Unique delivery ID for deduplication |
import crypto from "crypto";
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware example
app.post("/webhooks/mask", (req, res) => {
const signature = req.headers["x-mask-signature"];
const isValid = verifyWebhookSignature(
req.rawBody,
signature,
process.env.MASK_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.body;
console.log("Received event:", event.type, event.id);
// Process the event...
res.status(200).json({ received: true });
});import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route("/webhooks/mask", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Mask-Signature")
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return {"error": "Invalid signature"}, 401
event = request.get_json()
print(f"Received event: {event['type']} {event['id']}")
# Process the event...
return {"received": True}, 200Retry policy
Your endpoint must respond with a 2xx status code within 30 seconds. If the request times out or returns a non-2xx status, MASK retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 second |
| 2nd retry | 5 seconds |
| 3rd retry | 30 seconds |
| 4th retry | 2 minutes |
| 5th retry | 10 minutes |
After all 5 retries fail, the delivery is marked as failed. You can view failed deliveries and replay them from Settings → Webhooks in the dashboard. Each retry includes the same X-Mask-Delivery-Id header, so your endpoint can deduplicate by tracking delivery IDs.
Best practices
Respond quickly. Return a 200 immediately and process the event asynchronously. Don't block the response on database writes or external API calls.
Verify signatures. Always validate the X-Mask-Signature header before processing any payload. Use constant-time comparison to prevent timing attacks.
Handle duplicates. Use the X-Mask-Delivery-Id header to deduplicate events. The same event may be delivered more than once during retries.
Use HTTPS. Webhook URLs must use HTTPS. Plain HTTP endpoints are rejected when creating a subscription.