Webhooks Guide
Receive real-time HTTP POST notifications when events happen in your Snipy account. Subscribe to any of our 15 event types and build reactive integrations.
Setup
1. Create a webhook endpoint
Set up an HTTPS endpoint on your server that can receive POST requests. The endpoint must return a 2xx status code within 10 seconds to acknowledge receipt.
app.post("/webhook", (req, res) => {
const signature = req.headers["x-snipy-signature"];
const payload = JSON.stringify(req.body);
// Verify signature
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("Invalid signature");
}
const event = req.body;
console.log("Received:", event.event, event.data);
// Handle the event
switch (event.event) {
case "link.clicked":
// Track click
break;
case "subscriber.added":
// Sync to CRM
break;
}
res.status(200).send("OK");
});2. Subscribe to events
Use the API to create a webhook subscription with the events you want to listen for.
curl -X POST https://api.snipy.com/api/webhooks/subscribe \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourserver.com/webhook",
"events": ["link.clicked", "link.created", "subscriber.added"],
"secret": "whsec_your_signing_secret"
}'3. Verify signatures
Every webhook payload is signed with your secret using HMAC-SHA256. The signature is sent in the X-Snipy-Signature header. Always verify signatures to ensure payloads are authentic.
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Retry policy
If your endpoint does not return a 2xx response within 10 seconds, the webhook delivery is retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry (final) | 24 hours |
After 5 failed attempts, the webhook subscription is automatically deactivated. You can reactivate it from the dashboard or via the API.
Payload format
All webhook payloads follow a consistent structure with the event type, timestamp, and event-specific data.
{
"event": "event.type",
"timestamp": "2026-03-04T12:00:00Z",
"data": {
// Event-specific fields
}
}| Header | Description |
|---|---|
Content-Type | application/json |
X-Snipy-Signature | HMAC-SHA256 signature of the raw payload body |
X-Snipy-Event | Event type (e.g. link.clicked) |
X-Snipy-Delivery | Unique delivery ID for deduplication |
Event types
Below are all 15 supported event types with example payloads.
link.createdFired when a new short link is created.
{
"event": "link.created",
"timestamp": "2026-03-04T12:00:00Z",
"data": {
"id": 456,
"alias": "my-link",
"shorturl": "https://snipy.to/my-link",
"longurl": "https://example.com/page",
"userId": 42
}
}link.clickedFired every time a short link receives a click.
{
"event": "link.clicked",
"timestamp": "2026-03-04T12:05:00Z",
"data": {
"linkId": 456,
"alias": "my-link",
"country": "US",
"city": "San Francisco",
"browser": "Chrome",
"platform": "macOS",
"referrer": "twitter.com",
"ip": "203.0.113.1"
}
}link.updatedFired when a link's target URL or settings are modified.
{
"event": "link.updated",
"timestamp": "2026-03-04T14:00:00Z",
"data": {
"id": 456,
"alias": "my-link",
"changes": ["longurl", "password"],
"userId": 42
}
}link.expiredFired when a link reaches its expiration date or max clicks limit.
{
"event": "link.expired",
"timestamp": "2026-03-05T00:00:00Z",
"data": {
"id": 456,
"alias": "my-link",
"reason": "max_clicks",
"totalClicks": 100
}
}qr.createdFired when a new QR code is generated.
{
"event": "qr.created",
"timestamp": "2026-03-04T12:10:00Z",
"data": {
"id": 78,
"name": "Product QR",
"linkedUrl": "https://snipy.to/product",
"userId": 42
}
}qr.scannedFired when a QR code is scanned (triggers alongside link.clicked for the linked URL).
{
"event": "qr.scanned",
"timestamp": "2026-03-04T13:00:00Z",
"data": {
"qrId": 78,
"linkId": 456,
"country": "US",
"browser": "Safari",
"platform": "iOS"
}
}bio.viewedFired when a bio profile page receives a view.
{
"event": "bio.viewed",
"timestamp": "2026-03-04T15:00:00Z",
"data": {
"bioId": 12,
"slug": "johndoe",
"country": "GB",
"referrer": "google.com"
}
}bio.updatedFired when a bio profile's content or settings are changed.
{
"event": "bio.updated",
"timestamp": "2026-03-04T16:00:00Z",
"data": {
"bioId": 12,
"slug": "johndoe",
"changes": ["blocks", "theme"],
"userId": 42
}
}subscriber.addedFired when a new subscriber signs up through a bio newsletter block.
{
"event": "subscriber.added",
"timestamp": "2026-03-04T17:00:00Z",
"data": {
"bioId": 12,
"email": "fan@example.com",
"source": "newsletter_block"
}
}plan.upgradedFired when the user upgrades their subscription plan.
{
"event": "plan.upgraded",
"timestamp": "2026-03-04T10:00:00Z",
"data": {
"userId": 42,
"previousPlan": "Starter",
"newPlan": "Pro",
"billingCycle": "monthly"
}
}product.createdFired when a new digital product is listed in the creator store.
{
"event": "product.created",
"timestamp": "2026-03-04T11:00:00Z",
"data": {
"productId": 99,
"name": "Social Media Template Pack",
"price": 1999,
"currency": "usd",
"userId": 42
}
}product.purchasedFired when a customer purchases a digital product.
{
"event": "product.purchased",
"timestamp": "2026-03-04T18:00:00Z",
"data": {
"productId": 99,
"buyerEmail": "buyer@example.com",
"price": 1999,
"currency": "usd",
"sellerId": 42
}
}tip.receivedFired when a tip is received through a bio profile tip jar.
{
"event": "tip.received",
"timestamp": "2026-03-04T19:00:00Z",
"data": {
"bioId": 12,
"amount": 500,
"currency": "usd",
"tipper": "Anonymous",
"message": "Great content!"
}
}verification.approvedFired when a user's identity verification is approved.
{
"event": "verification.approved",
"timestamp": "2026-03-04T09:00:00Z",
"data": {
"userId": 42,
"verificationType": "identity",
"verifiedAt": "2026-03-04T09:00:00Z"
}
}