Webhook triggers: payload and HMAC signing
A webhook trigger lets your system tell an agent "act on this contact now" — for example, call a guest when a booking is confirmed, or WhatsApp a customer when an order ships. You send one signed HTTP request; the agent does the rest.
Set up the trigger first: Sidebar → Agents → open an agent → Triggers → add When an external system notifies us. Step-by-step: Webhook triggers and signing secrets.
The route
POST https://api.voxido.ai/api/v1/agents-v4/{agent_id}/webhookThe URL belongs to the agent. If the agent has two webhook triggers, both share it, and either trigger's secret is accepted.
Sign the request
| Header | Value |
|---|---|
Content-Type | application/json |
X-Voxido-Signature | Lowercase hex HMAC-SHA256 of the raw request body, using the trigger's signing secret as the key |
Rules:
- Don't send the secret itself. Send the HMAC of the body.
- No timestamp, no
sha256=prefix. - Sign the exact bytes you send. Re-formatting the JSON after signing breaks the signature.
- Use the trigger's signing secret — not a Voxido API key. API keys aren't accepted on this route.
Python
import hashlib, hmac, json, requests
secret = "THE_TRIGGER_SIGNING_SECRET"
body = json.dumps({"phone": "+919812345678", "context": {"order_id": "BW-1042"}})
signature = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
requests.post(
"https://api.voxido.ai/api/v1/agents-v4/AGENT_ID/webhook",
data=body,
headers={"Content-Type": "application/json", "X-Voxido-Signature": signature},
)Node.js
import crypto from "node:crypto";
const body = JSON.stringify({ phone: "+919812345678", context: { order_id: "BW-1042" } });
const signature = crypto.createHmac("sha256", process.env.TRIGGER_SECRET).update(body).digest("hex");
await fetch(`https://api.voxido.ai/api/v1/agents-v4/${agentId}/webhook`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Voxido-Signature": signature },
body,
});The payload
{
"contact_id": "c_01H...",
"phone": "+919812345678",
"trigger_source": "booking_confirmed",
"context": {
"reservation_id": "R-12345",
"check_in_date": "2026-05-18"
}
}| Field | Needed? | How it's used |
|---|---|---|
contact_id (or contactId) | One way to name the contact | Used first, if present |
phone (or phone_number, to) or email | If there's no contact_id | Finds the contact in the agent's workspace, or creates one |
trigger_source | No | A label for you. Passed to the agent with the rest. |
context | No | Any object. Passed to the agent with the rest. |
The whole body reaches the agent as event data, so it can use any field you send. If the body has no contact_id, phone or email, the request is accepted but nothing runs.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | {"status": "accepted"} | Signature is valid; the event is queued |
401 | "Invalid signature" | Missing or wrong X-Voxido-Signature |
404 | "No webhook trigger on this agent" | The agent has no webhook trigger, or doesn't exist |
404 | "Agent not found" | The agent was deleted |
429 | "Rate limit exceeded. Please try again later." | More than 30 requests a minute |
200 doesn't mean the agent ran
The work happens after the reply. If the agent isn't Active, the event is dropped quietly. Check the agent's status, then watch Sidebar → Agent activity.
Retries and duplicates
Voxido treats every request as new. If your tool retries a request, the agent runs again. Retry only on 429 or network errors, not on 200.
Turning a sender off
To cut a sender off for good, Regenerate the trigger's signing secret or remove the trigger. Requests signed with the old secret then get 401. Switching the trigger off may not stop a sender that still holds a valid secret, so regenerate the secret as well.
