openapi: 3.2.0 info: title: Voygr Calls Core API description: "\nProgrammatic outbound phone calls executed by an AI voice agent. Submit a task,\nthe agent places the call, conducts the conversation, and returns a structured\noutcome with a full transcript.\n\n## Authentication\n\nEvery request needs the `X-API-Key` header. Keys are issued per customer and\ncarry a credit quota, rate limits, and a concurrent-call limit.\n\n## Placing a call\n\n`POST /calls` accepts two request shapes — pick one per call.\n\n**Freeform** — describe the task in plain language:\n\n```bash\ncurl -s https://api.voygr.tech/calls -H \"X-API-Key: $API_KEY\" -H \"Content-Type: application/json\" -d '{\"target_phone\": \"+15551234567\",\n \"brief\": \"Ask what time the kitchen closes tonight.\",\n \"language\": \"en\"}'\n```\n\n**Structured** — a machine-readable `intent` plus `slots`, validated\ndeterministically before anything is dialed. Five intents are supported:\n\n| Intent | Required slots |\n|---|---|\n| `inquiry` | `target_phone`, `question` |\n| `info_gathering` | `target_phone`, `questions` |\n| `issue_resolution` | `target_phone`, `issue_description` |\n| `booking` | `target_phone`, `name`, `date`, `time`, `party_size` |\n| `cancellation` | `booking_id` — no phone; it comes from the stored booking |\n\nAn incomplete submission returns `422` listing exactly what is missing, each\nslot with a `suggested_question` to relay to your user — submit, collect,\nresubmit. Validation never dials and never charges. (The credit-hold check\nruns first, so a key that cannot cover the hold gets `402` even for an\nincomplete submission.)\n\n`language` accepts `en`, `es`, `fr`, `de`, `hi`, `ru`, `pt`, `ja`, `it`,\n`nl`, `sr`, `tr`, `pl`, or `auto` (the default); any other code is refused\nwith `422 unsupported_language`.\n\nThe success envelope differs by shape: freeform wraps the call object in\n`{\"call\": {...}}`; structured returns a flat envelope with top-level\n`call_id`, `status_url`, and `answer_url`. Both are documented with examples\non the operation below.\n\n## Call lifecycle\n\nCalls are asynchronous. Poll `GET /calls/{call_id}` until `status` reaches\n`completed`, `failed`, or `cancelled` — typically 30-90 seconds. The terminal\nresponse carries `outcome_type`, `outcome_summary`, and `transcript_full`.\nThere is no completion webhook.\n\nFor live progress, poll `GET /calls/{call_id}/events?after_event_id=N` with\nshort requests, advancing the cursor to the last `id:` you received — the\nbody is SSE-formatted text, but do NOT hold a long-lived stream open, and use\nthe query cursor (the gateway strips the `Last-Event-ID` header). If the\nagent needs input mid-call it emits an `ask_user` event — create the call\nwith `ask_user_mode: \"stream\"` to receive these, and respond via\n`POST /calls/{call_id}/answer`. A queued or ringing call can be aborted with\n`POST /calls/{call_id}/cancel`.\n\nOnce the call ends, `GET /calls/{call_id}/transcript-merged` returns the\ncomplete two-sided transcript rebuilt from the dual-channel recording —\nincluding speech the live pipeline never transcribed (IVR phone trees, hold\nannouncements). The live `transcript_full` stays real-time-only; the merged\none is the complete record. A `202 {\"status\": \"merger_pending\"}` means the\nmerge (which fires seconds after the call ends) hasn't finished — retry\nafter the `transcript_ready` SSE event, or force it with\n`POST /calls/{call_id}/transcript-merged/rebuild`.\n\n## Billing\n\nThe hold and the charge are different numbers:\n\n| Event | Credits |\n|---|---|\n| Hold at dial time (`credits_reserved`) | 30 — frozen, refundable, NOT a charge |\n| Successful outcome (`success_*`) — `credits_charge_on_success` | 10 |\n| Unsuccessful outcome (`failed_*` — no answer, voicemail, technical) | 0 |\n\nThe hold is released at completion and you are only ever charged the settled\namount. Requests are rejected with `402` when the available balance cannot\ncover the hold.\n\n`GET /v1/usage` reports `available` (spendable right now) and\n`call_credit_hold` (what one call freezes), so `available // call_credit_hold`\nis how many calls you can start.\n\n## Limits\n\n- Free keys: 5 requests/second, 10 requests/minute. Paid keys: 10\n requests/second, 100 requests/minute (`429` beyond).\n- Concurrent calls per key are limited (`409` with `active_call_ids` at the\n cap; typically 2).\n- Service maintenance windows return `503` with a `resume_at` timestamp.\n\n## Acceptable use\n\nCalls are transactional and user-initiated only: no telemarketing,\nsolicitation, bulk dialing, or harassment. The agent discloses that it is an\nAI assistant and that the call is recorded; disclosure is not configurable.\nUnited States destinations only. Recordings and transcripts are retained for\na maximum of 90 days.\n\n\nCredit rates: validation, enrichment, business status and categorize are 1 credit per request. An AI booking call is 10 credits. Credits never expire." version: 1.0.0 servers: - url: https://api.voygr.tech description: Production (stated by the provider in the spec description curl examples, api.voygr.tech/docs, and callwright-skill SKILL.md) tags: - name: core paths: /health: get: tags: - core summary: Health check description: Check if the API server is running operationId: health_check_health_get responses: '200': description: Successful Response content: application/json: schema: {} /v1/usage: get: tags: - core summary: Get API usage statistics description: Get current usage, quota, and reset date for the API key. operationId: get_usage_v1_usage_get parameters: - name: X-API-Key in: header required: false schema: anyOf: - type: string - type: 'null' title: X-Api-Key responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/UsageResponse' '401': description: Missing API key header content: application/json: example: success: false error: API key required. Include X-API-Key header. error_code: AUTHENTICATION_ERROR request_id: ad33009a-fdfe-4e25-9650-4a49099c3d4a schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Invalid, revoked, or unauthorized API key content: application/json: example: success: false error: Invalid or revoked API key error_code: AUTHENTICATION_ERROR request_id: 25b104f0-9ad0-4fc7-8530-c5d5ff8d9698 schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Validation Error content: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' components: schemas: HTTPValidationError: properties: detail: items: $ref: '#/components/schemas/ValidationError' type: array title: Detail type: object title: HTTPValidationError ErrorResponse: properties: success: type: boolean title: Success description: Always false for errors default: false error: type: string title: Error description: Error message error_code: anyOf: - type: string - type: 'null' title: Error Code description: Error category code (VALIDATION_ERROR, MODEL_ERROR, EXTERNAL_API_ERROR, etc.) request_id: anyOf: - type: string - type: 'null' title: Request Id description: Request ID for tracing validation_timestamp: anyOf: - type: string - type: 'null' title: Validation Timestamp description: UTC timestamp when error response was produced (ISO 8601). detail: anyOf: - additionalProperties: true type: object - type: 'null' title: Detail description: Additional error details type: object required: - error title: ErrorResponse description: Error response model. examples: - success: false error: API key required. Include X-API-Key header. error_code: AUTHENTICATION_ERROR request_id: ad33009a-fdfe-4e25-9650-4a49099c3d4a - success: false error: Invalid or revoked API key error_code: AUTHENTICATION_ERROR request_id: 25b104f0-9ad0-4fc7-8530-c5d5ff8d9698 ValidationError: properties: loc: items: anyOf: - type: string - type: integer type: array title: Location msg: type: string title: Message type: type: string title: Error Type input: title: Input ctx: type: object title: Context type: object required: - loc - msg - type title: ValidationError UsageResponse: properties: tier: type: string title: Tier description: API key tier (free, paid, enterprise) api_key_id: type: string title: Api Key Id description: API key ID customer_name: type: string title: Customer Name description: Customer name quota_limit: type: integer title: Quota Limit description: Maximum requests allowed per period current_usage: type: integer title: Current Usage description: Current usage count remaining: type: integer title: Remaining description: Remaining requests available: anyOf: - type: integer - type: 'null' title: Available description: Credits spendable right now — same value as `remaining`, named for the question callers ask. Already net of credits frozen by in-flight reservations. call_credit_hold: anyOf: - type: integer - type: 'null' title: Call Credit Hold description: Credits ONE outbound call freezes at dial time (a refundable hold, not a charge). available // call_credit_hold is the CREDIT-limited bound on calls in flight — entitlement and the per-customer concurrency cap gate independently, so it is not a guarantee that many will start. percentage_used: type: number title: Percentage Used description: Percentage of quota used reset_date: type: string title: Reset Date description: Date when usage resets (YYYY-MM-DD) period: type: string title: Period description: Quota period (monthly, weekly, daily) status: type: string title: Status description: API key status (active, revoked, suspended) validation_timestamp: anyOf: - type: string - type: 'null' title: Validation Timestamp description: UTC timestamp when usage response was produced (ISO 8601). type: object required: - tier - api_key_id - customer_name - quota_limit - current_usage - remaining - percentage_used - reset_date - period - status title: UsageResponse description: Response model for usage endpoint. examples: - api_key_id: f4d0a2c2-f73f-4de6-ac3e-f42ce35af4d6 customer_name: Acme Inc quota_limit: 10000 current_usage: 2375 remaining: 7625 percentage_used: 23.75 reset_date: '2026-03-31' period: monthly status: active - api_key_id: eaa59659-7fab-4d1f-a80d-4f1e0f9388e4 customer_name: Beta Foods quota_limit: 5000 current_usage: 5000 remaining: 0 percentage_used: 100.0 reset_date: '2026-03-01' period: monthly status: active