voepy

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 needs Authorization: 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 the Accept header.

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:

PrefixResource
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.00 top-up is amount_cents: 2500; a 12-cent call is cost_cents: 12. There are no floats and no other currencies at launch. A null money field means "not yet known" (e.g. cost_cents before rating finishes) — which is different from 0 ("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
ParameterDefaultMaxNotes
page11-indexed. Out-of-range pages return 404.
page_size20200Values 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 cursor parameter and no next_cursor field — page/page_size and next/previous URLs 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, and DELETE under /v1/. GET requests 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:

StatusMeaning
400 Bad RequestMalformed request — bad JSON, missing/invalid field.
401 UnauthorizedMissing or invalid API key.
402 Payment RequiredBalance + credit limit exhausted.
403 ForbiddenAuthenticated but not allowed (e.g. from not owned, country/prefix blocked).
404 Not FoundNo such resource, or an out-of-range page.
409 ConflictState conflict (e.g. acting on a call that already ended).
422 Unprocessable EntityWell-formed but semantically rejected.
429 Too Many RequestsA rate-limited unauthenticated endpoint was hit too often (see Rate limits).
5xxSomething 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-Key you sent, if any.
  • The method, full path, and approximate timestamp (UTC) of the request.
  • The error code and message you 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 code and how to handle it.