API Reference

REST API

Complete reference for the Epoch Voice AI REST API. 73 endpoints across 15 categories.

Base URL:https://voice.epochdm.com

Authentication

SessionMost endpoints require a NextAuth session cookie. Obtain via POST /api/auth/[...nextauth] (credentials login).
AdminAdmin endpoints require x-admin-key header matching ADMIN_API_KEY env var.
Cron SecretCron endpoints require Authorization: Bearer <CRON_SECRET> header.
Embed TokenWidget token exchange uses an embed token (ept_...) in the request body.
PublicPublic endpoints — no authentication required.

Authentication5 endpoints

POST/api/auth/registerPublic

Create a new user account and organization.

Request Body
json
{
  "name": "Jane Smith",
  "email": "jane@example.com",
  "password": "securepass123",
  "organizationName": "Acme Corp"
}
Response
json
{
  "message": "Registration successful",
  "organizationId": "clx...",
  "userId": "clx..."
}

Password must be at least 8 characters. Sends a welcome email on success.

POST/api/auth/forgot-passwordPublic

Request a password reset link via email.

Request Body
json
{ "email": "jane@example.com" }
Response
json
{ "message": "If an account with that email exists, a reset link has been sent." }

Always returns success to prevent email enumeration. Reset token expires in 1 hour.

POST/api/auth/reset-passwordPublic

Complete a password reset using the emailed token.

Request Body
json
{
  "token": "reset_token_from_email",
  "password": "newSecurePass456"
}
Response
json
{ "message": "Password has been reset successfully." }
PUT/api/auth/passwordSession

Change the current user's password.

Request Body
json
{
  "currentPassword": "oldPass",
  "newPassword": "newPass",
  "forced": false
}
Response
json
{ "success": true }

Set forced=true for admin-required password changes (skips current password check).

GET/api/auth/magic-linkPublic

Authenticate via a one-time magic link token. Redirects to dashboard on success.

Query Params
params
?token=<magic_link_token>
Response
json
Redirect to /dashboard (sets session cookie)

Usage & Billing7 endpoints

GET/api/usageSession

Get the organization's usage snapshot, plan details, call history, and feature flags.

Query Params
params
?days=30  (optional, default 30)
Response
json
{
  "snapshot": {
    "minutesUsed": 142.5,
    "minutesLimit": 500,
    "percentUsed": 28.5,
    "currentCycleStart": "2026-03-01T00:00:00Z",
    "currentCycleEnd": "2026-03-31T23:59:59Z"
  },
  "plan": "PRO",
  "phoneNumber": "+15551234567",
  "assistant": { "id": "...", "name": "Reception AI" },
  "features": {
    "transcripts": true,
    "analytics": true,
    "outbound": true
  },
  "history": [...],
  "recentCalls": [...]
}
GET/api/billing/historySession

Get overage billing records (up to 52 weeks) and current unbilled overage.

Response
json
{
  "records": [
    {
      "id": "...",
      "weekStart": "2026-03-03",
      "weekEnd": "2026-03-09",
      "overageMinutes": 12.5,
      "amountCents": 375,
      "status": "PAID"
    }
  ],
  "currentOverage": {
    "totalOverageMinutes": 5.2,
    "unbilledMinutes": 5.2,
    "estimatedCostCents": 156,
    "overageEnabled": true,
    "rateCentsPerMin": 30
  }
}
POST/api/stripe/checkoutSession

Create a Stripe checkout session for plan upgrade.

Request Body
json
{ "planTier": "PRO" }
Response
json
{ "url": "https://checkout.stripe.com/..." }

Valid tiers: STARTER, ADVANCED, PRO. Returns 400 if attempting a downgrade (use portal instead).

POST/api/stripe/portalSession

Get a link to the Stripe customer billing portal for managing subscriptions.

Request Body
json
{ "returnPath": "/dashboard/billing" }
Response
json
{ "url": "https://billing.stripe.com/..." }
POST/api/subscription/cancelSession

Cancel the current subscription (takes effect at period end).

Request Body
json
{ "acknowledged": true }
Response
json
{
  "success": true,
  "cancelledAt": "2026-03-13T...",
  "effectiveAt": "2026-04-01T...",
  "deletionAt": "2026-04-08T..."
}

Returns 409 if already cancelled.

POST/api/subscription/reactivateSession

Reverse a pending cancellation before the effective date.

Request Body
json
{}
Response
json
{ "success": true }
GET/api/subscription/statusSession

Check current subscription and cancellation status.

Response
json
{
  "plan": "PRO",
  "status": "active",
  "cancelledAt": null,
  "effectiveAt": null,
  "stripeSubscriptionId": "sub_..."
}

