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
}
}
JSONVene 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" } }
]
}
JSONThe 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
- Read the action types for every input and result field.
- Learn the question types.
- Read how actions run.