Skip to main content

Webhooks

📋 Copy Prompt
Help me build a webhook receiver for Integify SMS events.

**I want to:**
- Receive SMS_RECEIVED and DELIVERY_REPORT events from Integify
- Verify webhook signatures for security
- Store events in my database

**Context:**
- I'm using Python FastAPI (or Node.js Express — your choice)
- My webhook URL is https://myapp.com/webhooks/integify
- Integify sends POST requests with JSON payloads
- I need to validate that events truly come from Integify

**Please provide:**
1. A complete HTTP endpoint that receives webhooks
2. HMAC-SHA256 signature verification code
3. Error handling (malformed JSON, signature mismatch)
4. Example webhook payload structure
5. How to register the URL in the Integify dashboard

**Constraints:**
- Use standard library only (no external webhook frameworks)
- Signature algorithm: HMAC-SHA256
- Return HTTP 200 on success
- Log errors for debugging

Overview

Webhooks let Integify push real-time events to your server. Instead of polling the API for message status, you register an endpoint and Integify calls it whenever something happens — an SMS is delivered, fails, or a reply is received.

This guide covers setting up a webhook receiver, verifying request signatures, and handling the two main event types.

Event Types

| Event | When it fires | Use case | |-------|--------------|----------| | sms.delivered | SMS confirmed delivered by operator | Update order status, trigger follow-up | | sms.failed | Delivery permanently failed | Retry with different channel | | sms.received | Inbound SMS received through Integify | Two-way SMS flows, opt-outs |

Webhook Payload

Every webhook POST has this structure:

json
{
  "id": "evt_01HXYZ789",
  "type": "sms.delivered",
  "created_at": "2026-04-10T12:01:30Z",
  "data": {
    "message_id": "msg_01HXYZ123456",
    "to": "+2203001234",
    "status": "delivered",
    "delivered_at": "2026-04-10T12:01:28Z"
  }
}

For sms.received events, data looks like:

json
{
  "from": "+2207654321",
  "body": "STOP",
  "received_at": "2026-04-10T12:05:00Z"
}

Additional routing metadata may appear on data as Integify evolves; treat unknown fields as forward-compatible.

Signature Verification

Every request from Integify includes an X-Integify-Signature header. This is an HMAC-SHA256 hash of the raw request body, signed with your webhook secret.

Always verify signatures before processing events. This prevents replay attacks and spoofed requests.

The signature header format:

X-Integify-Signature: sha256=abc123...

Setup

  1. In your Integify dashboard, go to Settings → Webhooks
  2. Click Add Endpoint
  3. Enter your publicly accessible URL (e.g. https://myapp.com/webhooks/integify)
  4. Select the event types you want to receive
  5. Copy the Webhook Secret shown — you'll need it for signature verification

Your endpoint must respond with HTTP 200 within 10 seconds. If it times out or returns a non-2xx status, Integify retries up to 3 times with exponential backoff.

Code Examples

Python (FastAPI)

python
import hashlib
import hmac
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
WEBHOOK_SECRET = "whsec_your_secret_here"

def verify_signature(payload: bytes, header: str) -> bool:
    if not header or not header.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, header)

@app.post("/webhooks/integify")
async def handle_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("X-Integify-Signature", "")

    if not verify_signature(payload, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = await request.json()
    event_type = event.get("type")

    if event_type == "sms.delivered":
        message_id = event["data"]["message_id"]
        print(f"Delivered: {message_id}")
        # update your database here

    elif event_type == "sms.received":
        from_number = event["data"]["from"]
        body = event["data"]["body"]
        print(f"Received from {from_number}: {body}")
        # handle inbound SMS here

    return {"ok": True}

JavaScript (Express)

javascript
const express = require("express");
const crypto = require("crypto");

const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_here";

// Use raw body for signature verification
app.use("/webhooks/integify", express.raw({ type: "application/json" }));

function verifySignature(payload, header) {
  if (!header || !header.startsWith("sha256=")) return false;
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(payload)
      .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(header)
  );
}

app.post("/webhooks/integify", (req, res) => {
  const signature = req.headers["x-integify-signature"] || "";

  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body.toString());

  if (event.type === "sms.delivered") {
    console.log("Delivered:", event.data.message_id);
    // update your database here
  } else if (event.type === "sms.received") {
    console.log(`Received from ${event.data.from}: ${event.data.body}`);
    // handle inbound SMS here
  }

  res.json({ ok: true });
});

Testing with cURL

Simulate a webhook delivery locally:

bash
# Compute signature
BODY='{"id":"evt_test","type":"sms.delivered","data":{"message_id":"msg_test"}}'
SECRET="whsec_your_secret_here"
SIG="sha256=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

# Send test request
curl -X POST http://localhost:8000/webhooks/integify \
  -H "Content-Type: application/json" \
  -H "X-Integify-Signature: $SIG" \
  -d "$BODY"

Troubleshooting

Signature mismatch — Make sure you're verifying against the raw request body (not parsed JSON). Parsing then re-serializing can change whitespace and break the signature.

Endpoint not receiving events — Your URL must be publicly reachable. For local development, use ngrok or a similar tunnel: ngrok http 8000.

Timeouts — Your handler must respond within 10 seconds. Move heavy processing (database writes, external API calls) to a background queue and return 200 immediately.

Missing events — Check the Webhook Logs in your dashboard. Failed deliveries show the response code and body your endpoint returned.

Next Steps

  • Getting Started — Send messages and correlate IDs with webhook events
  • FAQ — Retry logic, event ordering, and delivery questions