Documentation

Reference

Beacon API

This document covers the currently implemented Beacon HTTP API. It does not describe planned features.

For the machine-readable contract, use GET /openapi.yaml on your Beacon install.

Base conventions

Authentication

Beacon is team-scoped.

Preferred auth header:

X-API-Key: beacon_...

Also supported by the current app:

  • Authorization: Bearer <api-key>
  • api_key query parameter
  • api_key request body field

Test mode is enabled by appending _test to a valid API key. In test mode, Beacon bypasses license validation and simulates provider-side delivery without requiring live Twilio credentials.

When authentication fails:

{
  "success": false,
  "error": "missing_team_context",
  "message": "The provided API key is missing or invalid."
}

MCP endpoint

Beacon also exposes a web MCP server at /api/mcp.

In practice, the MCP transport uses POST /api/mcp for client requests, and the server may also negotiate related MCP transport methods on the same path.

It uses the same tenant API key authentication model as the HTTP API and accepts the same auth options described above.

Middleware used on business endpoints

Most /api/* endpoints use:

  • tenant.api_key
  • rate.limit
  • idempotency
  • tenant.required
  • ValidateLicenseOrTestMode

Idempotency

Write requests support the Idempotency-Key header.

  • scoped per API key
  • empty keys rejected
  • max length: 255
  • successful JSON responses cached for 24 hours
  • replayed responses include X-Idempotency-Replayed: true
  • reusing a key with a different payload returns idempotency_key_mismatch

Rate limits

Current defaults:

  • burst: 50
  • refill: 10 requests / second
  • scoped per API key
  • env-configurable

Cursor pagination

Cursor-paginated endpoints use:

  • per_page — default 15, max 100
  • cursor — opaque cursor from the previous response

Typical shape:

{
  "hasMore": true,
  "nextCursor": "opaque-cursor-value"
}

Error shape

All API errors use:

{
  "success": false,
  "error": "error_code",
  "message": "Human-readable description."
}

Endpoint groups

Spec

GET /openapi.yaml

Returns the current OpenAPI 3.1 spec for the implemented API.


Messaging

POST /api/text

Send an outbound SMS.

Provide either phone_number to start or reuse a thread by phone, or threadId to reply within an existing thread.

Request fields

Field Required Notes
phone_number Required unless threadId is provided E.164 format
threadId Required unless phone_number is provided UUID
message Yes 1–160 characters
sender No Only allowed when starting by phone_number
replyWebhookUrl No Valid URL
webhookData No Max 100 chars

Success response

Returns the send result as JSON. The response includes the created message record ID as textId.

Common errors

  • invalid_phone
  • invalid_thread
  • message_required_or_empty
  • invalid_sender
  • thread_not_found
  • sending_number_not_resolved
  • recipient_opted_out
  • invalid_idempotency_key
  • idempotency_key_mismatch
  • message_failed
  • rate_limited
  • server_error

Threads

Threads are the current conversation abstraction used by Beacon.

GET /api/threads

List thread summaries for the authenticated team.

Query parameters:

  • per_page
  • cursor

Response shape:

{
  "threads": [
    {
      "id": "uuid",
      "contactName": "Jamie Doe",
      "contactPhone": "+15555555555",
      "senderName": "Sales",
      "senderPhone": "+15551234567",
      "lastDirection": "inbound",
      "lastBody": "Can you send me pricing?",
      "lastMessageAt": "2026-04-03T10:12:00.000000Z",
      "tags": []
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

GET /api/threads/{id}

Get one thread summary.

Errors:

  • thread_not_found
  • server_error

GET /api/threads/{id}/messages

Get message history for a thread.

Query parameters:

  • per_page
  • cursor

Messages are returned newest first.

Response shape:

{
  "messages": [
    {
      "id": "uuid",
      "direction": "inbound",
      "body": "Newest message",
      "status": "received",
      "createdAt": "2026-04-03T11:00:00.000000Z"
    }
  ],
  "nextCursor": null
}

GET /api/threads/{id}/replies?since=<timestamp>

Poll for inbound replies after a timestamp.

Query parameters:

  • since — required, parseable timestamp
  • per_page
  • cursor

Replies are returned oldest first.

Response shape:

{
  "replies": [
    {
      "id": "uuid",
      "direction": "inbound",
      "body": "First new inbound reply",
      "status": "received",
      "createdAt": "2026-04-03T11:00:00.000000Z"
    }
  ],
  "nextCursor": null
}

Common errors:

  • invalid_since
  • thread_not_found
  • server_error

Contacts

Contact endpoints

Method Endpoint Purpose
GET /api/contacts List contacts
POST /api/contacts Create a contact
GET /api/contacts/{id} Get one contact
PATCH /api/contacts/{id} Update a contact
POST /api/contacts/upsert Create or update by id, external ID, or phone
GET /api/contacts/by-phone/{phone} Find a contact by phone
GET /api/contacts/duplicates List duplicate groups
POST /api/contacts/merge Merge two contacts
POST /api/contacts/{id}/phone-numbers Add a phone number
PATCH /api/contacts/{id}/phone-numbers/{phoneId} Update a contact phone number
DELETE /api/contacts/{id}/phone-numbers/{phoneId} Delete a contact phone number

GET /api/contacts

Supported filters:

  • query
  • external_id
  • tag
  • updated_after
  • per_page
  • cursor

Response shape:

{
  "contacts": [
    {
      "id": "uuid",
      "name": "Jamie Doe",
      "company": "Acme",
      "role": "Buyer",
      "primaryPhone": "+15555555555",
      "tags": ["lead"],
      "externalId": "ext_123",
      "metadata": {"source": "crm"},
      "createdAt": "2026-04-03T10:00:00.000000Z",
      "updatedAt": "2026-04-03T10:00:00.000000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

POST /api/contacts

Create a contact.

Request fields

Field Required Notes
name Yes Max 255 chars
company No Max 255 chars
role No Max 255 chars
phones Yes Array, at least 1
tags No Array of strings
externalId No Max 255 chars
metadata No Object
notes No Array of strings

Returns 201 with:

{
  "success": true,
  "contact": {}
}

GET /api/contacts/{id}

Returns the full contact record, including:

  • summary fields
  • phones
  • notes

If not found:

  • contact_not_found

PATCH /api/contacts/{id}

Update any subset of:

  • name
  • company
  • role
  • externalId
  • metadata
  • tags
  • notes

Returns:

{
  "success": true,
  "contact": {}
}

POST /api/contacts/upsert

Create or update a contact by matching on one of:

  • id
  • externalId
  • phones

Behavior:

  • creates when no match exists
  • updates when exactly one contact matches
  • returns matchedBy when updated
  • returns created: true|false
  • returns ambiguous_contact_match if multiple contacts match the supplied phone number
  • returns contact_identity_conflict if lookup inputs resolve to different contacts

GET /api/contacts/by-phone/{phone}

Returns:

  • { "found": false, "contact": null } when no match exists
  • { "found": true, "contact": { ... } } when one match exists

Errors:

  • invalid_phone
  • multiple_contacts_found

GET /api/contacts/duplicates

Returns duplicate groups detected by:

  • same phone
  • same external ID
  • fuzzy name match within the same company

Response shape:

{
  "duplicateGroups": [
    {
      "reason": "same_phone",
      "contacts": []
    }
  ]
}

POST /api/contacts/merge

Merge one contact into another.

Request fields

Field Required Notes
sourceContactId Yes UUID
targetContactId Yes UUID
strategy No prefer_target, prefer_source, prefer_non_null, append
fields No Per-field merge strategy overrides

Returns:

{
  "success": true,
  "contact": {},
  "mergedFrom": "uuid",
  "movedThreads": 2
}

Common errors:

  • cannot_merge_same_contact
  • contact_not_found
  • invalid_merge_strategy
  • external_id_conflict

Contact phone number sub-resources

POST /api/contacts/{id}/phone-numbers

Add a phone number to a contact.

Fields:

  • phoneNumber — required
  • label — optional
  • isPrimary — optional boolean

PATCH /api/contacts/{id}/phone-numbers/{phoneId}

Update:

  • label
  • isPrimary

DELETE /api/contacts/{id}/phone-numbers/{phoneId}

Deletes a phone number unless it is the contact's last phone number.

Common errors across contact phone endpoints:

  • contact_not_found
  • contact_phone_not_found
  • duplicate_contact_phone
  • invalid_phone
  • cannot_delete_last_contact_phone

OTP

POST /api/otp/generate

Generate and send an OTP over SMS.

Request fields

Field Required Notes
phone Yes E.164 format
recipient_id Yes Max 255 chars
message No 1–160 chars, may include $OTP
lifetime No 30–600 seconds
length No 6–8 digits

Behavior:

  • does not return the plaintext OTP
  • invalidates older active OTPs for the same recipient_id
  • blocks sends to opted-out recipients

POST /api/otp/verify

Verify a submitted OTP.

Request fields

Field Required Notes
recipient_id Yes Max 255 chars
otp Yes 6–8 digits

Common OTP errors:

  • invalid_phone
  • invalid_recipient_id
  • invalid_message
  • invalid_lifetime
  • invalid_length
  • invalid_otp_format
  • recipient_opted_out
  • sending_number_not_resolved
  • message_failed
  • otp_expired
  • otp_invalid
  • otp_already_verified
  • otp_max_attempts

Business numbers

Business number endpoints

Method Endpoint Purpose
GET /api/business-numbers List business numbers
POST /api/business-numbers Create a business number
GET /api/business-numbers/{id} Get one business number
PATCH /api/business-numbers/{id} Update a business number
POST /api/business-numbers/{id}/make-primary Make a number primary
DELETE /api/business-numbers/{id} Archive a business number

GET /api/business-numbers

Supported filters:

  • status
  • type
  • agent_profile
  • is_primary

Response shape:

{
  "businessNumbers": [
    {
      "id": "uuid",
      "name": "Sales",
      "phoneNumber": "+15551234567",
      "friendlyName": "Main sales line",
      "type": "Local",
      "status": "active",
      "isPrimary": true,
      "capabilities": {"sms": true},
      "agentProfile": "sales-agent",
      "createdAt": "2026-04-03T10:00:00.000000Z",
      "updatedAt": "2026-04-03T10:00:00.000000Z"
    }
  ]
}

POST /api/business-numbers

Create a business number.

Request fields

Field Required Notes
name Yes Unique within the team
phoneNumber Yes E.164 format, globally unique
phoneNumberSid Yes Provider SID
friendlyName No Max 255 chars
type No Current enum-backed type
isPrimary No Boolean
agentProfile No Max 255 chars
settings No Object
capabilities No Object

Returns 201 with:

{
  "success": true,
  "businessNumber": {}
}

Common errors:

  • duplicate_business_number_name
  • duplicate_business_number_phone
  • validation_error

GET /api/business-numbers/{id}

Returns the full business number, including settings.

Errors:

  • business_number_not_found

PATCH /api/business-numbers/{id}

Update any subset of:

  • name
  • friendlyName
  • status (active or paused)
  • agentProfile
  • settings

Common errors:

  • business_number_not_found
  • duplicate_business_number_name
  • invalid_business_number_status

POST /api/business-numbers/{id}/make-primary

Makes an active business number the team's primary sending number.

Common errors:

  • business_number_not_found
  • invalid_business_number_status

DELETE /api/business-numbers/{id}

Archives a business number.

Common errors:

  • business_number_not_found
  • cannot_delete_primary_business_number

External webhooks

These endpoints are implemented for Twilio / provider callbacks.

Method Endpoint Purpose
POST /api/external/sms/status Outbound SMS delivery status
POST /api/external/sms/otp-status OTP delivery status
POST /api/external/sms/inbound Inbound SMS webhook

POST /api/external/sms/inbound

Behavior:

  • creates or appends to a thread
  • records inbound messages
  • detects opt-out keywords: STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT
  • detects opt-in keywords: START, UNSTOP
  • blocks future outbound SMS and OTP sends to opted-out recipients with recipient_opted_out

Current API surface at a glance

Implemented today:

  • messaging
  • threads and reply polling
  • contacts, merge, duplicates, notes, tags, phone numbers
  • OTP generation and verification
  • business number management
  • inbound and status webhooks
  • OpenAPI spec delivery at /openapi.yaml

Raw markdown · llms.txt