Developer API & webhooks
Pull your calls and bookings, create bookings from your own software, and get a signed webhook the moment Amelia finishes a call, books a job, or captures a lead. Works with Zapier, Make, n8n, or your own code.
Authentication
Make a key in your dashboard: Settings → API & integrations → Make a key. Keys start with ahvr_live_ and are shown once. Send it as a Bearer token on every request. A key only sees its own business. Delete a key there and it stops working at once.
curl https://www.afterhoursvirtualreceptionist.com/api/v1/me \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"Errors, limits and paging
All responses are JSON. Dates are ISO 8601 in UTC. Errors always look like this:
{ "error": { "code": "invalid_request", "message": "`since` must be an ISO 8601 date." } }Codes: unauthorized (401), invalid_request / invalid_url (400), not_found / not_enabled (404), rate_limited (429), server_error (500).
Rate limit: 120 requests per minute per key. Every response has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 also has Retry-After.
Lists return { "object": "list", "data": [...], "has_more": true, "next_cursor": "..." }. Pass cursor=next_cursor for the next page. limit is 1–100 (default 25).
Endpoints
Base URL: https://www.afterhoursvirtualreceptionist.com/api/v1
| GET | /me | The business this key belongs to. |
| GET | /calls | Calls, newest first. Filters: since, limit, cursor. |
| GET | /calls/{id} | One call with summary, transcript and recording link. |
| GET | /bookings | Bookings and urgent messages, newest first. |
| POST | /bookings | Create a booking. Runs the same steps as a booking made on a call. |
| GET | /contacts | Contacts, when contacts are on for the account (otherwise 404 not_enabled). |
| POST | /hooks | Subscribe a URL to one event (REST hooks / Zapier). |
| GET | /hooks | List webhook subscriptions. |
| DELETE | /hooks/{id} | Unsubscribe. |
| GET | /hooks/sample?event=… | Recent items shaped like an event's data, for testing. |
List calls since a date
curl "https://www.afterhoursvirtualreceptionist.com/api/v1/calls?since=2026-01-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"Each call:
{
"id": "00000000-0000-4000-8000-000000000001",
"object": "call",
"from_number": "+15555550100",
"to_number": "+15555550199",
"started_at": "2026-01-15T03:12:00.000Z",
"duration_sec": 142,
"outcome": "handled",
"sentiment": "Positive",
"summary": "Caller needs a water heater looked at tomorrow morning. Booked for 9 AM.",
"transcript": "Agent: Thanks for calling...",
"recording_url": null
}Get one call
curl https://www.afterhoursvirtualreceptionist.com/api/v1/calls/CALL_ID \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"List bookings
curl "https://www.afterhoursvirtualreceptionist.com/api/v1/bookings?limit=25" \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"Create a booking
Required: customer_name, customer_phone (US). Optional: customer_email, service, preferred_time (plain words like "Friday 2 PM"), address, notes, sms_consent (true only if the customer agreed to texts). It runs the same steps as a booking Amelia makes on a call: your add-ons, calendar invites to you and the customer (when an email is given), and the booking.created webhook. Returns 201 with the booking.
curl -X POST https://www.afterhoursvirtualreceptionist.com/api/v1/bookings \
-H "Authorization: Bearer ahvr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Jordan Lee",
"customer_phone": "+15555550100",
"service": "AC tune-up",
"preferred_time": "Friday 2 PM",
"address": "123 Main St, Springfield"
}'Contacts
GET /contacts works the same way as the other lists once contacts are on for your account. Before that it returns 404 not_enabled.
Webhooks
Add a URL in Settings → API & integrations → Add a webhook and pick the events you want. The URL must be public and use https. You get a signing secret (whsec_…) once; keep it on your server.
| call.completed | Amelia finished a call and wrote the summary. |
| booking.created | A booking or urgent message was saved (from a call, the text link, or the API). |
| lead.captured | Amelia saved a caller's name and number as a lead. |
| message.received | Someone texted your business number. |
| followup.created | A call or text needs a person and a follow-up was opened on your Follow-ups tab. |
Each delivery is a POST with a JSON body like this:
{
"id": "6f1c2a8e-3b7d-4c55-9a10-2f0d8c1e4b77",
"type": "booking.created",
"created_at": "2026-01-15T03:14:02.000Z",
"business_id": "…",
"data": {
"id": "00000000-0000-4000-8000-000000000002",
"object": "booking",
"call_id": "00000000-0000-4000-8000-000000000001",
"customer_name": "Sample Customer",
"customer_phone": "+15555550100",
"service": "Water heater repair — Time: tomorrow 9 AM",
"status": "booked",
"address": "123 Main St, Springfield",
"out_of_area": null,
"appointment_at": "2026-01-15T15:00:00.000Z",
"appointment_text": "tomorrow 9 AM",
"sms_consent": true,
"created_at": "2026-01-15T03:14:00.000Z",
"source": "call"
}
}Headers sent with every delivery:
Content-Type: application/json
AHVR-Event: booking.created
AHVR-Delivery: <delivery id>
AHVR-Signature: t=1768446842,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdAnswer with any 2xx within 8 seconds. Anything else, or no answer, is retried after 1 min, 5 min, 30 min, 2 h, 12 h — six tries in all. Answer 410 Gone and we turn that endpoint off. The id stays the same on retries and resends, so use it to skip duplicates. The dashboard shows the last 50 deliveries per endpoint with a Resend button.
Verify the signature
v1 is the hex HMAC-SHA256 of <t>.<raw body> using your signing secret. Use the raw body exactly as received, compare in constant time, and reject a t more than 5 minutes old.
Node.js
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Check one by hand
# t and body from a delivery in your logs
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "whsec_YOUR_SECRET"Zapier
Two ways to connect today, no code needed:
- Webhooks by Zapier. Make a Zap with the trigger Webhooks by Zapier → Catch Hook. Copy the URL Zapier gives you, paste it in Settings → API & integrations → Add a webhook, and pick your events. Press Send test so Zapier sees a sample.
- REST hooks (for Zapier apps, Make, n8n and custom builds). Subscribe with
POST /hooks, unsubscribe withDELETE /hooks/{id}, and useGET /hooks/sample?event=…for test data. Test the connection withGET /me.
# subscribe
curl -X POST https://www.afterhoursvirtualreceptionist.com/api/v1/hooks \
-H "Authorization: Bearer ahvr_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://hooks.zapier.com/hooks/standard/123/abc", "event": "call.completed"}'
# → { "object": "hook", "id": "HOOK_ID", "event": "call.completed", ... }
# unsubscribe
curl -X DELETE https://www.afterhoursvirtualreceptionist.com/api/v1/hooks/HOOK_ID \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"
# sample data for the Zap editor
curl "https://www.afterhoursvirtualreceptionist.com/api/v1/hooks/sample?event=booking.created" \
-H "Authorization: Bearer ahvr_live_YOUR_KEY"Questions or a missing endpoint? See integrations or security and compliance.