After Hours Virtual Receptionist
Call Amelia 24/7: (832) 979-5957

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/meThe business this key belongs to.
GET/callsCalls, newest first. Filters: since, limit, cursor.
GET/calls/{id}One call with summary, transcript and recording link.
GET/bookingsBookings and urgent messages, newest first.
POST/bookingsCreate a booking. Runs the same steps as a booking made on a call.
GET/contactsContacts, when contacts are on for the account (otherwise 404 not_enabled).
POST/hooksSubscribe a URL to one event (REST hooks / Zapier).
GET/hooksList 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.completedAmelia finished a call and wrote the summary.
booking.createdA booking or urgent message was saved (from a call, the text link, or the API).
lead.capturedAmelia saved a caller's name and number as a lead.
message.receivedSomeone texted your business number.
followup.createdA 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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Answer 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:

  1. 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.
  2. REST hooks (for Zapier apps, Make, n8n and custom builds). Subscribe with POST /hooks, unsubscribe with DELETE /hooks/{id}, and use GET /hooks/sample?event=… for test data. Test the connection with GET /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.