Assistants8 endpoints

GET/api/assistantsSession

List all assistants for the organization with call counts.

Response
json
{
  "assistants": [
    {
      "id": "...",
      "vapiAssistantId": "...",
      "name": "Reception AI",
      "status": "ACTIVE",
      "callCount": 47
    }
  ],
  "phoneNumber": "+15551234567"
}
POST/api/assistantsSession

Register a new assistant.

Request Body
json
{
  "vapiAssistantId": "vapi_...",
  "name": "Reception AI"
}
Response
json
{ "assistant": { "id": "...", "name": "Reception AI", ... } }

Enforces plan limits on assistant count. Returns 409 if VAPI assistant ID already registered.

PATCH/api/assistants/[id]Session

Update an assistant's display name.

Request Body
json
{ "name": "New Name" }
Response
json
{ "assistant": { "id": "...", "name": "New Name", ... } }
DELETE/api/assistants/[id]Session

Delete an assistant.

Response
json
{ "success": true }
GET/api/assistant-nameSession

Get the assistant's display name and whether the user can edit it.

Response
json
{
  "name": "Reception AI",
  "defaultName": "Savannah",
  "canEdit": true
}

canEdit is true on Pro plan and above.

PUT/api/assistant-nameSessionPro+

Update the assistant's display name. Propagates to greeting and system prompt.

Request Body
json
{ "name": "Alex" }
Response
json
{ "success": true }

Name must be 2-30 characters.

GET/api/first-message-modeSession

Get the assistant's greeting mode configuration.

Response
json
{
  "mode": "assistant-speaks-first",
  "firstMessage": "Hi! Thanks for calling Acme Corp. How can I help you today?"
}
PUT/api/first-message-modeSession

Update the greeting mode.

Request Body
json
{
  "mode": "assistant-speaks-first",
  "firstMessage": "Welcome! How can I help?"
}
Response
json
{ "success": true }

Valid modes: assistant-speaks-first, assistant-waits-for-user, assistant-speaks-first-with-model-generated-message

Voice & Prompt6 endpoints

GET/api/voiceSession

Get the current voice configuration.

Response
json
{
  "provider": "elevenlabs",
  "voiceId": "pNInz6obpgDQGcFmaJgB",
  "voiceName": "Savannah",
  "canEdit": true
}

canEdit is true on Advanced plan and above.

PUT/api/voiceSessionAdvanced+

Change the assistant's voice. Regenerates persona section in system prompt.

Request Body
json
{
  "voiceId": "pNInz6obpgDQGcFmaJgB",
  "voiceName": "Savannah"
}
Response
json
{ "success": true }
GET/api/voice/catalogPublic

List all available voices with preview URLs. Cached for 24 hours.

Response
json
[
  {
    "id": "pNInz6obpgDQGcFmaJgB",
    "name": "Savannah",
    "gender": "female",
    "desc": "Warm, professional female voice",
    "previewUrl": "https://..."
  }
]
GET/api/promptSession

Get the full system prompt and editing permissions.

Response
json
{
  "systemPrompt": "You are a friendly AI receptionist...",
  "assistantName": "Reception AI",
  "vapiAssistantId": "...",
  "canEdit": true
}

canEdit is true on Advanced plan and above.

PUT/api/promptSessionAdvanced+

Overwrite the entire system prompt.

Request Body
json
{ "systemPrompt": "You are a friendly AI receptionist..." }
Response
json
{ "success": true }

Inline tools are preserved automatically.

POST/api/prompt/regenerateSessionAdvanced+

AI-regenerate specific sections of the system prompt based on business data.

Request Body
json
{ "sections": ["persona", "context"] }
Response
json
{
  "success": true,
  "systemPrompt": "...",
  "regenerated": ["persona", "context"]
}

Defaults to both sections if not specified.

Knowledge Base4 endpoints

GET/api/kbSession

List all knowledge base sections for the active assistant.

Response
json
{
  "assistantId": "...",
  "vapiAssistantId": "...",
  "canEdit": true,
  "sections": [
    {
      "key": "faqs",
      "label": "FAQs",
      "text": "...",
      "vapiFileId": "...",
      "updatedAt": "..."
    }
  ]
}

canEdit is true on Starter plan and above.

PUT/api/kbSessionStarter+

Create or update a knowledge base section. Generates a Markdown file and uploads to VAPI.

Request Body
json
{
  "sectionKey": "faqs",
  "text": "Q: What are your hours?\nA: We're open Mon-Fri 9am-5pm."
}
Response
json
{
  "success": true,
  "section": {
    "key": "faqs",
    "vapiFileId": "...",
    "fileName": "faqs.md"
  }
}
DELETE/api/kbSessionStarter+

