API conventions
The rules that hold across every voepy endpoint: how you authenticate, what requests and responses look like, how IDs and money and timestamps are formatted, how lists paginate, how errors come back, and what idempotency and rate limiting actually do today.
This is the canonical reference. Other guides (Calls, Phone numbers, Billing) assume you've read it and won't repeat the basics.
Everything lives under
/v1/. Every request needsAuthorization: Bearer <api-key>.
Base URL & versioning
All endpoints are served from a single host under a version prefix:
https://api.voepy.com/v1/
The version is in the path (/v1/), not a header. There is one live
version today. When we ship a breaking change it will land under a new
prefix (/v2/) and /v1/ will keep working — we don't break a version
in place. Additive changes (new optional fields, new endpoints, new
enum values) ship inside /v1/ without a version bump, so write
clients that ignore unknown JSON fields.
Authentication
Send your API key as a bearer token on every request:
Authorization: Bearer voepy_live_<your-key>
Keys are tenant-scoped and carry their own permission scopes. They're server-side credentials — never ship one in browser or mobile code. Full details, rotation, and scopes are in Authentication.
Requests & responses
- JSON in, JSON out. Send request bodies as JSON with
Content-Type: application/json. Responses are JSON. - Field names are
snake_case— both in bodies you send and bodies you receive (customer_reference,duration_seconds,cost_cents). - Mutating verbs (
POST,PUT,PATCH,DELETE) carry a JSON body where applicable; reads (GET) take query parameters. - Send
Accept: application/json. The API can also render a browsable HTML view for the same URLs (handy while exploring in a browser), so if you don't ask for JSON explicitly a browser may get HTML back. Machine clients should always set theAcceptheader.
Identifiers
Every customer-visible resource has an opaque, prefixed string ID. The prefix tells you the resource type at a glance; the rest is a 32-character hex value. Treat the whole thing as an opaque string — don't parse it, don't assume a length beyond "string", and don't construct one yourself.
The format is always voepy_<type>_<32 hex chars>, e.g.:
voepy_call_9f1c2a7b4e8d4a1fb0c3d5e6f7a8b9c0
The real prefixes you'll encounter:
| Prefix | Resource |
|---|---|
voepy_call_ | Call |
voepy_leg_ | Call leg |
voepy_rec_ | Recording |
voepy_route_ | Route (maps to a connection; the upstream connection ID never leaks) |
voepy_stream_ | Media stream |
voepy_tx_ | Transcription session |
voepy_amd_ | Answering-machine-detection result |
voepy_conf_ | Conference |
voepy_member_ | Conference member |
voepy_sub_ | Subscription |
voepy_inv_ | Invoice |
voepy_port_ | Porting order |
voepy_doc_ | Porting document |
API keys use their own prefix, voepy_live_, on the wire — see
Authentication.
These IDs encode a UUID under the hood, so they are 32 hex characters after the prefix — not a sortable timestamp. Don't sort or paginate by ID; use the timestamp filters and pagination described below.
Value formats
A few formats are uniform everywhere:
- Money is integer cents, USD. A
$25.00top-up isamount_cents: 2500; a 12-cent call iscost_cents: 12. There are no floats and no other currencies at launch. Anullmoney field means "not yet known" (e.g.cost_centsbefore rating finishes) — which is different from0("free"). - Phone numbers are E.164. A leading
+followed by 6–15 digits, no spaces or dashes:+15125550100. - Timestamps are ISO-8601 with a timezone, UTC, millisecond
precision:
2026-05-12T14:32:18.000Z. Date/time query filters accept the same format.
Pagination
List endpoints use page-number pagination, not cursors. You pass a page number and (optionally) a page size:
GET /v1/calls?page=2&page_size=50
| Parameter | Default | Max | Notes |
|---|---|---|---|
page | 1 | — | 1-indexed. Out-of-range pages return 404. |
page_size | 20 | 200 | Values above 200 are clamped to 200. |
The response is the standard envelope — a total count, absolute
next/previous URLs (each null at the ends), and the page of rows
in results:
{
"count": 137,
"next": "https://api.voepy.com/v1/calls?page=3&page_size=50",
"previous": "https://api.voepy.com/v1/calls?page=1&page_size=50",
"results": [
{ "id": "voepy_call_9f1c2a7b4e8d4a1fb0c3d5e6f7a8b9c0", "...": "..." }
]
}
Walk a list by requesting page=1, then following next until it
comes back null. count is the total across all pages, not the size
of the current page.
This is a real page-number scheme, not a cursor scheme. There is no
cursorparameter and nonext_cursorfield —page/page_sizeandnext/previousURLs are what the API returns. Resource-list filters (status, direction, date ranges, etc.) are documented on each resource's own guide and combine with these pagination parameters.
Idempotency
Mutating requests support an Idempotency-Key header so a retry
after a network blip doesn't run the operation twice (place a call
twice, charge a card twice):
POST /v1/calls
Content-Type: application/json
Idempotency-Key: 0f8e7d6c-5b4a-3210-fedc-ba9876543210
{ "from": "+15125550100", "to": "+15558675309" }
How it behaves:
- Scope. Idempotency applies to
POST,PUT,PATCH, andDELETEunder/v1/.GETrequests ignore the header (reads are already safe to repeat). - Key. Use one unique value per logical operation — a UUID is
ideal. Max length is 128 characters; longer keys are rejected with
400 idempotency_key_too_long. - Replay window. The first response for a given key is remembered
for 24 hours. A repeat request with the same key, method, and
path returns that stored response verbatim — the underlying view is
not run again. Replayed responses carry the header
Idempotent-Replayed: true, so you can tell a replay from a fresh execution. - Scoped to your principal. Keys are namespaced by the authenticated API key, so your keys can't collide with another tenant's, and a key only replays if it reaches the same method and path it was first used on.
- What gets remembered. Successful responses and client errors
(
4xx) are cached and replayed. Server errors (5xx) are not cached, so retrying after a transient failure can still succeed.
Always send Idempotency-Key on operations that cost money or create
resources. See Calls for the call-specific guidance.
Rate limits
There is no hard per-tenant rate limit on the authenticated /v1/
API today. Authenticated calls are not throttled, and the API does
not return rate-limit headers (no X-RateLimit-*, no Retry-After)
or 429 responses on normal traffic.
A few unauthenticated endpoints are protected against abuse —
signup, email verification, password reset, and the billing webhook
receiver are rate-limited and can return 429 Too Many Requests. These
limits don't affect your API-key traffic.
This is the current state, not a promise that it stays unlimited.
Build clients that back off gracefully on 429 and 5xx anyway, so
you're covered when per-tenant quotas arrive. In the meantime, please
be a good neighbour: batch where you can and avoid tight polling loops
(prefer Webhooks over polling).
Errors
Every 4xx and 5xx response uses the same envelope: a single error
object with a machine-readable code and a human-readable message.
{
"error": {
"code": "balance_insufficient",
"message": "Tenant balance is exhausted; top up to place new calls."
}
}
Branch your logic on code (stable), not on message (may be reworded
or carry context). The HTTP status reflects the category:
| Status | Meaning |
|---|---|
400 Bad Request | Malformed request — bad JSON, missing/invalid field. |
401 Unauthorized | Missing or invalid API key. |
402 Payment Required | Balance + credit limit exhausted. |
403 Forbidden | Authenticated but not allowed (e.g. from not owned, country/prefix blocked). |
404 Not Found | No such resource, or an out-of-range page. |
409 Conflict | State conflict (e.g. acting on a call that already ended). |
422 Unprocessable Entity | Well-formed but semantically rejected. |
429 Too Many Requests | A rate-limited unauthenticated endpoint was hit too often (see Rate limits). |
5xx | Something on our side. 502 upstream_error is a carrier-side failure and is often retriable. |
The code values themselves and what to do about each are listed in
the Error reference.
Request IDs & debugging
voepy does not return a per-request correlation ID header today (there
is no X-Request-Id). The one diagnostic header the API does set is on
idempotent replays — Idempotent-Replayed: true — which tells you a
response came from the idempotency cache rather than a fresh execution.
When you contact support about a specific failure, include what you do have so we can find it fast:
- The resource ID involved (
voepy_call_…,voepy_inv_…, etc.) — these are designed to self-identify in our logs. - The
Idempotency-Keyyou sent, if any. - The method, full path, and approximate timestamp (UTC) of the request.
- The error
codeandmessageyou received.
For call-flow issues, Webhooks carry the same resource IDs, so the webhook payload you received is often the fastest way to pinpoint the event.
Next
- Authentication — get a key, understand scopes, rotate safely.
- Calls — the main resource, where idempotency and these conventions get put to work.
- Webhooks — listen for events instead of polling.
- Error reference — every
codeand how to handle it.
