webhook-setup

Configure webhooks to receive inbound messages and delivery updates with signature verification.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "webhook-setup" with this command: npx skills add zavudev/zavu-skills/zavudev-zavu-skills-webhook-setup

Webhook Setup

When to Use

Use this skill when setting up webhook endpoints to receive inbound messages, delivery status updates, or template approval notifications from Zavu.

Webhook Types

  • Sender Webhooks: Message events (inbound, delivery status, templates) - configured per sender
  • Project Webhooks: Project-level events (partner invitations) - one per project

Available Events

EventCategoryDescription
message.inboundInboundCustomer sent you a message
conversation.newInboundFirst message from a new contact
message.unsupportedInboundUnsupported message type received
message.reactionInboundCustomer reacted to a message
message.queuedOutboundMessage queued for delivery
message.sentOutboundMessage sent to carrier
message.deliveredOutboundMessage delivered to recipient
message.failedOutboundMessage delivery failed
template.status_changedTemplatesWhatsApp template approval status changed
invitation.status_changedInvitationsPartner invitation status changed

Configure Webhook via SDK

TypeScript - Create Sender with Webhook

const sender = await zavu.senders.create({
  name: "My Sender",
  phoneNumber: "+15551234567",
  webhookUrl: "https://your-app.com/webhooks/zavu",
  webhookEvents: ["message.inbound", "message.delivered", "message.failed"],
});
// Store sender.webhook.secret securely - only shown once!

Update Webhook

await zavu.senders.update({
  senderId: "snd_abc123",
  webhookUrl: "https://new-url.com/webhooks",
  webhookEvents: ["message.inbound"],
  webhookActive: true,
});

Regenerate Secret

const result = await zavu.senders.webhookSecret.regenerate({
  senderId: "snd_abc123",
});
console.log(result.secret); // whsec_new_secret...

Webhook Payload Structure

{
  "id": "evt_1705312200000_abc123",
  "type": "message.inbound",
  "timestamp": 1705312200000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": { }
}

Signature Verification

Header: X-Zavu-Signature: t=<timestamp>,v1=<hmac_sha256>

TypeScript (Express)

import crypto from "crypto";
import express from "express";

const app = express();
app.use("/webhooks/zavu", express.raw({ type: "application/json" }));

function verifyZavuSignature(req: express.Request, secret: string): boolean {
  const header = req.headers["x-zavu-signature"] as string;
  if (!header) return false;

  const parts = header.split(",");
  const timestamp = parseInt(parts.find(p => p.startsWith("t="))!.slice(2));
  const signature = parts.find(p => p.startsWith("v1="))!.slice(3);

  // Reject if older than 5 minutes (replay protection)
  if (Math.floor(Date.now() / 1000) - timestamp > 300) return false;

  const signedPayload = `${timestamp}.${req.body.toString()}`;
  const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post("/webhooks/zavu", (req, res) => {
  if (!verifyZavuSignature(req, process.env.ZAVU_WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  res.status(200).send("OK");

  // Process async
  processEvent(event).catch(console.error);
});

async function processEvent(event: any) {
  switch (event.type) {
    case "message.inbound":
      console.log("Inbound from:", event.data.from, event.data.text);
      break;
    case "message.delivered":
      console.log("Delivered:", event.data.messageId);
      break;
    case "message.failed":
      console.log("Failed:", event.data.messageId, event.data.errorMessage);
      break;
  }
}

Python (Flask)

import hmac, hashlib, time
from flask import Flask, request

app = Flask(__name__)

def verify_zavu_signature(req, secret):
    header = req.headers.get("X-Zavu-Signature")
    if not header:
        return False

    parts = header.split(",")
    timestamp = int(next(p for p in parts if p.startswith("t="))[2:])
    signature = next(p for p in parts if p.startswith("v1="))[3:]

    # Reject if older than 5 minutes
    if int(time.time()) - timestamp > 300:
        return False

    signed_payload = f"{timestamp}.{req.data.decode('utf-8')}"
    expected = hmac.new(
        secret.encode(), signed_payload.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

@app.route("/webhooks/zavu", methods=["POST"])
def handle_webhook():
    if not verify_zavu_signature(request, WEBHOOK_SECRET):
        return "Invalid signature", 401

    event = request.json
    # Process event...
    return "OK", 200

Retry Policy

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes
4th retry1 hour
5th retry4 hours

After 5 retries, delivery is marked as failed.

Best Practices

  1. Return 200 quickly - respond within 30 seconds, process async
  2. Verify signatures - always verify in production
  3. Idempotent handlers - check event.id to skip duplicates
  4. Use raw body - signature is computed on raw body, not parsed JSON
  5. Test with ngrok - expose local server for development

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

zavu-rules

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

whatsapp-templates

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

phone-numbers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

broadcast-campaign

No summary provided by upstream source.

Repository SourceNeeds Review