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