API & webhooks

Send from your phones. Receive events in your tools.

Create an API key

In the dashboard, open Integrations → Webhooks & API → Manage integration → API keys → Create key. Choose the phones and permissions, then copy the key. It is shown once. Keep it on your server, never in browser scripts, public URLs or messages.

Authorization: Bearer YOUR_VEXAPHONE_API_KEY
Base URL: https://api.vexaphone.com

Keys apply only to the selected phones; new phones are not added automatically. Create a new key to change its phone selection or permissions. Rotation replaces the secret while keeping its logical request identity. Revocation blocks future API access; already accepted phone work continues under its original expiry and service checks.

PermissionScope
Discover selected phonessenders:read
Send SMS / read this client's accepted SMSmessages:send / messages:read
Start calls / read command status and selected-phone call detailscalls:write / calls:read
Read retained phone events from all originsevents:read
Download authorised recordings, transcripts and attachmentsmedia:read, plus the resource's read permission
Include the account's shared voicemailvoicemail:read, plus events:read for events or media:read for audio

Send an SMS or start a handset call

First call GET /v1/senders and use a returned senderId. Display names and SIM-reported numbers identify the handset; a reported number may be missing or unverified. API recipients must include an international +country code. Vexaphone does not guess the recipient's country.

Replace the uppercase placeholders below. The examples are intentionally not executable as-is.

curl https://api.vexaphone.com/v1/messages \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: YOUR_STABLE_REQUEST_ID' \
  --data '{"senderId":"SENDER_UUID","to":"RECIPIENT_IN_E164","text":"Your message","expiresAt":"FUTURE_UTC_TIMESTAMP"}'

SMS expiry must be in the future and at most 24 hours away. Reuse the same Idempotency-Key and exact body, including expiry, after a lost response. A changed body under the same key conflicts. Do not generate a new request identity merely because a response or carrier delivery report is missing.

POST /v1/call-commands
Idempotency-Key: YOUR_STABLE_REQUEST_ID

{"senderId":"SENDER_UUID","to":"RECIPIENT_IN_E164"}

202 means accepted, not delivered or answered. Calls are placed on the selected Android handset; this API does not carry browser audio. A submitted command is not proof of connection. Follow its status, then the actual call event. Connected calls can include another voicemail system and do not prove a human answered.

Read events, calls and files

EndpointAccess and behaviour
GET /v1/sendersRequires senders:read. Lists only selected authorized phones; readiness is a recent phone report, not a reservation or service guarantee.
POST /v1/messagesRequires messages:send. Recipient must include +country code. expiresAt must be in the future, at most 24 hours away. Retries use the same key and unchanged body, including expiry.
GET /v1/messages/{messageId}Requires messages:read. This API client’s accepted SMS only; no other clients, CRMs or inbound history.
POST /v1/call-commandsRequires calls:write. Requests a call on the selected Android phone. 202 is not proof of dialling or answering. Replay returns the original acceptance and cannot extend the short expiry.
GET /v1/call-commands/{commandId}Requires calls:read. This client’s commands only; callId is included when actual phone history arrives.
GET /v1/eventsRequires events:read. Retained selected-phone events from every origin. Shared voicemail also requires voicemail:read. Filtered rows advance the cursor; an empty page does not imply end-of-stream. Stale nonzero cursors return 410. after=0 explicitly restarts the retained window.
GET /v1/calls/{callId}Requires calls:read. Ended calls on selected current phone/SIM grants, from any origin. Safe details and optional protected artifact references.
GET /v1/calls/{callId}/recordingRequires calls:read and media:read. Audio content; supports a single byte range. A later transcript revision does not invalidate unchanged audio.
GET /v1/calls/{callId}/transcriptRequires calls:read and media:read. Phone-provided UTF-8 plain text when available.
GET /v1/voicemails/{voicemailId}/recordingRequires voicemail:read and media:read. Explicit account-shared mailbox, no handset inference. Revision is 1.
GET /v1/messages/{messageId}/attachments/{mediaId}Requires events:read and media:read. Exact selected-phone message/attachment identity, from any origin. No arbitrary storage-key access.

Message and call-command status belongs to the same logical API client. Event and actual-call reads include selected-phone activity from the handset, dashboard, API and connected CRMs. The source CRM retains its own SMS status path; other connected integrations receive passive history rather than another send command.

