Webhook signatures

Indibaba signs every outbound webhook delivery with an HMAC-SHA256 of the raw request body. Verify the signature in your receiver before trusting the payload.

Signature format

Each delivery carries an X-Indibaba-Signature header with the format:

http
X-Indibaba-Signature: sha256=<hex-encoded HMAC>

The HMAC is computed as HMAC-SHA256(secret, raw_request_body). The secret is the value returned in secret_plain when the endpoint was created or last rotated.

Pitfall: Verify against the raw request body, not a JSON-reparsed version. Re-serialising changes whitespace, key ordering, or float precision and breaks the signature. Most HTTP frameworks expose the raw body via request.body / req.rawBody / io.ReadAll(r.Body).

Additional headers

Indibaba also sends:

  • X-Indibaba-Event-Type — the event name, e.g. marketing.contact.created.
  • X-Indibaba-Delivery-Id — a UUID per delivery attempt; use this for receiver-side dedupe on retries.
  • X-Indibaba-Timestamp — ISO-8601 UTC. Not part of the signature digest; the receiver can use it for stale- delivery detection.

Verifier samples

Python

python
# Indibaba webhook verifier — Python
import hmac
import hashlib

def verify_indibaba_signature(secret: str, raw_body: bytes, header: str) -> bool:
    """
    `secret` is the value returned in `secret_plain` when the endpoint
    was created (or last rotated).  `raw_body` is the exact bytes
    Indibaba POSTed — do NOT json.loads + json.dumps; whitespace
    matters.  `header` is the `X-Indibaba-Signature` request header,
    formatted as `sha256=<hex>`.
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    received = header.removeprefix("sha256=") if header else ""
    return hmac.compare_digest(expected, received)


# Flask example:
from flask import Flask, request

app = Flask(__name__)
SECRET = "your-stored-secret"

@app.post("/indibaba-webhook")
def webhook():
    sig = request.headers.get("X-Indibaba-Signature", "")
    if not verify_indibaba_signature(SECRET, request.get_data(), sig):
        return ("invalid signature", 401)
    payload = request.get_json()
    print("event_type:", payload["event"])
    return ("ok", 200)

Node.js

javascript
// Indibaba webhook verifier — Node (built-in crypto, no deps)
import crypto from "node:crypto";

function verifyIndibabaSignature(secret, rawBody, header) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const received = (header ?? "").replace(/^sha256=/, "");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received.padEnd(expected.length, "0")),
  ) && received.length === expected.length;
}


// Express example. Use express.raw() so req.body is a Buffer —
// JSON.parse + re-stringify would corrupt the signature.
import express from "express";

const app = express();
const SECRET = "your-stored-secret";

app.post(
  "/indibaba-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["x-indibaba-signature"] ?? "";
    if (!verifyIndibabaSignature(SECRET, req.body, sig)) {
      return res.status(401).send("invalid signature");
    }
    const payload = JSON.parse(req.body.toString("utf8"));
    console.log("event_type:", payload.event);
    res.send("ok");
  },
);

Go

go
// Indibaba webhook verifier — Go (standard library)
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
    "strings"
)

const indibabaSecret = "your-stored-secret"

func verifyIndibabaSignature(secret string, rawBody []byte, header string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    received := strings.TrimPrefix(header, "sha256=")
    return hmac.Equal([]byte(expected), []byte(received))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "read failure", 400)
        return
    }
    sig := r.Header.Get("X-Indibaba-Signature")
    if !verifyIndibabaSignature(indibabaSecret, body, sig) {
        http.Error(w, "invalid signature", 401)
        return
    }
    // ... do something with body ...
    w.Write([]byte("ok"))
}

func main() {
    http.HandleFunc("/indibaba-webhook", webhookHandler)
    http.ListenAndServe(":8080", nil)
}

Ruby

ruby
# Indibaba webhook verifier — Ruby
require "openssl"
require "json"

def verify_indibaba_signature(secret, raw_body, header)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  received = (header || "").sub(/\Asha256=/, "")
  # Constant-time compare; bail on length mismatch.
  return false if received.length != expected.length
  Rack::Utils.secure_compare(expected, received)
end


# Sinatra example:
require "sinatra"

INDIBABA_SECRET = "your-stored-secret"

post "/indibaba-webhook" do
  raw = request.body.read
  sig = request.env["HTTP_X_INDIBABA_SIGNATURE"]
  halt 401, "invalid signature" unless verify_indibaba_signature(
    INDIBABA_SECRET, raw, sig
  )
  payload = JSON.parse(raw)
  puts "event_type: #{payload['event']}"
  "ok"
end

Retries + dedupe

Indibaba retries failed deliveries on an exponential backoff ((60s, 300s, 900s, 1800s); 4 attempts total per delivery). Retries carry the same X-Indibaba-Delivery-Id so idempotent receivers ignore duplicates cleanly. The endpoint auto-disables after 10 consecutive failures across deliveries — fix your receiver and click Resume to re-enable.

Event catalog

Seller-owned endpoints can subscribe to a curated 16-event subset; admin endpoints see the full 21-event catalog plus every seller’s events. The seller subset:

text
marketing.contact.created
marketing.contact.subscribed
marketing.contact.unsubscribed
marketing.suppression.added
marketing.campaign.sent
marketing.campaign.completed
marketing.campaign.failed
marketing.automation.run.completed
marketing.voice.call.completed
marketing.voice.call.failed
marketing.chatbot.session.created
marketing.chatbot.message.user
orders.created
orders.completed
orders.cancelled

Hit GET /v1/seller/growth/webhooks/event-types for the live list — it’s the single source of truth.

Test fire: Once you’ve registered an endpoint, hit POST /v1/seller/growth/webhooks/{id}/test-fire to send a synthetic delivery without waiting for a real event. Useful for verifying your receiver is reachable + signature-verifies cleanly.

Rotating the secret

If you suspect the secret has leaked, rotate it via POST /v1/seller/growth/webhooks/{id}/rotate-secret — the new secret is returned once in secret_plain. The previous secret stops working immediately on rotate; in-flight deliveries signed with the old secret will fail signature verification on your end. Plan the rotation alongside a receiver-side update.