Docs

Quickstart

Create an action, receive its webhooks, answer a question, and read the result.

This guide checks whether a prescription is ready at a pharmacy. You create a Pharmacy status action, receive its webhooks, answer a question, and read the result.

Before you start

You need two secrets from Vene:

  • An API key. It authenticates every request.
  • A webhook signing secret for the HTTPS URL where you want events. It proves an event came from Vene.

Vene shows each secret once. Keep both in your secret manager. The examples show them as <api_key> and <webhook_secret>.

1. Create an action

Send the action type and its input, with a new Idempotency-Key for each action. A retry with the same key after a timeout returns the same action, so the pharmacy never gets a second call.

curl https://api.venehealth.com/v1/actions \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <idempotency_key>" \
  --data @- <<'JSON'
{
  "type": "pharmacy_status",
  "input": {
    "patient": {
      "ref": "pt_123",
      "first_name": "Jane",
      "last_name": "Doe",
      "date_of_birth": "1958-03-14"
    },
    "pharmacy": {
      "name": "CVS Pharmacy",
      "address": {
        "line1": "123 Market St",
        "city": "San Francisco",
        "state": "CA",
        "postal_code": "94103"
      }
    },
    "prescription": {
      "drug_name": "lisinopril",
      "strength": "10 mg",
      "quantity": 30,
      "prescriber": { "first_name": "Ana", "last_name": "Patel", "npi": "1234567893" }
    },
    "include_cash_price": true
  }
}
JSON

Vene answers 201 with the action object. Keep its id. The status is queued, and Vene starts work right away.

2. Receive webhooks

Vene posts an event to your webhook URL when the action asks a question and when it ends. Check the Vene-Signature header against the raw body before you trust it, then answer 2xx within 10 seconds.

import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const TOLERANCE_SECONDS = 5 * 60

/** `Vene-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` */
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) > TOLERANCE_SECONDS) 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)
}

createServer(async (request, response) => {
  let rawBody = ''
  for await (const chunk of request) rawBody += chunk
  const signature = request.headers['vene-signature']
  if (!verifyVeneSignature(rawBody, String(signature ?? ''), '<webhook_secret>')) {
    response.writeHead(400).end()
    return
  }
  const event = JSON.parse(rawBody)
  response.writeHead(200).end() // acknowledge first, then work
  await handleEvent(event) // your code: deduplicate by event.id
}).listen(3000)

Events can arrive more than once and out of order. Deduplicate by event.id, and keep the copy with the newest data.action.updated_at. See Webhooks.

3. Answer a question

Suppose the pharmacist needs the prescription number to find the fill. The call wraps up, and the action moves to waiting_for_answer. Your webhook gets action.question, and the open question has answer: null:

{
  "id": "<question_id>",
  "call_id": "<call_id>",
  "question": "What is the prescription number for the lisinopril?",
  "help": "The pharmacist could not find the fill by name and date of birth.",
  "asked_at": "2026-11-02T18:09:40.000Z",
  "answered_at": null,
  "answer": null,
  "type": "input",
  "path": "prescription.rx_number"
}

An input question names an optional input field you left empty. Your answer is checked by that field's rules and saved into the action's input. Answer by question_id:

curl https://api.venehealth.com/v1/actions/<action_id>/answer \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "answers": [
    { "question_id": "<question_id>", "answer": { "value": "RX4417721" } }
  ]
}
JSON

The action moves back to in_progress and calls again. If you cannot answer, send { "unavailable": true } instead. The action then ends failed with declined_by_you.

4. Read the result

When the action ends, your webhook gets action.ended with the whole action, so you never need to fetch it. You can also read it any time:

curl https://api.venehealth.com/v1/actions/<action_id> \
  -H "Authorization: Bearer <api_key>"
{
  "id": "<action_id>",
  "type": "pharmacy_status",
  "status": "succeeded",
  "input": {
    "patient": { "ref": "pt_123", "first_name": "Jane", "last_name": "Doe", "date_of_birth": "1958-03-14" },
    "pharmacy": {
      "name": "CVS Pharmacy",
      "address": { "line1": "123 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94103" }
    },
    "prescription": {
      "drug_name": "lisinopril",
      "strength": "10 mg",
      "quantity": 30,
      "rx_number": "RX4417721",
      "prescriber": { "first_name": "Ana", "last_name": "Patel", "npi": "1234567893" }
    },
    "include_cash_price": true
  },
  "questions": [
    {
      "id": "<question_id>",
      "call_id": "<call_id>",
      "question": "What is the prescription number for the lisinopril?",
      "help": "The pharmacist could not find the fill by name and date of birth.",
      "asked_at": "2026-11-02T18:09:40.000Z",
      "answered_at": "2026-11-02T18:12:03.000Z",
      "answer": { "value": "RX4417721" },
      "type": "input",
      "path": "prescription.rx_number"
    }
  ],
  "result": {
    "summary": "Lisinopril 10 mg is ready for pickup at CVS on Market St. The cash price is $12.40 for 30 tablets.",
    "failure_reason": null,
    "next_step": null,
    "fill_status": "ready",
    "ready_at": null,
    "hold_reason": null,
    "hold_detail": null,
    "cash_price": { "price": { "amount_cents": 1240, "currency": "USD" }, "quantity": 30 }
  },
  "trail": [
    {
      "id": "<call_id>",
      "call_type": "ask_pharmacy_fill_status",
      "place": {
        "name": "CVS Pharmacy",
        "address": { "line1": "123 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94103" },
        "phone": "+14155550188"
      },
      "started_at": "2026-11-02T18:04:20.000Z",
      "ended_at": "2026-11-02T18:09:32.000Z",
      "ended_reason": "completed"
    },
    {
      "id": "<call_id_2>",
      "call_type": "ask_pharmacy_fill_status",
      "place": {
        "name": "CVS Pharmacy",
        "address": { "line1": "123 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94103" },
        "phone": "+14155550188"
      },
      "started_at": "2026-11-02T18:12:30.000Z",
      "ended_at": "2026-11-02T18:15:02.000Z",
      "ended_reason": "completed"
    }
  ],
  "created_at": "2026-11-02T18:04:11.000Z",
  "updated_at": "2026-11-02T18:15:10.000Z",
  "ended_at": "2026-11-02T18:15:10.000Z"
}

Next

On this page