Getting Started with the Vosy API
Everything you need to make your first authenticated call to the Vosy Voice AI API: base URL, Bearer keys, the scope model, response envelopes, rate limits, idempotency, key rotation, webhooks, and copy-paste curl walkthroughs.
On this page
Base URL & spec
The API is versioned and served from a single host. Every path in this guide is relative to the base URL.
| Item | Value |
|---|---|
| Base URL | https://api.vosy.ai/v2 |
| Machine-readable spec | https://api.vosy.ai/openapi.json — OpenAPI 3.1 |
| Content type | application/json (file uploads use multipart/form-data) |
| Correlation id | every authed response returns X-Request-ID — include it when you report an issue |
The spec is the contract. There are no client SDKs yet, but the OpenAPI document generates working clients in any language via tools like openapi-generator. Each operation in the spec lists the scope(s) that authorize it under x-required-scopes.
Authentication
Every request authenticates with a scoped Bearer key in the Authorization header:
Authorization: Bearer vosy_sk_...
Keys carry the vosy_sk_ prefix. Treat a key like a password: it belongs on your server, never in a browser, a mobile app, or a git commit. If a key is exposed, revoke or rotate it (see Key rotation). A missing or invalid key returns 401; a key that lacks the scope for an endpoint returns 403.
Scopes
A key is granted an explicit set of scopes — roughly 40 in total — and each endpoint declares which scope(s) authorize it. Scopes follow a resource:action shape, so you provision a key with exactly the access an integration needs and nothing more.
Read and write are independent. Granting kb:write does not imply kb:read — if an integration both uploads documents and lists them back, request both kb:read and kb:write. The same holds for every resource. A key that can create something it cannot read back is a common first-integration surprise.
The scope families:
| Resource | Scopes |
|---|---|
| Agents | agents:read · agents:write · agents:delete |
| Knowledge bases | kb:read · kb:write |
| Calls | calls:schedule · calls:read · calls:cancel · calls:recordings |
| Campaigns | campaigns:read · campaigns:write · campaigns:delete · campaigns:execute |
| Telephony | telephony:read · telephony:write · telephony:delete · telephony:manage |
| Schedules (business hours) | schedules:read · schedules:write · schedules:delete |
| Scheduling (appointments) | scheduling:read · scheduling:write |
| Webhooks | webhooks:read · webhooks:write · webhooks:delete · webhooks:manage |
| DNC | dnc:read · dnc:write |
| SMS | sms:read · sms:write |
| Usage | usage:read |
| API keys | api-keys:read · api-keys:write · api-keys:delete · api-keys:manage |
The live list — with a human description for each scope and a set of ready-made presets — is available at GET /api-keys/scopes. A * super-scope and per-resource wildcards like agents:* exist for keys that need broad access.
Response envelopes
Successful responses share one shape. List endpoints add a meta pagination block:
{ "success": true, "data": { ... } }
{ "success": true, "data": [ ... ], "meta": { "limit": 25, "offset": 0 } }
There are two error shapes, and the field order differs between them. Router (validation, not-found, conflict) errors put message first:
{
"success": false,
"error": {
"message": "Human-readable summary",
"code": "bad_request",
"details": { "validation_errors": { "slot_start": "..." } }
}
}
Auth and rate-limit errors put code first and may add retry_after:
{
"success": false,
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "Human-readable summary",
"retry_after": 30
}
}
Branch on status and code, never on the message. Messages are for humans and can change without notice. Auth codes include MISSING_AUTH, INVALID_API_KEY, INSUFFICIENT_SCOPE, and RATE_LIMIT_EXCEEDED; router codes are lowercase, like bad_request and not_found.
Rate limits & headers
Requests are limited per key to 60 per minute and 1,000 per hour. Every authed response carries the current budget so you can pace yourself without guessing:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | requests allowed in the current window |
X-RateLimit-Remaining | requests remaining in the current window |
X-RateLimit-Reset | Unix epoch seconds when the window resets |
Retry-After | seconds to wait — sent on a 429 |
X-Request-ID | correlation id, on every response including failures |
X-API-Key-Expires-At / -In-Days | present only when the key is within 7 days of expiry |
On a 429, back off for at least Retry-After seconds before retrying. Watching X-RateLimit-Remaining and slowing down as it approaches zero avoids ever hitting the limit.
Idempotency
Write operations that place calls accept an Idempotency-Key header so a retried request never triggers the action twice. It is honored on POST /calls, POST /campaigns/{id}/start, and POST /campaigns/{id}/dial.
- Send a unique key (up to 255 chars) per logical operation — a natural choice is your own record id, e.g.
order-4821-call-1. - A replay within ~24 hours returns the original response and adds
Idempotency-Replayed: trueinstead of dialing again. - A duplicate that arrives while the first is still in flight gets a
409— retry after the first settles.
Key rotation
A key can replace itself with zero downtime. POST /api-keys/{id}/rotate issues a new secret while the old key keeps working through a grace window, so you can roll the new value out before the old one stops:
curl -X POST https://api.vosy.ai/v2/api-keys/{id}/rotate \
-H "Authorization: Bearer $VOSY_KEY" \
-H "Content-Type: application/json" \
-d '{ "grace_period_hours": 24 }'
grace_period_hours defaults to 24 and accepts 1–168 (up to a week). Deploy the new key everywhere during the grace window, then let the old one expire. Rotation requires api-keys:write (or api-keys:manage) and is available on Business and Enterprise plans.
curl walkthroughs
Four end-to-end examples. Each assumes your key is in an environment variable: export VOSY_KEY=vosy_sk_....
1 · List your agents GET /agents
Agents are the voice personas that talk to your callers. Listing them needs agents:read. The endpoint paginates with limit (1–100, default 25) and offset, and filters by status.
curl "https://api.vosy.ai/v2/agents?status=active&limit=25" \
-H "Authorization: Bearer $VOSY_KEY"
{
"success": true,
"data": [ { "id": 42, "name": "Front Desk", "status": "active" } ],
"meta": { "limit": 25, "offset": 0 }
}
2 · Schedule an outbound call POST /calls
Place a single outbound call with calls:schedule. Only bot_id (the agent) and destination_phone (E.164) are required. Keys in custom_data fill the {{placeholders}} in the agent's prompt at dial time. The Idempotency-Key makes a retry safe.
curl -X POST https://api.vosy.ai/v2/calls \
-H "Authorization: Bearer $VOSY_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821-call-1" \
-d '{
"bot_id": "42",
"destination_phone": "+15551234567",
"contact_name": "Alex",
"custom_data": { "account_tier": "gold" }
}'
Poll GET /calls/{id} for status and results, or subscribe to call.* webhooks (below) so you don't have to poll. The full history, transcripts and recordings live under /call-logs — reading recordings needs the separate calls:recordings scope.
3 · Book an appointment POST /scheduling/appointments
Booking needs scheduling:write. The one required field is slot_start, and its format matters: it must be an RFC-3339 timestamp with an explicit offset (or a trailing Z). A naive local string with no offset is rejected with a 400. The safe source of a valid value is a start returned by GET /scheduling/availability.
curl -X POST https://api.vosy.ai/v2/scheduling/appointments \
-H "Authorization: Bearer $VOSY_KEY" \
-H "Content-Type: application/json" \
-d '{
"slot_start": "2026-08-05T14:00:00-04:00",
"attendee_name": "Jordan Lee",
"attendee_email": "jordan@example.com",
"attendee_phone": "+15551234567"
}'
Rescheduling returns a new id. PATCH /scheduling/appointments/{id} cancels the old row and creates a replacement, so the response's appointment_id is a new id — persist it, don't assume the old one still points at a live booking. Pass "require_approval": true on creation to hold the slot as pending until your team calls .../approve or .../decline; cancel a confirmed booking with DELETE.
4 · Register a webhook POST /webhooks
Subscribe an endpoint to events with webhooks:write. Provide a url and at least one event; the signing secret is returned once, only on creation — store it, you'll need it to verify deliveries.
curl -X POST https://api.vosy.ai/v2/webhooks \
-H "Authorization: Bearer $VOSY_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/vosy",
"events": [
"call.completed", "call.failed",
"appointment.booked", "appointment.rescheduled"
]
}'
{
"success": true,
"data": {
"id": 7,
"name": "example.com",
"url": "https://example.com/hooks/vosy",
"status": "active",
"secret": "a1b2c3d4…" // 64-char hex, shown only here
}
}
Webhooks
Instead of polling, subscribe to lifecycle events and Vosy will POST them to your endpoint. Each delivery body has a stable shape:
{
"event": "appointment.booked",
"timestamp": "2026-08-05T14:00:00+00:00",
"data": { ... }
}
Verifying signatures
Every delivery is signed with HMAC-SHA256 using your subscription secret. Two headers carry the proof:
| Header | Value |
|---|---|
X-Webhook-Timestamp | Unix seconds when the delivery was signed |
X-Webhook-Signature | hex HMAC-SHA256 of "{timestamp}.{raw_body}" |
To verify: take the raw request body exactly as received, build the string timestamp + "." + body using the X-Webhook-Timestamp value, compute HMAC-SHA256 with your secret, hex-encode it, and compare it to X-Webhook-Signature with a constant-time comparison. Reject the delivery on any mismatch. Do the check before parsing or trusting the payload.
Events
The catalog spans the call, campaign and appointment lifecycles — subscribe only to what you need:
- Calls —
call.started,call.answered,call.ended,call.completed,call.failed,call.transferred. - Campaigns —
campaign.started,campaign.paused,campaign.completed,campaign.failed, plus per-contactcontact.completed/contact.failed. - Appointments —
appointment.booked(a confirmed booking or an approved pending request),appointment.cancelled(a cancel or a decline), andappointment.rescheduled.appointment.bookedfires however the booking happened — including when a voice agent books mid-call — so your UI can update in real time.
Pausing a subscription
Stop deliveries without deleting the subscription — and without losing the secret — by setting its status:
curl -X PATCH https://api.vosy.ai/v2/webhooks/{id} \
-H "Authorization: Bearer $VOSY_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'
A paused subscription stops receiving events; set status back to active to resume. Use POST /webhooks/{id}/test to send yourself a sample event, and GET /webhooks/{id}/logs to inspect delivery attempts.
Next steps
- Generate a typed client from the OpenAPI 3.1 spec in your language of choice.
- Read the scope descriptions and presets at
GET /api-keys/scopesbefore you request a key's permissions. - Need a key? API access is available on Business and Enterprise plans — contact us and tell us what you're building.
Ready to build on Vosy?
API access is available on Business and Enterprise plans. Tell us what you're building and we'll get you a key and a scoped set of permissions.
Contact Sales