Webhooks
Webhooks
Voxa receives two inbound webhooks — one from Stripe and one from ElevenLabs. Both are verified at the Next.js edge, then forwarded into n8n for orchestration. Voxa never returns non-2xx to a webhook caller: all payloads are persisted to the idempotency log first, then handled async, so retries are safe.
Verifying signatures
Stripe uses HMAC-SHA256 with the timestamped signature header stripe-signature. Voxa reads the raw request body via request.text() (NOT parsed JSON) and passes it to stripe.webhooks.constructEvent alongside STRIPE_WEBHOOK_SECRET. If verification fails the endpoint returns 400.
ElevenLabs delivers post-call analytics signed with ELEVENLABS_WEBHOOK_SECRET. Voxa uses constant-time HMAC verification (see app/api/elevenlabs/webhook/route.ts).
const rawBody = await request.text();
const signature = request.headers.get('stripe-signature');
if (!signature) return new Response('Missing signature', { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch {
return new Response('Invalid signature', { status: 400 });
}Always raw, never parsed
request.json() before signature verification re-serializes the body and the resulting hash will not match. The Stripe webhook handler MUST read request.text() first.Stripe events
/api/stripe/webhookAccepts every Stripe event the dashboard is subscribed to. Forwards only the events Voxa orchestrates against.
checkout.session.completedeventoptionalTriggers n8n W1 (onboarding). Voxa upserts the subscriptions row first, then invokes the workflow with { business_id, tier, stripe_customer_id, ... }.
customer.subscription.updatedeventoptionalUpdated locally in the subscriptions table. Not forwarded.
customer.subscription.deletedeventoptionalTriggers n8n W4 (cleanup) — release the Twilio number, delete the EL agent, archive the row.
invoice.payment_failedeventoptionalTriggers n8n W4 (cleanup) — pauses the agent and WhatsApps the owner.
ElevenLabs post-call
/api/elevenlabs/webhookFires 5–15 seconds after the caller hangs up. Forwards to n8n W3 for billing + lead extraction.
{
"type": "post_call_transcription",
"data": {
"conversation_id": "conv_xyz",
"agent_id": "agent_xyz",
"started_at": "2026-05-20T08:14:02Z",
"ended_at": "2026-05-20T08:18:47Z",
"transcript": [
{ "role": "agent", "text": "Namaste, Sharma Dental..." },
{ "role": "user", "text": "Mujhe kal subah appointment chahiye." }
],
"audio_url": "https://elevenlabs.io/...",
"language": "hi",
"caller_id": "+919876543210",
"summary": "Hindi caller booked an appointment for tomorrow morning."
}
}Why forward to n8n?