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:
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.
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
# 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
// 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
// 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
# 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:
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.cancelledHit GET /v1/seller/growth/webhooks/event-types for the live list — it’s the single source of truth.
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.