Remove a knowledge base section.

Query Params
params
?section=faqs
Response
json
{ "success": true }
POST/api/kb/uploadSession

Upload a file (PDF, DOCX, CSV, TXT) to use as a knowledge base source.

Request Body
json
multipart/form-data:
  file: <binary>
  section: "company-info"
Response
json
{
  "fileId": "...",
  "fileName": "handbook.pdf",
  "fileSize": 245000
}

Max file size: 10MB. Accepted types: PDF, DOCX, CSV, TXT.

Calls4 endpoints

GET/api/calls/[id]Session

Get details for a specific call including transcript (if available on plan).

Response
json
{
  "call": {
    "id": "...",
    "vapiCallId": "...",
    "status": "COMPLETED",
    "startedAt": "2026-03-13T10:00:00Z",
    "endedAt": "2026-03-13T10:05:30Z",
    "durationMinutes": 5.5,
    "customerNumber": "+15559876543",
    "transcript": "...",
    "summary": "..."
  },
  "plan": "PRO",
  "features": { "transcripts": true, "recordings": true }
}

Transcripts and recordings are gated by plan tier.

GET/api/calls/browser-tokenSessionEnterprise

Get a Twilio Client access token for browser-based calling from the dashboard.

Response
json
{
  "token": "eyJ...",
  "identity": "org_...:user_...",
  "phoneNumber": "+15551234567",
  "expiresIn": 3600
}
POST/api/calls/outboundSessionEnterprise

Initiate an AI-powered outbound call to a customer.

Request Body
json
{ "customerNumber": "+15559876543" }
Response
json
{
  "success": true,
  "call": {
    "id": "...",
    "status": "queued",
    "customerNumber": "+15559876543"
  }
}

Validates E.164 phone format. Checks available minutes before dialing.

GET/api/calls/outbound/statusSession

Poll the status of an outbound call.

Query Params
params
?vapiCallId=<string>
Response
json
{
  "id": "...",
  "status": "in-progress",
  "startedAt": "2026-03-13T10:00:00Z",
  "endedAt": null,
  "minutes": 2.3,
  "endReason": null
}

Returns 404 if the call hasn't been recorded by the webhook yet.

SMS2 endpoints

POST/api/sms/sendSessionPro+

Send an SMS message from your business phone number.

Request Body
json
{
  "to": "+15559876543",
  "body": "Hi! Your appointment is confirmed for tomorrow at 2pm."
}
Response
json
{
  "success": true,
  "messageSid": "SM...",
  "status": "queued"
}

Requires active A2P registration (CAMPAIGN_APPROVED status).

GET/api/sms/logsSession

Get paginated SMS message history.

