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.

EventDescription
link.createdA new short link was created
link.clickedA short link was clicked
link.updatedA link's destination or settings changed
link.deletedA link was permanently deleted
page.viewedA bio page was viewed
page.publishedA bio page was published
qr.scannedA QR code was scanned
domain.verifiedA 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.

POST/api/v1/webhooks
Request
curl -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"
  }'
Response — 201 Created
{
  "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:

GET/api/v1/webhooks— list all webhook subscriptions
DELETE/api/v1/webhooks/:id— remove a webhook subscription

Payload 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.

link.clicked payload
{
  "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"
  }
}
link.created payload
{
  "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"
  }
}
page.viewed payload
{
  "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

HeaderDescription
X-Mask-SignatureHMAC-SHA256 hex digest of the request body
X-Mask-EventThe event type (e.g. link.clicked)
X-Mask-Delivery-IdUnique delivery ID for deduplication
Node.js signature verification
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 });
});
Python signature verification
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}, 200

Retry 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:

AttemptDelay
1st retry1 second
2nd retry5 seconds
3rd retry30 seconds
4th retry2 minutes
5th retry10 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.