GET /v1/events?after=0&limit=30 examines at most 30 rows by default, up to 100. Persist nextCursor even when a page has no visible events. Continue until the cursor stops advancing. A nonzero cursor behind the retained seven-day window returns 410 cursor_expired; explicitly restart with after=0 to read what remains.

Artifact references require the bearer header and exact revision. They are not permanent public URLs. Adding a transcript does not invalidate unchanged audio; replaced or retired files return 410. Media availability depends on the phone and approved setup. Vexaphone does not generate a transcript for every call. Shared voicemail is account-level, not assigned to an inferred handset.

Receive events at one endpoint

Open Integrations → Webhooks & API → Webhooks → Add webhook. Enter a public HTTPS URL, select phones and event types, and copy the signing key. One URL can receive all selected types. New phones require explicit selection. URLs with credentials, fragments, IP literals, private hostnames or Vexaphone service domains are rejected.

Call endings arrive independently of later recording or transcript availability. Content is represented by optional metadata/references, not inline audio or transcript bytes. A message between two connected phones retains each phone's own outbound/inbound perspective; events are not merged or reversed.

Payload example

{
  "eventId": "11111111-1111-4111-8111-111111111111",
  "type": "message.received",
  "schemaVersion": "1",
  "occurredAt": "2026-11-07T01:00:00Z",
  "recordedAt": "2026-11-07T01:00:01Z",
  "data": {
    "messageId": "22222222-2222-4222-8222-222222222222",
    "revision": 1,
    "deviceId": "33333333-3333-4333-8333-333333333333",
    "deviceName": "Office phone",
    "vexaphoneNumber": "+61400000000",
    "contactNumber": "+61400000001",
    "contactAddress": {
      "raw": "0400000001",
      "kind": "phone"
    },
    "numberCountry": "AU",
    "normalizationSource": "phone_country",
    "direction": "inbound",
    "transport": "sms",
    "origin": "phone",
    "originId": null,
    "commandId": null,
    "text": "Synthetic example. Do not send.",
    "submissionState": "not_applicable",
    "deliveryState": "not_applicable",
    "attachments": []
  }
}

Use type to choose the data shape and schemaVersion to identify its version. The origin is phone, dashboard, api or integration; originId and commandId are correlation references, never credentials. Filter message.received / inbound for autoreplies instead of responding repeatedly to your own outbound/status events.

Verify and acknowledge

webhook-id: EVENT_UUID
webhook-timestamp: UNIX_SECONDS
webhook-signature: v1,BASE64_SIGNATURE

We use the Standard Webhooks signature format. Verify HMAC-SHA256 over the exact UTF-8 bytes of id + "." + timestamp + "." + rawBody, using the Base64-decoded signing key after removing whsec_. Compare signatures in constant time, check timestamp freshness (normally five minutes), and accept any valid v1 signature during rotation. Never verify a parsed/reserialized JSON object.

Store the event ID durably for deduplication (at least seven days is recommended). Retry attempts use the same ID and byte-identical body, with a fresh timestamp/signature. Return 2xx promptly after durable acceptance and process your automation separately. A 2xx acknowledgement ends our retries.

Errors and recovery

API 401 means invalid, expired or revoked credentials; 403 means missing scope or access; 404 means unavailable; 409 means a conflicting action; 410 means an expired replay/artifact reference; 416 means an unsupported byte range. On 429 or 503, honour Retry-After and retry accepted-action recovery with the same request key/body. Admission counters are separated by account and request family; they are abuse controls rather than strict billing quotas.

Webhook timeouts, network errors, 408, 429 and 5xx use bounded backoff. Redirects are never followed. Permanent 4xx and 410 pause the endpoint for review. Open Webhooks → Deliveries to see the receiver result, then fix the URL/authentication or use Actions → Resume. There are at most 100 attempts and 72 hours from enqueue; pausing/resuming does not restart the budget.

Renaming a webhook preserves queued work. Changing its URL, event selection or phone selection starts a new configuration for future events; old pending deliveries do not move to the new configuration. Revocation stops future attempts and removes signing material. Requests already in progress may finish. Signing-key rotation overlaps the previous key for 24 hours; another rotation is blocked during that overlap. Save the new secret immediately; it cannot be retrieved later. If it is lost during overlap, revoke/recreate the destination and use retained events for recovery.

Delivery history and replay events are retained for seven days. Existing call/SMS history and recording retention are separate. If an old delivery can no longer be retried, use the retained event API and your receiver's event-ID deduplication where available. Contact support@vexaphone.com with your account and approximate event time; never email API or signing keys.