Docs

Webhooks

Events, signatures, retries, and the 24-hour window.

Vene posts an event to your webhook URL whenever something needs your attention. Each event carries the whole action, so you never need to call back to read it.

Vene sets one webhook URL for your organization and gives you its signing secret once. Production posts only to HTTPS URLs.

Events

TypeSent whendata
action.questionOne or more questions opened. The open ones have answer: null.{ "action": Action }
action.call_startedA call for the action began. Optional to listen to.{ "action": Action, "call": Call }
action.call_endedA call for the action ended, with its result. Optional to listen to.{ "action": Action, "call": Call }
action.endedThe action ended as succeeded, failed, or cancelled.{ "action": Action }
inbound.messageA place called about one of your patients with no open action, and left a message.{ "message": InboundMessage }

You need only action.question, action.ended, and inbound.message. The call events show progress; answer them with 2xx even when you ignore them.

The action's full history is at List events. Besides these, it holds action.callback_received and action.answered, which never reach your webhook. In that list every event carries the action as it is now; a webhook carries it as it was when the event happened.

Each event has an id (evt_...), its type, created_at, and data. The event reference lists every field: Action question, Action call started, Action call ended, Action ended, and Inbound message.

Example

{
  "id": "<event_id>",
  "type": "inbound.message",
  "created_at": "2026-11-04T16:22:05.000Z",
  "data": {
    "message": {
      "id": "<message_id>",
      "patient_ref": "pt_123",
      "call_id": "<call_id>",
      "caller_name": "Rosa at Oakland Heart Center",
      "phone": "+15105550147",
      "message": "Dr. Chen needs a recent lipid panel before the visit on the 10th. Please have the patient get labs this week.",
      "received_at": "2026-11-04T16:22:05.000Z"
    }
  }
}

Verifying the signature

Every request carries a Vene-Signature header:

Vene-Signature: t=<timestamp>,v1=<signature>

t is the time Vene signed the request, in Unix seconds. v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with your signing secret. To verify:

  1. Read the raw request body before any JSON parsing.
  2. Split the header on ,, then each part on its first =.
  3. Refuse the request when t is more than five minutes from your clock.
  4. Compute the HMAC of t, a ., and the raw body, and compare it to v1 in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyVeneSignature(rawBody: string, header: string | undefined, secret: string) {
  if (!header) return false
  const parts = new Map(
    header.split(',').map((part) => {
      const separator = part.indexOf('=')
      return [part.slice(0, separator).trim(), part.slice(separator + 1).trim()] as const
    }),
  )
  const timestamp = Number(parts.get('t'))
  const received = parts.get('v1')
  if (!Number.isSafeInteger(timestamp) || !received) return false
  if (Math.abs(Date.now() / 1000 - timestamp) > 5 * 60) return false
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const a = Buffer.from(received)
  const b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}

Delivery and retries

  • Answer with any 2xx status within 10 seconds. Do slow work after you answer.
  • Vene does not follow redirects. A 3xx counts as a failure.
  • A failed delivery is retried 30 seconds later, then after a delay that doubles each time, up to one hour between attempts.
  • Vene stops retrying 24 hours after the event. An undelivered event then expires. Its action is still readable through the API.
  • Each event is sent once per endpoint after it succeeds. A slow 2xx that times out on Vene's side counts as a failure and is retried, so an event can still arrive twice.

Handling events

Events can arrive more than once, and out of order.

  • Deduplicate by event.id. The same change always carries the same ID, in webhooks and in the events list.
  • Each webhook carries the action as it was when the event happened. Keep the copy with the newest data.action.updated_at.
  • Treat a webhook as a prompt to act. When in doubt, retrieve the action.

On this page