Webhooks, cron and internal endpoints

Webhook paths are excluded from middleware (api/webhooks is in the matcher’s negative lookahead) and authenticate themselves. Handler behaviour is documented in Events.

Provider webhooks

POST /api/webhooks/event-logger

Retell AI call events.

Flow
  1. Parse the payload and extract source, event type and call.call_id.

  2. Ignore inbound calls as a safety measure.

  3. Upsert a WebhookEvent row, unique on (source, eventId).

  4. Dispatch through webhookEventDispatcher.

  5. Return 200.

Registered event types

call_analyzed only. The call_started registration is commented out in lib/events/event-handler.ts, so call_started payloads are stored but not handled.

There is no signature verification on this endpoint. Treat the URL as a secret and add verification before exposing it more widely.

POST /api/webhooks/resend

Resend email events.

Flow
  1. Verify the Svix signature when RESEND_WEBHOOK_SECRET is set; skipped when unset (development).

  2. Upsert a WebhookEvent row, idempotent on the svix-id header.

  3. Extract data.email_id as the correlation key.

  4. Dispatch through ResendWebhookDispatcher.

Handled

email.sent, email.delivered, email.delivery_delayed, email.opened, email.clicked, email.bounced, email.complained, email.unsubscribed, email.received. See Resend email events.

POST /api/webhooks/nooks

Human-dialer call outcomes.

Flow
  1. Optional secret check — Authorization: Bearer or X-Nooks-Signature against NOOKS_WEBHOOK_SECRET.

  2. Upsert a WebhookEvent row with source = "Nooks", idempotent on eventId.

  3. Dispatch through NooksWebhookDispatcher.

Handled

call.logged. See Nooks events.

POST /api/webhooks/hubspot

HubSpot CRM subscription notifications.

Flow
  1. Validate the HubSpot HMAC (or legacy) signature using HUBSPOT_WEBHOOK_CLIENT_SECRET (lib/utils/hubspot/webhook-signature.ts).

  2. Normalise the payload (lib/utils/hubspot/webhook-normalize.ts).

  3. Publish as Crm.HubSpotWebhook to the CRM queue via publishCrmWorkflowEvent.

POST /api/webhooks/telnyx

Telnyx AI assistant call events. Store only — no handlers are registered.

Flow
  1. Require TELNYX_PUBLIC_KEY. Return 503 when it is unset.

  2. Verify the Ed25519 signature (telnyx-timestamp, telnyx-signature-ed25519) over the raw body. Return 401 when the signature is missing, invalid, or older than five minutes.

  3. Upsert a WebhookEvent row with source = "Telnyx", idempotent on data.id.

  4. Dispatch through TelnyxWebhookDispatcher. Unknown event types are stored and return 200.

POST /api/webhooks/elevenlabs

ElevenLabs Agents post-call events. Store only — no dispatcher or handlers.

Flow
  1. Require ELEVENLABS_WEBHOOK_SECRET. Return 503 when it is unset.

  2. Verify the ElevenLabs-Signature HMAC (t=…​,v0=…​) over the raw body. Return 401 when it is missing, malformed, invalid, or more than 30 minutes old.

  3. Parse the JSON body; return 400 when it is not a JSON object.

  4. Upsert a WebhookEvent row with source = "ElevenLabs", idempotent on {type}:{conversation_id}. post_call_audio is stored without its base64 audio.

POST /api/webhooks/five9

Accepts a Five9 payload and stores it for later processing.

This route writes with source = "Nooks" and eventType = "five_9", generating a random eventId and providerCallId. Because the id is random, replays are not idempotent, and the rows share the Nooks source with genuine Nooks events. Filter on eventType to separate them. No dispatch happens — the payload is stored only.

Cron jobs

There are no HTTP cron endpoints. The global jobs are Inngest cron functions, which Inngest invokes through the signed /api/inngest endpoint. Schedules are in UTC.

Function Schedule Effect

phoneNumberDailyResetCron

0 0 * * *

resetDailyCounts() — zeroes PhoneNumbers.dailyCallCount

sendingDomainDailyResetCron

0 0 * * *

Zeroes EmailSendingDomain.dailySentCount and clears dailySentDate

phoneNumberOptimizerCron

15 11 * * 1

Releases numbers unused 3+ days with fewer than 10 lifetime calls, one Inngest step per number; returns { candidates, released, errors }

To run one now, invoke it from the Inngest dashboard (or the Inngest dev server locally).

Internal endpoints

Gated by verifyInternalSecret() — Authorization: Bearer <INTERNAL_API_SECRET>. Fails closed when the secret is unset.

Endpoint Caller

POST /api/campaigns/{id}/broadcast

pg_cron, per call campaign

POST /api/email-campaigns/{id}/broadcast

pg_cron, per email campaign

Inngest

GET|POST|PUT /api/inngest

Serves every registered function through serve() from inngest/next: testContextCollection, processRequest, purchasePhoneNumber, callTarget, callEndedAnalysis, nooksCallAnalysis, emailSend, emailCampaignDuplicate, sequenceExecute.

Authenticated by Inngest’s own signing key (INNGEST_SIGNING_KEY). Adding a function means adding it to the functions array — a function that is not listed will never run. See Workflows.

Diagnostics

GET /api/sentry-example-api

Throws deliberately to verify Sentry wiring, paired with /sentry-example-page.