Overview
The phone.systems API is a RESTful JSON API. All requests are made over HTTPS to:
https://api.odygy.com/v1Requests and responses use application/json and UTF-8 encoding. Resource identifiers are opaque strings — treat them as such, don't parse them.
Authentication
The API uses bearer tokens. Generate an API key from the admin console under Settings → API keys and send it in the Authorization header of every request:
GET /v1/users HTTP/1.1
Host: api.odygy.com
Authorization: Bearer ps_live_abc123def456...
Accept: application/jsonKey scopes
Keys can be scoped to a subset of resources:
read:*— read-only access.calls:write— place and control calls.recordings:read— download recordings and transcripts.webhooks:write— manage webhook subscriptions.
Conventions
- Timestamps are ISO 8601 in UTC, e.g.
2026-04-23T10:12:45Z. - Phone numbers are in E.164 format, e.g.
+40741234567. - Durations are expressed in seconds unless otherwise noted.
- Money amounts are integers in the currency's smallest unit (cents for EUR/USD).
- Unknown fields in request bodies are rejected with
400 Bad Request.
Pagination
List endpoints return at most 50 items per page and use cursor-based pagination. The response includes a next_cursor when more results are available:
{
"data": [ { "id": "usr_01HZY..."}, ... ],
"next_cursor": "eyJpZCI6ICJ1c3JfMDFI..."
}Pass it as the cursor query parameter to fetch the next page. Use limit (max 100) to change the page size.
Errors
Errors use standard HTTP status codes and a machine-readable body:
{
"error": {
"code": "number_unavailable",
"message": "The requested number is not available in this region.",
"request_id": "req_01HZYABCDEF..."
}
}| Name | Type | Description |
|---|---|---|
| 400 | Bad Request | The request was malformed or failed validation. |
| 401 | Unauthorized | Missing or invalid API key. |
| 403 | Forbidden | The key lacks the required scope. |
| 404 | Not Found | The resource does not exist or was deleted. |
| 409 | Conflict | The request conflicts with the current state (e.g. duplicate port request). |
| 429 | Too Many Requests | You exceeded the rate limit. See Rate limits. |
| 5xx | Server Error | Something went wrong on our side — retries with exponential backoff are recommended. |
Rate limits
By default each API key is limited to 100 requests per second with a burst of 200, and to 20 concurrent outbound call creations. The current budget is exposed on every response:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 94
X-RateLimit-Reset: 1713869412When the limit is exceeded the API returns 429 Too Many Requests with a Retry-After header telling you how many seconds to wait.
Accounts
/v1/accountcurl https://api.odygy.com/v1/account \
-H "Authorization: Bearer $ODYGY_API_KEY"{
"id": "acc_01HZY8ABCXYZ",
"name": "Acme Ltd",
"country": "RO",
"currency": "EUR",
"created_at": "2023-09-12T08:00:00Z"
}Users
/v1/users/v1/users| Name | Type | Description |
|---|---|---|
| namerequired | string | Display name. |
| emailrequired | string | Must be unique within the account. Used for login and notifications. |
| extensionrequired | string | 3–6 digits. Must be unique within the account. |
| role | enum | One of admin, supervisor, agent. Defaults to agent. |
| caller_id_number | string | E.164 number used for outbound calls by default. |
curl -X POST https://api.odygy.com/v1/users \
-H "Authorization: Bearer $ODYGY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ivy Bennett",
"email": "ivy@acme.io",
"extension": "1244",
"role": "agent"
}'/v1/users/{id}/v1/users/{id}/v1/users/{id}Numbers
/v1/numbers/v1/numbers/search| Name | Type | Description |
|---|---|---|
| countryrequired | string | ISO 3166-1 alpha-2 country code. |
| type | enum | One of local, mobile, tollfree. |
| prefix | string | Optional area-code prefix, e.g. 40264 for Cluj. |
| features | string[] | Any of voice, sms, fax. |
/v1/numbers/{id}/attachCall flows
/v1/call_flows/v1/call_flows{
"name": "Main IVR",
"entry": "greeting_1",
"blocks": {
"greeting_1": {
"type": "greeting",
"audio_url": "https://assets.acme.io/welcome.mp3",
"next": "ivr_1"
},
"ivr_1": {
"type": "ivr",
"prompt_tts": "Press 1 for sales, 2 for support.",
"choices": {
"1": "queue_sales",
"2": "queue_support"
},
"timeout_next": "voicemail_1"
},
"queue_sales": { "type": "queue", "queue_id": "que_01HZY..." },
"queue_support": { "type": "queue", "queue_id": "que_01HZZ..." },
"voicemail_1": { "type": "voicemail", "mailbox_id": "vm_01HZA..." }
}
}/v1/call_flows/{id}/v1/call_flows/{id}/v1/call_flows/{id}Calls
/v1/calls| Name | Type | Description |
|---|---|---|
| fromrequired | string | E.164 caller-ID number that your account owns. |
| torequired | string | E.164 destination number. |
| user_id | string | User whose device should ring first (leg A). |
| recording | boolean | Record this call. Defaults to the flow's setting. |
| ai_features | string[] | Subset of summary, sentiment, transcript, topics. |
curl -X POST https://api.odygy.com/v1/calls \
-H "Authorization: Bearer $ODYGY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "+40264000111",
"to": "+441632960000",
"user_id": "usr_01HZY...",
"recording": true,
"ai_features": ["summary", "sentiment"]
}'/v1/calls/v1/calls/{id}Recordings
/v1/calls/{id}/recording/v1/calls/{id}/transcript{
"call_id": "cal_01HZY...",
"language": "en-US",
"segments": [
{ "t": 0.00, "speaker": "caller", "text": "Hi, I'm calling about invoice 4011." },
{ "t": 3.42, "speaker": "agent", "text": "Of course — let me pull it up." }
]
}Webhooks
Subscribe to events to receive real-time notifications. Register an endpoint once, then listen for any number of event types.
/v1/webhooks| Name | Type | Description |
|---|---|---|
| urlrequired | string | HTTPS URL that will receive POSTs. |
| eventsrequired | string[] | Event types to subscribe to. |
| secret | string | Used to sign payloads. If omitted, one is generated. |
Event types
call.initiated— a call has started.call.answered— an agent picked up.call.completed— hangup, with CDR.recording.ready— recording file is available.transcript.ready— AI transcript is available.voicemail.received— new voicemail dropped.number.ported— port-in completed.
Signature verification
Every request carries an Odygy-Signature header containing a timestamp and an HMAC-SHA256 of the raw body using your endpoint secret. Reject any request whose signature is invalid or whose timestamp is more than 5 minutes old.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const [tPart, sigPart] = header.split(",");
const t = tPart.split("=")[1];
const given = sigPart.split("=")[1];
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(given, "hex"),
Buffer.from(expected, "hex")
);
}SDKs
Official, type-safe SDKs wrap the REST API:
- Node.js / TypeScript —
npm install @odygy/phone-systems - Python —
pip install odygy - Go —
go get github.com/odygy/phone-systems-go
import { Odygy } from "@odygy/phone-systems";
const odygy = new Odygy({ apiKey: process.env.ODYGY_API_KEY });
const call = await odygy.calls.create({
from: "+40264000111",
to: "+441632960000",
recording: true,
aiFeatures: ["summary", "sentiment"],
});
console.log(call.id);Changelog
- 2026-04-01 — v1 general availability.
ai_featuresnow acceptstopics. - 2026-02-14 — Webhooks signature format updated. Old format is still accepted until 2026-06-01.
- 2026-01-08 — New
/v1/calls/{id}/transcriptendpoint.
/v2, etc.). v1 endpoints remain supported for at least 12 months after deprecation.