{"openapi":"3.1.0","info":{"title":"isahar Voice — Public API","version":"1.0.0","description":"Public API for data in your isahar Voice account: calls, agents, queues, phone numbers, and a limited set of controlled write operations (call notes and click-to-dial). Authenticate with an API key (Account → API Keys in the admin panel). The key secret is displayed only once, when the key is created; it cannot be retrieved later, only rotated. Write access requires explicit scopes (notes:write and calls:dial). These scopes are never granted by default to new keys; legacy keys without scopes are read-only, never unrestricted. Your account can also receive proactive notifications through webhooks—see the Webhooks section below.","license":{"name":"isahar Voice Proprietary License","url":"https://voice.isahar.io/en/terms-and-conditions/"}},"servers":[{"url":"https://api.voice.isahar.io","description":"Production"}],"security":[{"basicAuth":[]}],"tags":[{"name":"Meta","description":"Authentication checks and endpoint discovery."},{"name":"Calls","description":"Call history for the account."},{"name":"Notes","description":"Call notes—a controlled write operation requiring the notes:write scope."},{"name":"Agents","description":"Account agents and their current presence."},{"name":"Dialing","description":"Click-to-dial requests explicit agent confirmation; it never starts the call directly. Requires the calls:dial scope."},{"name":"Queues","description":"Account call queues."},{"name":"Numbers","description":"Account phone numbers."},{"name":"Contacts","description":"Bulk sync of account address-book contacts. Requires the contacts:write scope."},{"name":"Webhooks","description":"Notifications sent by isahar Voice to your URLs when account events occur. Configure them under Integrations → Webhooks."}],"paths":{"/v1/ping":{"get":{"tags":["Meta"],"summary":"Check authentication","description":"Confirms that the API key and secret are valid. No scope is required; any valid key can call this endpoint, regardless of its scopes.","operationId":"ping","responses":{"200":{"description":"Authentication successful.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingResponse"},"example":{"ok":true,"api_version":"v1","tenant_id":"acme"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/calls":{"get":{"tags":["Calls"],"summary":"List calls","description":"Requires the `calls:read` scope.","operationId":"listCalls","security":[{"basicAuth":[]}],"x-required-scopes":["calls:read"],"parameters":[{"name":"from","in":"query","description":"Start date (ISO 8601). Filters by started_at.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End date (ISO 8601). Filters by started_at.","schema":{"type":"string","format":"date-time"}},{"name":"direction","in":"query","schema":{"type":"string","enum":["INBOUND","OUTBOUND"]}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/CallStatus"}},{"name":"agent_id","in":"query","schema":{"type":"string"}},{"name":"queue_id","in":"query","schema":{"type":"string"}},{"name":"phone_number","in":"query","description":"E.164 format (for example, +40712345678). Searches both the external and internal numbers.","schema":{"type":"string"}},{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"order","in":"query","description":"Sort order by started_at.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"Paginated list of calls.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/calls/{id}":{"get":{"tags":["Calls"],"summary":"Get call details","description":"Requires the `calls:read` scope.","operationId":"getCall","security":[{"basicAuth":[]}],"x-required-scopes":["calls:read"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Requested call.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Call"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/calls/{call_id}/notes":{"post":{"tags":["Notes"],"summary":"Add a call note","description":"Requires the `notes:write` scope, which must be granted explicitly; it is not part of read access or the default access of legacy keys without scopes. Creates or replaces the call's current note using the same model as the web application (Call.metadata.note): one current version, with no edit history. The API does not support editing or deleting notes. Requires the `Idempotency-Key` header; see the dedicated section below.","operationId":"createCallNote","security":[{"basicAuth":[]}],"x-required-scopes":["notes:write"],"parameters":[{"$ref":"#/components/parameters/CallId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCallNoteRequest"},"example":{"content":"Customer requested a return, order #10234."}}}},"responses":{"201":{"description":"The note was saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallNoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/calls/{id}/recording-link":{"get":{"tags":["Calls"],"summary":"Temporary recording link","description":"Requires the `recordings:read` scope, granted explicitly; it is not part of general `calls:read` access because it exposes audio, not just metadata. Generates a signed link that expires in 1 hour and requires no authentication. Regenerate it on every display (for example, in an HTML5 player inside a grid) rather than caching it longer than that. This is different from the link placed automatically in the Magento order comment, which expires after 30 days and is generated only once.","operationId":"getRecordingLink","security":[{"basicAuth":[]}],"x-required-scopes":["recordings:read"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Link generated.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordingLinkResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/agents":{"get":{"tags":["Agents"],"summary":"List agents","description":"Requires the `agents:read` scope.","operationId":"listAgents","security":[{"basicAuth":[]}],"x-required-scopes":["agents:read"],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"Paginated list of agents.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Get agent details","description":"Requires the `agents:read` scope.","operationId":"getAgent","security":[{"basicAuth":[]}],"x-required-scopes":["agents:read"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Requested agent.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/agents/{agent_id}/dial":{"post":{"tags":["Dialing"],"summary":"Click-to-dial (requires agent confirmation)","description":"Requires the `calls:dial` scope, which must be granted explicitly and independently of `notes:write`; neither write scope grants the other. This endpoint does NOT start a call automatically. It creates a CALL REQUEST shown as a prompt in the agent application (\"Call\" / \"Decline\"). The call starts only after the agent explicitly selects \"Call\", using the agent softphone's existing outbound-call flow. The agent may decline the request, or it may expire without a response (30 seconds by default); no call starts in either case. A separate, stricter rate limit applies. Requires the `Idempotency-Key` header; see the dedicated section below.","operationId":"dialAgent","security":[{"basicAuth":[]}],"x-required-scopes":["calls:dial"],"parameters":[{"$ref":"#/components/parameters/AgentId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialAgentRequest"},"example":{"to":"+40712345678","from_number_id":"8f14e45f-4e0d-4c9a-9a4a-000000000000"}}}},"responses":{"202":{"description":"The call request has been sent to the agent and is awaiting confirmation. This does NOT mean that the call has started.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"X-RateLimit-Dial-Limit":{"$ref":"#/components/headers/DialRateLimitLimit"},"X-RateLimit-Dial-Remaining":{"$ref":"#/components/headers/DialRateLimitRemaining"},"X-RateLimit-Dial-Reset":{"$ref":"#/components/headers/DialRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DialRequestResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/DialWriteConflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/DialRateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/contacts/bulk-upsert":{"post":{"tags":["Contacts"],"summary":"Bulk create/update contacts","description":"Requires the `contacts:write` scope, granted explicitly. Useful for an initial customer sync from an external CRM or store. Maximum 100 contacts per request. Unlike the other write endpoints in this API, an invalid item in a batch does NOT reject the whole request with `400` — it is reported individually in `results`, with `status: \"skipped\"` and a reason, so a sync of thousands of customers does not stop at the first row with a missing or malformed phone number. An existing contact (same `phoneE164`) is updated, not duplicated — `data` is merged over any existing contact data rather than replacing it (it will not erase fields an agent added manually).","operationId":"bulkUpsertContacts","security":[{"basicAuth":[]}],"x-required-scopes":["contacts:write"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpsertContactsRequest"},"example":{"contacts":[{"phoneE164":"+40712345678","name":"Ion Popescu","email":"ion.popescu@exemplu.ro","externalId":"1042"}]}}}},"responses":{"200":{"description":"Batch processed (even if some elements were skipped; see `results`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpsertContactsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/queues":{"get":{"tags":["Queues"],"summary":"List queues","description":"Requires the `queues:read` scope.","operationId":"listQueues","security":[{"basicAuth":[]}],"x-required-scopes":["queues:read"],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"Paginated list of queues.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/queues/{id}":{"get":{"tags":["Queues"],"summary":"Get queue details","description":"Requires the `queues:read` scope.","operationId":"getQueue","security":[{"basicAuth":[]}],"x-required-scopes":["queues:read"],"parameters":[{"$ref":"#/components/parameters/Id"}],"responses":{"200":{"description":"Requested queue.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Queue"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/numbers":{"get":{"tags":["Numbers"],"summary":"List phone numbers","description":"Requires the `numbers:read` scope.","operationId":"listNumbers","security":[{"basicAuth":[]}],"x-required-scopes":["numbers:read"],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"Paginated list of phone numbers.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumberListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}},"webhooks":{"call.created":{"post":{"tags":["Webhooks"],"summary":"A new call was created","description":"Sent as soon as a new inbound or outbound call is recorded in the account—at the start of the call, not at the end. See the Webhooks guide at /docs/en/webhooks for signature verification, retries, and examples.","operationId":"callCreatedWebhook","security":[],"parameters":[{"$ref":"#/components/parameters/IsaharSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallCreatedEvent"},"example":{"id":"evt_9f1c2a3b4d5e6f708192a3b4","type":"call.created","version":"1","created_at":"2026-01-15T10:00:00.000Z","data":{"callId":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","direction":"INBOUND","from":"+40711111111","to":"+40788800000"}}}}},"responses":{"200":{"description":"Any 2xx status confirms delivery. Any other status, or a 10-second timeout, counts as a failure and triggers a retry."}}}},"call.ended":{"post":{"tags":["Webhooks"],"summary":"A call ended","description":"Sent when a call ends, regardless of the outcome (COMPLETED, MISSED, FAILED, or VOICEMAIL). Missed calls also trigger the separate `call.missed` event.","operationId":"callEndedWebhook","security":[],"parameters":[{"$ref":"#/components/parameters/IsaharSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallEndedEvent"},"example":{"id":"evt_1a2b3c4d5e6f708192a3b4c5","type":"call.ended","version":"1","created_at":"2026-01-15T10:03:12.000Z","data":{"callId":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","direction":"INBOUND","from":"+40711111111","to":"+40788800000","status":"COMPLETED"}}}}},"responses":{"200":{"description":"Any 2xx status confirms delivery. Any other status, or a 10-second timeout, counts as a failure and triggers a retry."}}}},"call.missed":{"post":{"tags":["Webhooks"],"summary":"An inbound call was missed","description":"Sent in addition to `call.ended` when an inbound call ends with the MISSED status because no one answered.","operationId":"callMissedWebhook","security":[],"parameters":[{"$ref":"#/components/parameters/IsaharSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallMissedEvent"},"example":{"id":"evt_5e6f708192a3b4c5d6e7f809","type":"call.missed","version":"1","created_at":"2026-01-15T10:03:12.000Z","data":{"callId":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","from":"+40711111111","to":"+40788800000"}}}}},"responses":{"200":{"description":"Any 2xx status confirms delivery. Any other status, or a 10-second timeout, counts as a failure and triggers a retry."}}}},"recording.ready":{"post":{"tags":["Webhooks"],"summary":"A call recording is available","description":"Sent when the call recording has been saved and is available. The payload contains only the call ID; download the recording from the application. The webhook does not expose a public file URL.","operationId":"recordingReadyWebhook","security":[],"parameters":[{"$ref":"#/components/parameters/IsaharSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordingReadyEvent"},"example":{"id":"evt_708192a3b4c5d6e7f809f1c2","type":"recording.ready","version":"1","created_at":"2026-01-15T10:03:20.000Z","data":{"callId":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8"}}}}},"responses":{"200":{"description":"Any 2xx status confirms delivery. Any other status, or a 10-second timeout, counts as a failure and triggers a retry."}}}}},"components":{"securitySchemes":{"basicAuth":{"type":"http","scheme":"basic","description":"Username = keyId (for example, key_xxxxx); password = the key secret. Both are displayed when the key is created. The secret is shown only once and cannot be retrieved later, only rotated. HTTP Basic cannot carry scopes in `security`, so each operation documents its required scope explicitly in its description and in the `x-required-scopes` extension."}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string"}},"CallId":{"name":"call_id","in":"path","required":true,"schema":{"type":"string"}},"AgentId":{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}},"Page":{"name":"page","in":"query","description":"Page number, starting at 1.","schema":{"type":"integer","minimum":1,"default":1}},"PerPage":{"name":"per_page","in":"query","description":"Results per page.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"description":"Required for every write operation. Choose a value (UUID v4 recommended) that is unique per logical operation. Retrying the SAME endpoint with the SAME value and the SAME API key does not repeat the action; it returns the first execution's exact response (status and body), regardless of the body sent with the retry. A concurrent request with the same value while the first is still processing receives `409 idempotency_key_in_progress`. A missing header returns `400 invalid_request`.","schema":{"type":"string","example":"8f14e45f-4e0d-4c9a-9a4a-6f9a2f0b7e21"}},"IsaharSignature":{"name":"X-Isahar-Signature","in":"header","required":true,"description":"Delivery signature. See the Webhooks guide at /docs/en/webhooks for the exact formula and Node.js/PHP verification examples.","schema":{"type":"string","example":"t=1700000000,v1=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d1"}}},"headers":{"RequestId":{"description":"Unique request identifier generated by the server. It matches `request_id` in error response bodies and is present on EVERY response, whether successful or not. Use it when contacting support.","schema":{"type":"string","example":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}},"RateLimitLimit":{"description":"Request limit per minute: the stricter of the per-key and per-account limits.","schema":{"type":"integer","example":60}},"RateLimitRemaining":{"description":"Number of requests remaining in the current 60-second window.","schema":{"type":"integer","example":42}},"RateLimitReset":{"description":"Unix timestamp, in seconds, when the current window resets.","schema":{"type":"integer","example":1700000060}},"DialRateLimitLimit":{"description":"Limit specific to POST /v1/agents/{agent_id}/dial. It is separate from and stricter than the general limit.","schema":{"type":"integer","example":10}},"DialRateLimitRemaining":{"description":"Number of click-to-dial requests remaining in the current 60-second window.","schema":{"type":"integer","example":7}},"DialRateLimitReset":{"description":"Unix timestamp, in seconds, when the current click-to-dial limit window resets.","schema":{"type":"integer","example":1700000060}},"RetryAfter":{"description":"Seconds until the rate limit that rejected this request resets.","schema":{"type":"integer","example":18}}},"responses":{"BadRequest":{"description":"Invalid parameters or request body.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"invalid_request","message":"per_page must be an integer between 1 and 200.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"Unauthorized":{"description":"Authentication is missing or invalid, or the key has been revoked. X-RateLimit-* headers are absent because authentication failed before rate limiting was evaluated.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"invalid_credentials","message":"The API key or secret is invalid.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"Forbidden":{"description":"The API key does not have the scope required by this endpoint.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"forbidden","message":"This API key does not have the required scope (calls:read).","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"NotFound":{"description":"The resource does not exist in this account. The response is identical for an unknown ID and a resource belonging to another account, so these cases cannot be distinguished.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"not_found","message":"The call does not exist in this account.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"RateLimited":{"description":"The general request limit has been exceeded. The default is 60 requests per minute, per key and per account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"rate_limit_exceeded","message":"You have exceeded the request limit. Try again later.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"DialRateLimited":{"description":"Either the general limit or the click-to-dial-specific limit has been exceeded. The default click-to-dial limit is 10 requests per minute.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"X-RateLimit-Dial-Limit":{"$ref":"#/components/headers/DialRateLimitLimit"},"X-RateLimit-Dial-Remaining":{"$ref":"#/components/headers/DialRateLimitRemaining"},"X-RateLimit-Dial-Reset":{"$ref":"#/components/headers/DialRateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"general_limit":{"summary":"General limit (60/minute)","value":{"error":{"code":"rate_limit_exceeded","message":"You have exceeded the request limit. Try again later.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}},"dial_limit":{"summary":"Click-to-dial-specific limit (10/minute)","value":{"error":{"code":"rate_limit_exceeded","message":"You have exceeded the click-to-dial request limit.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}}}},"IdempotencyConflict":{"description":"A request with the same Idempotency-Key is already being processed; the first execution has not finished.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"idempotency_key_in_progress","message":"A request with this Idempotency-Key is already being processed.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"DialWriteConflict":{"description":"An Idempotency-Key collision occurred, the agent is currently unavailable, or the agent already has an active call request that has not expired.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"idempotency_in_progress":{"summary":"Idempotency-Key already being processed","value":{"error":{"code":"idempotency_key_in_progress","message":"A request with this Idempotency-Key is already being processed.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}},"agent_unavailable":{"summary":"Agent unavailable (offline, disabled, or already on a call)","value":{"error":{"code":"agent_unavailable","message":"The agent is currently unavailable.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}},"dial_request_pending":{"summary":"Agent already has an unresolved active call request","value":{"error":{"code":"dial_request_pending","message":"An active call request already exists for this agent.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}}}},"UnprocessableEntity":{"description":"The source number (`from_number_id`) cannot be used for outbound calls because it does not have the `voice` capability.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"number_not_enabled_for_outbound","message":"This number cannot be used for outbound calls.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}},"InternalError":{"description":"Unexpected internal error. No internal technical details are exposed; use `request_id` if you need help from support.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"code":"internal_error","message":"An internal error occurred. Contact support and provide the request_id from this response.","request_id":"req_a1b2c3d4e5f6a1b2c3d4e5f6"}}}}}},"schemas":{"PingResponse":{"type":"object","required":["ok","api_version"],"properties":{"ok":{"type":"boolean"},"api_version":{"type":"string"},"tenant_id":{"type":"string"}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","description":"Stable error code for program logic. Build your integration around `code`, never the text in `message`, which may be localized or reworded.","enum":["invalid_request","invalid_credentials","forbidden","not_found","rate_limit_exceeded","internal_error","idempotency_key_in_progress","agent_unavailable","dial_request_pending","number_not_enabled_for_outbound"]},"message":{"type":"string","description":"Human-readable message for logs or integration UIs. It may be localized or reworded; do not use it for program logic."},"request_id":{"type":"string"}}}}},"PaginationMeta":{"type":"object","required":["page","per_page","total","next_page"],"properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"},"next_page":{"type":["integer","null"]}}},"CallStatus":{"type":"string","enum":["RINGING","IN_IVR","QUEUED","CONNECTED","COMPLETED","MISSED","FAILED","VOICEMAIL"]},"CallRef":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"Call":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["INBOUND","OUTBOUND"]},"status":{"$ref":"#/components/schemas/CallStatus"},"external_number":{"type":"string","description":"The other party's phone number, in E.164 format."},"internal_number":{"type":"string","description":"The account's own phone number involved in this call, in E.164 format."},"agent":{"oneOf":[{"$ref":"#/components/schemas/CallRef"},{"type":"null"}]},"queue":{"oneOf":[{"$ref":"#/components/schemas/CallRef"},{"type":"null"}]},"started_at":{"type":"string","format":"date-time"},"agent_answered_at":{"type":["string","null"],"format":"date-time","description":"The time the agent was actually connected (bridged), distinct from an IVR answer."},"ended_at":{"type":["string","null"],"format":"date-time"},"duration_seconds":{"type":["integer","null"]},"wait_seconds":{"type":["integer","null"],"description":"Time spent in the queue before the agent was connected."},"missed_reason":{"type":["string","null"],"enum":[null,"blacklisted","billing_blocked"]},"has_recording":{"type":"boolean"}}},"CallListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Call"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Agent":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"extension":{"type":"string"},"role":{"type":"string","enum":["ADMIN","AGENT"]},"active":{"type":"boolean"},"presence":{"type":"string","enum":["DISPONIBIL","CONFORM_PROGRAMULUI","IN_AFARA_PROGRAMULUI","MOTIV","INDISPONIBIL","IN_APEL"],"description":"The agent's current effective presence."},"active_call":{"oneOf":[{"type":"object","properties":{"call_id":{"type":"string"},"since":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}],"description":"Populated only while presence is IN_APEL; this value is not simulated."}}},"AgentListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Queue":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"member_count":{"type":"integer"}}},"QueueListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Queue"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"PhoneNumber":{"type":"object","properties":{"id":{"type":"string"},"e164":{"type":"string"},"name":{"type":["string","null"]},"number_type":{"type":["string","null"]},"capabilities":{"type":"array","items":{"type":"string"}},"has_ivr_flow":{"type":"boolean"}}},"PhoneNumberListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PhoneNumber"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CreateCallNoteRequest":{"type":"object","required":["content"],"properties":{"content":{"type":"string","maxLength":2000,"description":"Note text. Whitespace is trimmed automatically; an empty value after trimming returns 400."}}},"CallNoteCreatedBy":{"type":"object","properties":{"type":{"type":"string","enum":["api_key"]},"name":{"type":"string","description":"API key name, never the secret."}}},"CallNoteResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"call_id":{"type":"string"},"content":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"created_by":{"$ref":"#/components/schemas/CallNoteCreatedBy"}}}}},"DialAgentRequest":{"type":"object","required":["to","from_number_id"],"properties":{"to":{"type":"string","description":"E.164 number to call (for example, +40712345678)."},"from_number_id":{"type":"string","description":"ID of an account PhoneNumber to use as the source number."}}},"DialRequestResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"agent_id":{"type":"string"},"to":{"type":"string"},"from_number":{"type":"string"},"status":{"type":"string","enum":["pending"]},"expires_at":{"type":"string","format":"date-time"}}}}},"RecordingLinkResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","description":"Signed, expiring link to the recording; requires no authentication."},"expires_at":{"type":"string","format":"date-time","description":"Expires one hour after generation. Regenerate it on every display rather than caching it."}}}}},"BulkUpsertContactItem":{"type":"object","required":["phoneE164"],"properties":{"phoneE164":{"type":"string","description":"E.164 format (for example, +40712345678)."},"name":{"type":"string","maxLength":200},"email":{"type":"string","format":"email"},"externalId":{"type":"string","maxLength":100,"description":"The customer's ID in your own system (for example, a Magento customer ID)."},"data":{"type":"object","description":"Free-form additional fields, merged over any existing contact data (does not replace it)."}}},"BulkUpsertContactsRequest":{"type":"object","required":["contacts"],"properties":{"contacts":{"type":"array","maxItems":100,"minItems":1,"items":{"$ref":"#/components/schemas/BulkUpsertContactItem"}}}},"BulkUpsertContactResult":{"type":"object","properties":{"phone_e164":{"type":"string"},"status":{"type":"string","enum":["upserted","skipped"]},"reason":{"type":"string","description":"Present only when status is \"skipped\"."}}},"BulkUpsertContactsResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"upserted":{"type":"integer"},"total":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/BulkUpsertContactResult"}}}}}},"WebhookEventEnvelope":{"type":"object","description":"Common structure of every webhook delivery. See the event-specific `data` field in each event schema.","required":["id","type","version","created_at","data"],"properties":{"id":{"type":"string","description":"Unique event identifier, not a delivery identifier. If multiple active webhook URLs receive the event, they all receive the same `id`.","example":"evt_9f1c2a3b4d5e6f708192a3b4"},"type":{"type":"string","description":"Event type."},"version":{"type":"string","description":"Payload format version. Currently always \"1\".","example":"1"},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object"}}},"CallCreatedEventData":{"type":"object","required":["callId","direction","from","to"],"properties":{"callId":{"type":"string"},"direction":{"type":"string","enum":["INBOUND","OUTBOUND"]},"from":{"type":"string"},"to":{"type":"string"}}},"CallCreatedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEventEnvelope"},{"type":"object","properties":{"type":{"type":"string","const":"call.created"},"data":{"$ref":"#/components/schemas/CallCreatedEventData"}}}]},"CallEndedEventData":{"type":"object","required":["callId","direction","from","to","status"],"properties":{"callId":{"type":"string"},"direction":{"type":"string","enum":["INBOUND","OUTBOUND"]},"from":{"type":"string"},"to":{"type":"string"},"status":{"$ref":"#/components/schemas/CallStatus"}}},"CallEndedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEventEnvelope"},{"type":"object","properties":{"type":{"type":"string","const":"call.ended"},"data":{"$ref":"#/components/schemas/CallEndedEventData"}}}]},"CallMissedEventData":{"type":"object","required":["callId","from","to"],"properties":{"callId":{"type":"string"},"from":{"type":"string"},"to":{"type":"string"}}},"CallMissedEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEventEnvelope"},{"type":"object","properties":{"type":{"type":"string","const":"call.missed"},"data":{"$ref":"#/components/schemas/CallMissedEventData"}}}]},"RecordingReadyEventData":{"type":"object","required":["callId"],"properties":{"callId":{"type":"string"}}},"RecordingReadyEvent":{"allOf":[{"$ref":"#/components/schemas/WebhookEventEnvelope"},{"type":"object","properties":{"type":{"type":"string","const":"recording.ready"},"data":{"$ref":"#/components/schemas/RecordingReadyEventData"}}}]}}},"x-rate-limiting":{"description":"Each API key and tenant has its own configurable limit, with a default of 60 requests per minute. Click-to-dial (POST /v1/agents/{agent_id}/dial) also has a separate, stricter limit, with a default of 10 requests per minute. Limits are reported through the X-RateLimit-* headers; see components.headers. When a limit is exceeded, the 429 response also includes Retry-After in seconds."},"x-write-actions":"The currently available write operations are intentionally limited and require human confirmation where appropriate: call notes (`notes:write`, immediate) and click-to-dial (`calls:dial`, which requires explicit agent confirmation before dialing). The API never starts a call directly.","x-examples":{"curl":"curl -u \"key_xxxxx:your_secret\" https://api.voice.isahar.io/v1/calls"}}