Query Params
params
?page=1&limit=20&direction=INBOUND
Response
json
{
  "logs": [
    {
      "id": "...",
      "direction": "INBOUND",
      "from": "+15559876543",
      "to": "+15551234567",
      "body": "Hello!",
      "status": "received",
      "createdAt": "..."
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 156,
    "totalPages": 8
  }
}

Phone Numbers5 endpoints

GET/api/phone-numbersSession

Get the organization's provisioned phone number and SIP trunk details.

Response
json
{
  "phoneNumber": "+15551234567",
  "vapiPhoneNumberId": "...",
  "twilioPhoneNumberSid": "PN...",
  "hasSipTrunk": true,
  "hasAssistant": true,
  "assistantName": "Reception AI"
}
POST/api/phone-numbersSession

Provision a new phone number via Twilio SIP trunking.

Request Body
json
{ "areaCode": "415" }
Response
json
{
  "success": true,
  "phoneNumber": "+14155551234",
  "vapiPhoneNumberId": "...",
  "twilioPhoneNumberSid": "PN..."
}

Returns 409 if already provisioned. Requires an active assistant.

GET/api/phone-numbers/searchSession

Search available phone numbers by area code or locality.

Query Params
params
?areaCode=415&locality=San+Francisco&limit=5
Response
json
{
  "numbers": [
    { "phoneNumber": "+14155551234", "locality": "San Francisco", "region": "CA" }
  ]
}
GET/api/transfer-numberSession

Get the configured call transfer number.

Response
json
{ "transferNumber": "+15559876543", "canEdit": true }

canEdit is true on Advanced plan and above.

PUT/api/transfer-numberSessionAdvanced+

Set or clear the call transfer number. Updates inline tools and handling instructions.

Request Body
json
{ "transferNumber": "+15559876543" }
Response
json
{ "success": true, "transferNumber": "+15559876543" }

Normalizes to E.164. Omit transferNumber to clear.

Business Settings6 endpoints

GET/api/business-infoSession

Get the organization's business type, description, and website URL.

Response
json
{
  "businessType": "dental_office",
  "businessDescription": "Family dental practice...",
  "websiteUrl": "https://acmedental.com"
}
PUT/api/business-infoSession

Update business info. Surgically updates the system prompt and syncs to VAPI.

Request Body
json
{
  "businessType": "dental_office",
  "businessDescription": "Family dental practice...",
  "websiteUrl": "https://acmedental.com"
}
Response
json
{ "success": true, "businessType": "dental_office", ... }
GET/api/business-hoursSession

Get the organization's business hours and timezone.

Response
json
{
  "businessHours": {
    "monday": { "open": "09:00", "close": "17:00", "closed": false },
    "tuesday": { "open": "09:00", "close": "17:00", "closed": false }
  },
  "timezone": "America/Los_Angeles"
}
PUT/api/business-hoursSession

Update business hours. Surgically updates the hours section in the system prompt.

Request Body
json
{
  "businessHours": { ... },
  "timezone": "America/Los_Angeles"
}
Response
json
{ "success": true, "businessHours": { ... }, "timezone": "..." }
GET/api/business-addressSession

Get the organization's business address.

Response
json
{ "businessAddress": "123 Main St, Suite 100, San Francisco, CA 94102" }
PUT/api/business-addressSession

Update business address. Surgically updates the address in the system prompt.

Request Body
json
{ "businessAddress": "123 Main St, Suite 100, San Francisco, CA 94102" }
Response
json
{ "success": true, "businessAddress": "..." }

Calendar Integration4 endpoints

POST/api/calendar/connectSessionAdvanced+

Connect a Cal.com calendar for AI-powered appointment booking.

Request Body
json
{
  "provider": "CAL_COM",
  "apiKey": "cal_live_...",
  "eventTypeId": 12345
}
Response
json
{ "success": true, "provider": "CAL_COM" }

Validates the API key, creates function tools on the VAPI assistant, and updates the system prompt.

GET/api/calendar/statusSession

Check the current calendar integration status.

Response
json
{
  "connected": true,
  "provider": "CAL_COM",
  "config": { "eventTypeId": 12345 },
  "connectedAt": "2026-03-01T...",
  "canEdit": true
}
POST/api/calendar/disconnectSession

Disconnect the calendar integration. Removes tools from VAPI and updates the prompt.

Request Body
json
{}
Response
json
{ "success": true }
GET/api/calendar/testSession

Run a diagnostic test on the calendar integration (org lookup, credential decryption, API connectivity).

Response
json
{
  "ok": true,
  "timestamp": "2026-03-13T...",
  "steps": [
    { "name": "org_lookup", "ok": true },
    { "name": "plan_check", "ok": true },
    { "name": "credential_decrypt", "ok": true },
    { "name": "cal_com_api", "ok": true }
  ]
}

Widget & Embed Tokens4 endpoints

POST/api/widget/tokenEmbed Token

Exchange an embed token for a short-lived Twilio access token. Used by the widget internally.

Request Body
json
{ "embedToken": "ept_abc123..." }
Response
json
{
  "token": "eyJ...",
  "identity": "org_...:widget_...",
  "phoneNumber": "+15551234567",
  "orgName": "Acme Corp",
  "expiresIn": 3600
}

CORS-enabled. Validates domain whitelist. Token TTL is 1 hour with auto-refresh.

GET/api/embed-tokenSessionPro+

List all active embed tokens for the organization.

Response
json
{
  "tokens": [
    {
      "id": "...",
      "label": "Main Website",
      "tokenPrefix": "ept_abc1...",
      "allowedDomains": ["mysite.com"],
      "lastUsedAt": "2026-03-12T...",
      "createdAt": "2026-02-15T..."
    }
  ]
}
POST/api/embed-tokenSessionPro+

Generate a new embed token. The raw token is returned only once.

Request Body
json
{
  "label": "HubSpot CRM",
  "allowedDomains": ["app.hubspot.com"]
}
Response
json
{
  "token": "ept_a1b2c3d4e5f6...",
  "prefix": "ept_a1b2c3d4",
  "label": "HubSpot CRM"
}

Copy the token immediately — it cannot be retrieved again.

DELETE/api/embed-tokenSession

Revoke an embed token. Takes effect immediately.

Query Params
params
?id=<token_id>
Response
json
{ "revoked": true }

A2P Compliance (SMS Registration)4 endpoints

GET/api/a2p/statusSession

Check A2P (Application-to-Person) SMS registration status.

Response
json
{
  "status": "CAMPAIGN_APPROVED",
  "paid": true,
  "brandType": "SOLE_PROPRIETOR",
  "messagingServiceSid": "MG...",
  "smsUsed": 42,
  "hasSmsSubscription": true
}
POST/api/a2p/initiateSessionPro+

Start or retry A2P brand registration with Twilio.

Request Body
json
{}
Response
json
{ "success": true, "status": "BRAND_PENDING" }

Only allowed if status is NOT_STARTED or FAILED. Requires prior payment via /api/a2p/checkout.

POST/api/a2p/checkoutSessionPro+

Create a Stripe checkout session for A2P registration fee.

Request Body
json
{
  "businessInfo": {
    "businessName": "Acme Corp",
    "businessType": "SOLE_PROPRIETOR",
    "ein": "12-3456789"
  },
  "representativeInfo": {
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@acme.com",
    "phone": "+15551234567"
  },
  "campaignInfo": {
    "useCase": "appointment_reminders",
    "sampleMessages": ["Your appointment is tomorrow at 2pm."]
  }
}
Response
json
{ "url": "https://checkout.stripe.com/..." }
POST/api/a2p/campaignSessionPro+

Submit an SMS campaign for approval after brand is approved.

Request Body
json
{ "campaignInfo": { ... } }
Response
json
{
  "success": true,
  "campaignSid": "QE...",
  "status": "CAMPAIGN_PENDING"
}

Requires brand status to be BRAND_APPROVED first.

Onboarding3 endpoints

GET/api/onboardingSession

Check onboarding status and retrieve saved data.

Response
json
{
  "completed": false,
  "completedAt": null,
  "data": { "businessName": "Acme Corp", "businessType": "dental_office", ... },
  "phoneNumber": null,
  "organizationName": "Acme Corp"
}
POST/api/onboardingSession

Save onboarding data. Set complete=true to trigger provisioning pipeline.

Request Body
json
{
  "onboardingData": {
    "businessName": "Acme Corp",
    "businessType": "dental_office",
    "websiteUrl": "https://acmedental.com",
    "voiceId": "pNInz6obpgDQGcFmaJgB"
  },
  "complete": true
}
Response
json
{
  "success": true,
  "completed": true,
  "phoneNumber": "+14155551234",
  "provisioningErrors": []
}

Returns 400 if onboarding was already completed.

POST/api/onboarding/analyzeSession

AI-analyze a website URL to extract business data for onboarding.

Request Body
json
{ "url": "https://acmedental.com" }
Response
json
{
  "extractedData": {
    "businessName": "Acme Dental",
    "businessType": "dental_office",
    "description": "Family dental practice...",
    "hours": { ... },
    "address": "123 Main St..."
  },
  "businessName": "Acme Dental",
  "pagesScraped": 3,
  "scrapeWarnings": []
}

Only available during onboarding (before completion). Uses Claude AI for analysis.

Webhooks (Incoming)6 endpoints

POST/api/webhooks/vapiPublic

Receives call events from VAPI (call started, ended, transcript, tool calls). Core webhook for minutes tracking and call logging.

Request Body
json
{ "message": { "type": "end-of-call-report", ... } }
Response
json
200 OK (always)

Handles events: call.started, call.ended, end-of-call-report, tool-calls (calendar). Always returns 200.

POST/api/webhooks/stripePublic

Receives Stripe payment events (invoice paid, subscription changes).

Request Body
json
Stripe webhook payload (signature verified)
Response
json
200 OK

Verified via STRIPE_WEBHOOK_SECRET signature.

POST/api/webhooks/twilio/voicePublic

Twilio voice webhook — generates TwiML for incoming/outgoing browser calls.

Request Body
json
Twilio webhook form data
Response
json
TwiML XML
POST/api/webhooks/twilio/statusPublic

Receives Twilio call status updates (ringing, in-progress, completed).

Request Body
json
Twilio webhook form data
Response
json
200 OK
POST/api/webhooks/twilio/sms-inboundPublic

Receives inbound SMS messages from Twilio.

Request Body
json
Twilio webhook form data
Response
json
TwiML XML
POST/api/webhooks/twilio/sms-statusPublic

Receives SMS delivery status updates from Twilio.

Request Body
json
Twilio webhook form data
Response
json
200 OK

Cron Jobs5 endpoints

GET/api/cron/billing-resetCron Secret

Daily cycle reset — resets monthly minute counters at billing period boundaries.

Response
json
{ "reset": 12, "skipped": 450, "errors": 0 }
GET/api/cron/sync-callsCron Secret

Daily sync — pulls latest call data from VAPI for all organizations.

Response
json
{ "synced": 85, "errors": 0 }
GET/api/cron/overage-billingCron Secret

Weekly (Monday 6am EST) — bills organizations for overage minutes used in the past week.

Response
json
{ "billed": 3, "skipped": 40, "errors": 0 }
GET/api/cron/payment-retryCron Secret

Daily — retries failed overage payments.

Response
json
{ "retried": 1, "succeeded": 1, "failed": 0 }
GET/api/cron/a2p-statusCron Secret

Daily — checks and updates A2P registration statuses with Twilio.

Response
json
{ "checked": 5, "updated": 1 }

Machine-Readable Version

The full API reference is also available as structured Markdown for LLMs and automated tools.

Open raw Markdown
markdown
---
title: Epoch Voice AI — REST API Reference
version: 1.0
last_updated: 2026-03-13
base_url: https://voice.epochdm.com
url: https://voice.epochdm.com/docs/api
raw_markdown_url: https://voice.epochdm.com/docs/api/llm.md
---

# Epoch Voice AI — REST API Reference

Base URL: `https://voice.epochdm.com`

## Authentication Methods

| Method | Description |
|--------|-------------|
| **Session** | NextAuth session cookie via `POST /api/auth/[...nextauth]` credentials login |
| **Admin** | `x-admin-key` header matching `ADMIN_API_KEY` env var |
| **Cron** | `Authorization: Bearer <CRON_SECRET>` header |
| **Embed Token** | `ept_...` token in request body (widget token exchange only) |
| **None** | Public endpoint, no auth required |

---

## Authentication

### POST /api/auth/register
- **Auth:** None
- **Body:** `{ name, email, password, organizationName }`
- **Response:** `{ message, organizationId, userId }`
- **Notes:** Password min 8 chars. Sends welcome email.

### POST /api/auth/forgot-password
- **Auth:** None
- **Body:** `{ email }`
- **Response:** `{ message }` (always succeeds to prevent enumeration)

### POST /api/auth/reset-password
- **Auth:** None
- **Body:** `{ token, password }`
- **Response:** `{ message }`

### PUT /api/auth/password
- **Auth:** Session
- **Body:** `{ currentPassword, newPassword, forced? }`
- **Response:** `{ success: true }`

### GET /api/auth/magic-link?token=...
- **Auth:** None (token-based)
- **Response:** Redirect to /dashboard with session cookie

---

## Usage & Billing

### GET /api/usage
- **Auth:** Session
- **Query:** `?days=30`
- **Response:** `{ snapshot: { minutesUsed, minutesLimit, percentUsed, currentCycleStart, currentCycleEnd }, plan, phoneNumber, assistant, features, history, recentCalls }`

### GET /api/billing/history
- **Auth:** Session
- **Response:** `{ records: [{ weekStart, weekEnd, overageMinutes, amountCents, status }], currentOverage: { totalOverageMinutes, unbilledMinutes, estimatedCostCents, overageEnabled, rateCentsPerMin } }`

### POST /api/stripe/checkout
- **Auth:** Session
- **Body:** `{ planTier: "STARTER" | "ADVANCED" | "PRO" }`
- **Response:** `{ url }`

### POST /api/stripe/portal
- **Auth:** Session
- **Body:** `{ returnPath? }`
- **Response:** `{ url }`

### POST /api/subscription/cancel
- **Auth:** Session
- **Body:** `{ acknowledged: true }`
- **Response:** `{ success, cancelledAt, effectiveAt, deletionAt }`

### POST /api/subscription/reactivate
- **Auth:** Session
- **Response:** `{ success: true }`

### GET /api/subscription/status
- **Auth:** Session
- **Response:** `{ plan, status, cancelledAt, effectiveAt, stripeSubscriptionId }`

---

## Assistants

### GET /api/assistants
- **Auth:** Session
- **Response:** `{ assistants: [{ id, vapiAssistantId, name, status, callCount }], phoneNumber }`

### POST /api/assistants
- **Auth:** Session
- **Body:** `{ vapiAssistantId, name }`
- **Response:** `{ assistant }` (201)

### PATCH /api/assistants/[id]
- **Auth:** Session
- **Body:** `{ name }`
- **Response:** `{ assistant }`

### DELETE /api/assistants/[id]
- **Auth:** Session
- **Response:** `{ success: true }`

### GET /api/assistant-name
- **Auth:** Session
- **Response:** `{ name, defaultName, canEdit }` (canEdit: Pro+)

### PUT /api/assistant-name
- **Auth:** Session | **Plan:** Pro+
- **Body:** `{ name }` (2-30 chars)
- **Response:** `{ success: true }`

### GET /api/first-message-mode
- **Auth:** Session
- **Response:** `{ mode, firstMessage }`

### PUT /api/first-message-mode
- **Auth:** Session
- **Body:** `{ mode, firstMessage? }`
- **Response:** `{ success: true }`
- **Modes:** assistant-speaks-first, assistant-waits-for-user, assistant-speaks-first-with-model-generated-message

---

## Voice & Prompt

### GET /api/voice
- **Auth:** Session
- **Response:** `{ provider, voiceId, voiceName, canEdit }` (canEdit: Advanced+)

### PUT /api/voice
- **Auth:** Session | **Plan:** Advanced+
- **Body:** `{ voiceId, voiceName? }`
- **Response:** `{ success: true }`

### GET /api/voice/catalog
- **Auth:** None
- **Response:** `[{ id, name, gender, desc, previewUrl }]`
- **Notes:** Cached 24 hours

### GET /api/prompt
- **Auth:** Session
- **Response:** `{ systemPrompt, assistantName, vapiAssistantId, canEdit }` (canEdit: Advanced+)

### PUT /api/prompt
- **Auth:** Session | **Plan:** Advanced+
- **Body:** `{ systemPrompt }`
- **Response:** `{ success: true }`

### POST /api/prompt/regenerate
- **Auth:** Session | **Plan:** Advanced+
- **Body:** `{ sections?: ["persona", "context"] }`
- **Response:** `{ success, systemPrompt, regenerated }`

---

## Knowledge Base

### GET /api/kb
- **Auth:** Session
- **Response:** `{ assistantId, vapiAssistantId, canEdit, sections: [{ key, label, text, vapiFileId, updatedAt }] }` (canEdit: Starter+)

### PUT /api/kb
- **Auth:** Session | **Plan:** Starter+
- **Body:** `{ sectionKey, text?, vapiFileId?, fileName? }`
- **Response:** `{ success, section: { key, vapiFileId, fileName } }`

### DELETE /api/kb?section=faqs
- **Auth:** Session | **Plan:** Starter+
- **Response:** `{ success: true }`

### POST /api/kb/upload
- **Auth:** Session
- **Body:** multipart/form-data: `file` + `section`
- **Response:** `{ fileId, fileName, fileSize }`
- **Notes:** Max 10MB. Types: PDF, DOCX, CSV, TXT.

---

## Calls

### GET /api/calls/[id]
- **Auth:** Session
- **Response:** `{ call: { id, vapiCallId, status, startedAt, endedAt, durationMinutes, customerNumber, transcript, summary }, plan, features }`
- **Notes:** Transcripts/recordings gated by plan.

### GET /api/calls/browser-token
- **Auth:** Session | **Plan:** Enterprise
- **Response:** `{ token, identity, phoneNumber, expiresIn: 3600 }`

### POST /api/calls/outbound
- **Auth:** Session | **Plan:** Enterprise
- **Body:** `{ customerNumber }`
- **Response:** `{ success, call: { id, status, customerNumber } }`

### GET /api/calls/outbound/status?vapiCallId=...
- **Auth:** Session
- **Response:** `{ id, status, startedAt, endedAt, minutes, endReason }`

---

## SMS

### POST /api/sms/send
- **Auth:** Session | **Plan:** Pro+
- **Body:** `{ to, body }`
- **Response:** `{ success, messageSid, status }`
- **Notes:** Requires A2P CAMPAIGN_APPROVED status.

### GET /api/sms/logs?page=1&limit=20&direction=INBOUND
- **Auth:** Session
- **Response:** `{ logs: [...], pagination: { page, limit, total, totalPages } }`

---

## Phone Numbers

### GET /api/phone-numbers
- **Auth:** Session
- **Response:** `{ phoneNumber, vapiPhoneNumberId, twilioPhoneNumberSid, hasSipTrunk, hasAssistant, assistantName }`

### POST /api/phone-numbers
- **Auth:** Session
- **Body:** `{ areaCode? }`
- **Response:** `{ success, phoneNumber, vapiPhoneNumberId, twilioPhoneNumberSid }`

### GET /api/phone-numbers/search?areaCode=415&limit=5
- **Auth:** Session
- **Response:** `{ numbers: [{ phoneNumber, locality, region }] }`

### GET /api/transfer-number
- **Auth:** Session
- **Response:** `{ transferNumber, canEdit }` (canEdit: Advanced+)

### PUT /api/transfer-number
- **Auth:** Session | **Plan:** Advanced+
- **Body:** `{ transferNumber? }`
- **Response:** `{ success, transferNumber }`

---

## Business Settings

### GET /api/business-info
- **Auth:** Session
- **Response:** `{ businessType, businessDescription, websiteUrl }`

### PUT /api/business-info
- **Auth:** Session
- **Body:** `{ businessType?, businessDescription?, websiteUrl? }`
- **Response:** `{ success, businessType, businessDescription, websiteUrl }`

### GET /api/business-hours
- **Auth:** Session
- **Response:** `{ businessHours, timezone }`

### PUT /api/business-hours
- **Auth:** Session
- **Body:** `{ businessHours, timezone? }`
- **Response:** `{ success, businessHours, timezone }`

### GET /api/business-address
- **Auth:** Session
- **Response:** `{ businessAddress }`

### PUT /api/business-address
- **Auth:** Session
- **Body:** `{ businessAddress }`
- **Response:** `{ success, businessAddress }`

---

## Calendar Integration

### POST /api/calendar/connect
- **Auth:** Session | **Plan:** Advanced+
- **Body:** `{ provider: "CAL_COM", apiKey, eventTypeId }`
- **Response:** `{ success, provider }` (201)

### GET /api/calendar/status
- **Auth:** Session
- **Response:** `{ connected, provider, config, connectedAt, canEdit }`

### POST /api/calendar/disconnect
- **Auth:** Session
- **Response:** `{ success: true }`

### GET /api/calendar/test
- **Auth:** Session
- **Response:** `{ ok, timestamp, steps: [{ name, ok }] }`

---

## Widget & Embed Tokens

### POST /api/widget/token
- **Auth:** Embed Token
- **Body:** `{ embedToken: "ept_..." }`
- **Response:** `{ token, identity, phoneNumber, orgName, expiresIn: 3600 }`
- **Notes:** CORS-enabled. Domain whitelist enforced. 1-hour TTL.

### GET /api/embed-token
- **Auth:** Session | **Plan:** Pro+
- **Response:** `{ tokens: [{ id, label, tokenPrefix, allowedDomains, lastUsedAt, createdAt }] }`

### POST /api/embed-token
- **Auth:** Session | **Plan:** Pro+
- **Body:** `{ label?, allowedDomains?: string[] }`
- **Response:** `{ token: "ept_...", prefix, label }`
- **Notes:** Token shown only once.

### DELETE /api/embed-token?id=...
- **Auth:** Session
- **Response:** `{ revoked: true }`

---

## A2P Compliance (SMS Registration)

### GET /api/a2p/status
- **Auth:** Session
- **Response:** `{ status, paid, brandType, messagingServiceSid, smsUsed, hasSmsSubscription }`

### POST /api/a2p/initiate
- **Auth:** Session | **Plan:** Pro+
- **Response:** `{ success, status: "BRAND_PENDING" }`

### POST /api/a2p/checkout
- **Auth:** Session | **Plan:** Pro+
- **Body:** `{ businessInfo, representativeInfo, campaignInfo }`
- **Response:** `{ url }`

### POST /api/a2p/campaign
- **Auth:** Session | **Plan:** Pro+
- **Body:** `{ campaignInfo }`
- **Response:** `{ success, campaignSid, status: "CAMPAIGN_PENDING" }`

---

## Onboarding

### GET /api/onboarding
- **Auth:** Session
- **Response:** `{ completed, completedAt, data, phoneNumber, organizationName }`

### POST /api/onboarding
- **Auth:** Session
- **Body:** `{ onboardingData, complete? }`
- **Response:** `{ success, completed, phoneNumber?, provisioningErrors? }`

### POST /api/onboarding/analyze
- **Auth:** Session
- **Body:** `{ url }`
- **Response:** `{ extractedData, businessName, pagesScraped, scrapeWarnings }`

---

## Webhooks (Incoming)

### POST /api/webhooks/vapi
- VAPI call events (call.started, call.ended, end-of-call-report, tool-calls). Always returns 200.

### POST /api/webhooks/stripe
- Stripe payment events. Verified via webhook signature.

### POST /api/webhooks/twilio/voice
- TwiML generation for voice calls.

### POST /api/webhooks/twilio/status
- Call status updates (ringing, in-progress, completed).

### POST /api/webhooks/twilio/sms-inbound
- Inbound SMS messages.

### POST /api/webhooks/twilio/sms-status
- SMS delivery status updates.

---

## Cron Jobs

### GET /api/cron/billing-reset
- **Auth:** Cron | Daily 00:00 UTC | Resets monthly minute counters

### GET /api/cron/sync-calls
- **Auth:** Cron | Daily 00:00 UTC | Syncs call data from VAPI

### GET /api/cron/overage-billing
- **Auth:** Cron | Weekly Monday 11:00 UTC | Bills overage minutes

### GET /api/cron/payment-retry
- **Auth:** Cron | Daily 12:00 UTC | Retries failed payments

### GET /api/cron/a2p-status
- **Auth:** Cron | Daily 00:00 UTC | Checks A2P registration status