{"openapi":"3.1.3","info":{"title":"EchoCall Developer Platform - High-Performance AI Voice & Chat API","version":"1.16.0","contact":{"name":"EchoCall - Support","url":"https://echocall.de/support","email":"team@echocall.de"},"license":{"name":"Proprietary - EchoCall Global Terms","url":"https://echocall.de/terms-conditions"},"termsOfService":"https://echocall.de/terms-conditions","description":"\nWelcome to the **EchoCall Public API**. This platform is designed for developers, resellers, and enterprises to programmatically build, manage, and scale ultra-realistic AI voice agents and smart chatbots.\n\n### 🚀 Getting Started\nOur API follows RESTful principles and returns standard JSON responses. To start building, you'll need:\n1.  **An API Key**: Generate your `eck_live_...` key in the [EchoCall Developer Portal](https://hub.echocall.de/developer).\n2.  **Base URL**: Use `https://hub.echocall.de/api/v1` for all requests.\n\n### 🔐 Authentication & Security\nEchoCall uses **Bearer Token Authentication**. Send your API key in the `Authorization` header:\n```bash\ncurl -X GET \"https://hub.echocall.de/api/v1/agents\" \\\n     -H \"Authorization: Bearer YOUR_API_KEY\"\n```\n> [!IMPORTANT]\n> Keep your keys secure. A key grants full access to your account's data under its scopes.\n\n### 🧑‍💼 Acting on behalf of a customer\nReseller keys can run any customer-facing operation as one of their customers by adding the header `X-EchoCall-Customer: <customerId>`. The request is then treated exactly as if that customer had sent it: ownership, limits and usage are the customer's, and none of the reseller operations are reachable with the header set. This is how a self-hosted front end serves many customers with a single key. The customer id is the one returned by `POST /resellers/customers`.\n```bash\ncurl -X GET \"https://hub.echocall.de/api/v1/agents\" \\\n     -H \"Authorization: Bearer YOUR_RESELLER_KEY\" \\\n     -H \"X-EchoCall-Customer: 501\"\n```\n\n### 🎭 White-labeling & Reselling\nEchoCall is built with an **API-First White-labeling Strategy**. \n-   **Proprietary Models**: EchoCall utilizes its own high-performance models, `EchoCall-Voice` and `EchoCall-Smart`, specifically optimized for low-latency conversational AI.\n-   **Reseller Scopes**: Resellers can manage their own customers via the `/resellers` endpoints, adjusting balances, credits, and plans dynamically.\n\n### 🚦 Rate Limiting & Pagination\n-   **Rate Limits**: Limits are applied **per API key, per second** (`rateLimitPerSecond`, **500 requests per second** by default). Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Exceeding the limit returns **429** with `Retry-After: 1`.\n-   **Pagination**: Most list endpoints support `page` and `perPage` query parameters. `page` starts at 1; the default `perPage` varies per endpoint and the maximum `perPage` is **100**.\n\n### 🔔 Webhooks\nRegister an endpoint with `POST /webhooks` and EchoCall pushes events to it as they happen. `GET /webhooks/events` returns the full catalog; subscribe to `\"*\"` to receive everything.\n\n**Envelope.** Every delivery is a single JSON object:\n```json\n{\n  \"id\": \"evt_1f0c9a24-6f0e-4a1e-9f0c-2b6a1c1c1c1c\",\n  \"event\": \"call.ended\",\n  \"createdAt\": \"2026-01-31T09:12:44.512Z\",\n  \"data\": { }\n}\n```\n\n**Headers.**\n| Header | Meaning |\n| --- | --- |\n| `X-EchoCall-Event` | Event name, same value as `event` in the body. |\n| `X-EchoCall-Delivery` | Envelope id. Use it to make your handler idempotent. |\n| `X-EchoCall-Signature` | `t=<unix seconds>,v1=<hex>` (see below). |\n| `User-Agent` | `EchoCall-Webhooks/1.0` |\n\n**Signature verification.** `v1` is the HMAC-SHA256 of `${t}.${rawBody}`, keyed with the secret returned once when the webhook was created. Compute it over the **raw** request body, before any JSON parsing, and compare in constant time:\n```js\nimport crypto from 'crypto';\n\nfunction verify(secret, header, rawBody, toleranceSeconds = 300) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;\n  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(parts.v1 ?? '');\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\n**Delivery, retries and auto-disable.** Answer with any 2xx status within **10 seconds**; anything else counts as a failure. A failed delivery is retried three times, after **1s**, **5s** and **30s**. `lastDeliveryAt` and `lastDeliveryStatus` on the webhook record the final outcome of each event, `failedDeliveries` counts consecutive failures and resets to 0 on the next success. After **25** consecutive failures the endpoint is set to `isActive: false` and receives nothing until you re-enable it with `PATCH /webhooks/{id}`. Use `POST /webhooks/{id}/test` to check an endpoint at any time; that ping is sent once, with no retry.\n\n### Errors\n\nEvery failing request answers with the same envelope, whatever went wrong:\n\n```json\n{\n  \"error\": {\n    \"code\": \"validation_error\",\n    \"message\": \"Invalid request body\",\n    \"details\": { \"name\": { \"_errors\": [\"Required\"] } }\n  }\n}\n```\n\nBranch on `error.code`, never on `error.message`: the code is stable, the wording is not.\n`details` appears only on validation failures and carries the per-field report from the\nrequest schema.\n\n| Status | `error.code` | What it means |\n| --- | --- | --- |\n| 400 | `validation_error`, `invalid_id`, `invalid_date`, `bad_request` | The request could not be accepted as written. |\n| 401 | `unauthorized`, `invalid_api_key` | The `Authorization` header was missing, malformed, or the key is unknown, revoked or expired. |\n| 402 | `insufficient_balance` | The account wallet does not cover the purchase. |\n| 403 | `insufficient_permissions`, `forbidden` | The key is valid but lacks the required scope, or the endpoint is reseller-only. |\n| 404 | `not_found` | No such resource is visible to this key. Resources owned by another account are reported as missing. |\n| 409 | `conflict` | The resource exists but its state does not allow the operation. |\n| 412 | `provider_not_configured` | The account is not yet provisioned for the voice engine. |\n| 429 | `rate_limit_exceeded` | Too many requests. Wait for `Retry-After` seconds. |\n| 500 | `internal_error`, `database_error` | An unexpected server fault. Safe to retry. |\n| 502 | `upstream_error`, `sync_failed` | A dependency the endpoint needs answered badly. |\n| 503 | `service_unavailable`, `marketplace_disabled` | The feature is switched off for this deployment. |\n\nA 403 caused by a missing scope also carries `error.requiredScope`, naming the scope the key\nwould need.\n\n### Scopes\n\nEvery API key carries a set of scopes. Each operation names the one it requires in its\n`x-required-scope` extension, and self-service keys are currently issued with all of them.\nReseller endpoints (`/resellers`) and the provisioning hooks (`/provisioning`) carry a scope like\nevery other operation, and additionally require the key to belong to a reseller account: the\nscope alone does not make a key a partner key.\n\n| Scope | What it allows |\n| --- | --- |\n| `read:agents` | Read voice agents, their knowledge sources and their integration assignments. Also required for the voice library and for reading batch calling campaigns. |\n| `write:agents` | Create, change and delete voice agents, their knowledge and their integration assignments. Also required to create, submit, cancel and delete batch calling campaigns. |\n| `read:chatbots` | Read chatbots, their knowledge sources and their integration assignments. |\n| `write:chatbots` | Create, change and delete chatbots, their knowledge and their integration assignments. |\n| `read:phone-numbers` | List owned numbers, browse the marketplace and read regulatory verification state. |\n| `write:phone-numbers` | Buy, reassign, cancel numbers and submit regulatory verification documents. Purchases charge the account wallet. |\n| `read:conversations` | Read call records, chat conversations and chat transcripts. |\n| `write:conversations` | Send messages into a chat conversation, reassign it, archive it and close it. |\n| `read:analytics` | Read usage, revenue and per-agent performance aggregates. |\n| `read:webhooks` | List webhook endpoints and read the event catalog. |\n| `write:webhooks` | Register, change, test and delete webhook endpoints. |\n| `read:tickets` | Read support tickets and their message threads. |\n| `write:tickets` | Open tickets and post replies. |\n| `read:integrations` | List integrations, read one with its credentials masked, read the tool catalog, and read the bookings and event types of a calendar integration. |\n| `write:integrations` | Connect, change, test and remove integrations, and confirm, decline and cancel bookings in their calendars. |\n| `read:billing` | Read plans, invoices, subscription, wallet balance and transactions. |\n| `write:billing` | Start a checkout for a plan. |\n| `read:profile` | Read the account profile, its usage and limits, and the notification inbox. |\n| `write:profile` | Mark notifications read. |\n| `read:resellers` | Read partner data: customers, plans, subscriptions, invoices, analytics and settings. |\n| `write:resellers` | Change partner data and run the provisioning lifecycle hooks. |\n\n### Pagination\n\nList endpoints take `page` (1-based) and `perPage`. `perPage` is clamped to at most 100\neverywhere; the default differs per endpoint and is stated on each operation. The response\ncarries a `pagination` object with `page` and `perPage`, and with `total` only where the\nhandler runs a count query. Where `total` is absent, that is documented on the operation:\npage until you receive fewer items than you asked for.\n"},"servers":[{"url":"https://hub.echocall.de/api/v1","description":"Global Production Server (Germany-based, High Availability)"}],"tags":[{"name":"Agents","description":"High-performance AI Voice Agents powered by EchoCall-Voice models. Create, update, and manage conversational flows for phone and web.","x-attribute":"Core Feature"},{"name":"Chatbots","description":"Intelligent Web Chatbots using the EchoCall-Smart LLM. Includes widget customization, branding, and real-time event streaming.","x-attribute":"Core Feature"},{"name":"Tickets","description":"White-labeled customer support ticket system. Resellers can manage end-user issues seamlessly.","x-attribute":"Platform Service"},{"name":"Knowledge","description":"The \"Brain\" of your agents. Manage the internal knowledge base used by LLMs to provide factual, context-aware answers.","x-attribute":"Infrastructure"},{"name":"Voices","description":"Access the extensive EchoCall Voice Library. Customize tone, pitch, and stability for your AI agents.","x-attribute":"Assets"},{"name":"Billing","description":"Access real-time usage metrics, download PDF invoices, and manage subscription plans.","x-attribute":"Finance"},{"name":"Reseller","description":"The Command Center for Resellers. Create customers, manage credits/balances, and brand the platform as your own.","x-role":["reseller"]},{"name":"PhoneNumbers","description":"Buy and assign international phone numbers to your voice agents for direct PSTN connectivity.","x-attribute":"Connectivity"},{"name":"Conversations","description":"Full audit logs for all voice calls and chat sessions. Access recordings, transcripts, and metadata.","x-attribute":"Observability"},{"name":"Analytics","description":"Deep insights into usage, revenue, and agent performance. Essential for ROI tracking.","x-attribute":"Insights"},{"name":"Webhooks","description":"Real-time event notifications for your application. Listen for call-ended, message-sent, and ticket-updated events.","x-attribute":"Integration"},{"name":"Integrations","description":"Connect EchoCall to Cal.com, Calendly, Google Calendar, and more. Automate scheduling via AI.","x-attribute":"Ecosystem"},{"name":"Users","description":"Profile management and identity services.","x-attribute":"Identity"},{"name":"BatchCalling","description":"Outbound batch call campaigns. Create and dispatch mass-call jobs via AI voice agents to a list of recipients.","x-attribute":"Automation"},{"name":"Notifications","description":"User notification inbox. List unread events and mark them as read.","x-attribute":"Platform Service"},{"name":"Provisioning","description":"Account lifecycle hooks for automated reseller account management. Reseller API keys only.","x-role":["reseller"]},{"name":"Utilities","description":"Service health and localization helpers.","x-role":["public"]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"eck_live_<64 hex characters>","description":"Send the API key as a bearer token: `Authorization: Bearer eck_live_...`. That is the only accepted form; the header must start with `Bearer ` exactly, and the token must match `eck_(live|test)_[a-f0-9]{64}`. Keys are issued in the Developer Portal and carry a set of scopes; each operation names the scope it needs in its `x-required-scope` extension."}},"parameters":{"ActAsCustomer":{"name":"X-EchoCall-Customer","in":"header","required":false,"description":"Reseller keys only. Runs this request as the named customer of the reseller, exactly as if that customer had sent it with a key of their own: ownership checks, limits and usage all apply to the customer, and none of the reseller operations are reachable while it is set. The value is the customer id returned by `POST /resellers/customers` and listed by `GET /resellers/customers`. Errors: 400 `invalid_customer_header` (not a positive integer), 403 `act_as_requires_reseller` (not a reseller key), 404 `customer_not_found` (no customer of this reseller with that id), 403 `customer_suspended`. The `/resellers/*` and `/provisioning/*` operations never accept it.","schema":{"type":"integer","minimum":1,"example":501}}},"schemas":{"Error":{"type":"object","description":"The single error envelope every failing request answers with. There is no other error shape in this API.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable, machine-readable identifier for the failure. Branch on this, not on `message`.","examples":["validation_error","unauthorized","insufficient_permissions","not_found","conflict","rate_limit_exceeded","internal_error","customer_not_found","customer_suspended","tool_not_permitted"]},"message":{"type":"string","description":"Human-readable explanation. Wording may change between releases."},"details":{"description":"Present on validation failures: the per-field report from the request schema.","type":["object","array","null"],"additionalProperties":true}}}}},"Pagination":{"type":"object","description":"Paging information for a list response. Which keys are present varies by endpoint: `total` is only returned where the handler runs a separate count query, and it is documented per operation where it is absent.","properties":{"page":{"type":"integer","description":"The page that was returned. Starts at 1."},"perPage":{"type":"integer","description":"Items per page that was applied after clamping."},"total":{"type":"integer","description":"Total number of matching records across all pages. Absent on endpoints that do not count."}},"required":["page","perPage"]},"Agent":{"type":"object","description":"The full public configuration of a voice agent.","properties":{"id":{"type":"string","description":"Public agent identifier, of the form `agent_<number>`.","pattern":"^agent_[0-9]+$"},"name":{"type":"string","description":"Internal name of the agent."},"description":{"type":["string","null"],"description":"Free-text description."},"language":{"type":"string","description":"Primary BCP-47 language code."},"additionalLanguages":{"type":"array","description":"Further languages the agent will answer in. Empty when none are configured.","items":{"type":"string"}},"voice":{"type":"object","description":"The voice this agent speaks with.","properties":{"id":{"type":["string","null"],"description":"Voice identifier."},"name":{"type":["string","null"],"description":"Display name of the voice."}}},"systemPrompt":{"type":["string","null"],"description":"The instruction text that defines the agent behaviour."},"firstMessage":{"type":["string","null"],"description":"What the agent says before the caller does."},"temperature":{"type":["number","null"],"description":"Sampling temperature. Higher values produce more varied replies."},"llmModel":{"type":["string","null"],"description":"Identifier of the language model behind the agent."},"maxTokens":{"type":["integer","null"],"description":"Cap on the length of a single generated reply."},"enableInterruptions":{"type":"boolean","description":"Whether the caller can talk over the agent."},"enableBackchannel":{"type":"boolean","description":"Whether the agent emits listening noises while the caller speaks."},"enableFillers":{"type":"boolean","description":"Whether the agent uses filler words for a more natural rhythm."},"privacySettings":{"type":"object","description":"Recording and retention behaviour for this agent.","properties":{"saveCallAudio":{"type":"boolean","description":"Whether call audio is stored at all."},"zeroPiiRetention":{"type":"boolean","description":"When true, no personally identifying data is retained after the call."},"retentionDays":{"type":["integer","null"],"description":"How many days recordings and transcripts are kept."}}},"status":{"type":"string","enum":["active","inactive"],"description":"Whether the agent accepts calls."},"createdAt":{"type":"string","format":"date-time","description":"When the agent was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the agent was last changed."}},"required":["id","name","status"]},"AgentSummary":{"type":"object","description":"A voice agent as it appears in a list. Fewer fields than the detail view: no prompt, model or privacy settings.","properties":{"id":{"type":"string","description":"Public agent identifier, always of the form `agent_<number>`. Path parameters accept it with or without the prefix.","pattern":"^agent_[0-9]+$"},"name":{"type":"string","description":"Internal name of the agent."},"description":{"type":["string","null"],"description":"Free-text description."},"language":{"type":"string","description":"Primary BCP-47 language code the agent speaks, for example `de` or `en`."},"voice":{"type":"object","description":"The voice this agent speaks with.","properties":{"id":{"type":["string","null"],"description":"Voice identifier, as used by the /voices endpoints."},"name":{"type":["string","null"],"description":"Display name of the voice."}}},"status":{"type":"string","enum":["active","inactive"],"description":"Whether the agent accepts calls."},"createdAt":{"type":"string","format":"date-time","description":"When the agent was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the agent was last changed."}},"required":["id","name","status"]},"CreateAgent":{"type":"object","properties":{"name":{"type":"string","description":"The unique identifier name for your voice agent. Shown in the EchoCall dashboard."},"language":{"type":"string","description":"ISO 639-1 language code (e.g., \"en\", \"de\"). This determines the base model and pronunciation rules."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All supported languages as ISO-639-1 codes. First element = primary language. E.g. [\"de\", \"en\", \"fr\"]."},"systemPrompt":{"type":"string","description":"Instructions that define the agent's personality, knowledge, and behavior. Use clear, concise commands."},"firstMessage":{"type":"string","description":"The very first sentence the agent speaks when a call/chat begins. Supports SSML-like cues."},"voiceId":{"type":"string","description":"The unique ID of the EchoCall voice to be used for speech synthesis."},"temperature":{"type":"number","description":"Controls randomness. 0 is deterministic/focused, 100 is highly creative/varied."},"llmModel":{"type":"string","default":"EchoCall-Voice","description":"The advanced LLM \"brain\" powering the agent. Options: EchoCall-Voice, EchoCall-Smart."},"maxTokens":{"type":"number","default":1000,"description":"Upper limit on the number of tokens the agent can generate per response."},"ttsModel":{"type":"string","enum":["echocall-flash","echocall-flash-lite","echocall-ultra","echocall-multilingual"],"description":"Text-to-speech model. Leave unset for automatic selection based on `language`. `echocall-flash`: fast, multilingual (recommended default). `echocall-flash-lite`: fastest, English only. `echocall-ultra`: most expressive/natural, supports emotional audio tags, widest language coverage. `echocall-multilingual`: balanced multilingual quality. Each model supports a different set of languages for `language`/`supportedLanguages` - pick a model that covers your target language(s), otherwise the request may fail."},"stability":{"type":"number","description":"Voice stability (0.0-1.0). 0.3 = expressive/varied, 0.7 = stable/consistent. Default: 0.5."},"speed":{"type":"number","description":"Speaking speed multiplier (0.7-1.2). 0.9 = slower/clearer, 1.0 = normal, 1.1 = faster."},"turnEagerness":{"type":"string","enum":["patient","normal","eager"],"description":"How quickly the agent responds: patient (waits longer), normal, eager (responds fast)."},"enableInterruptions":{"type":"boolean","default":true,"description":"If true, the agent will stop speaking immediately when the user starts talking."},"enableBackchannel":{"type":"boolean","default":true,"description":"Enables realistic listening sounds like \"mhm\", \"yeah\", \"I see\" during user speech."},"enableFillers":{"type":"boolean","default":true,"description":"Allows the agent to use natural filler words like \"well\", \"actually\" to sound more human."},"backgroundVoiceDetection":{"type":"boolean","description":"If true, the agent detects and ignores background noise/voices."},"asrKeywords":{"type":"array","items":{"type":"string"},"description":"Keywords for improved ASR recognition (product names, brand names, etc.)."},"saveCallAudio":{"type":"boolean","default":false,"description":"Whether to record and store the full audio of the conversation for later review."},"zeroPiiRetention":{"type":"boolean","default":false,"description":"Privacy mode. If enabled, PII data is stripped and logs are not permanently stored."},"retentionDays":{"type":"number","default":30,"description":"Number of days to keep conversation logs. Defaults to 30. Set to -1 for permanent storage."},"systemToolsConfig":{"type":"object","properties":{"endCall":{"type":"boolean","default":false},"voicemailDetection":{"type":"boolean","default":false},"languageDetection":{"type":"boolean","default":false},"playKeypadTouchTone":{"type":"boolean","default":false},"skipTurn":{"type":"boolean","default":false}},"description":"System tool toggles (endCall, voicemailDetection, languageDetection, playKeypadTouchTone, skipTurn)."},"transferNumbers":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","default":"Transfer to this number"}},"required":["number","name"]},"description":"Phone numbers the agent can transfer calls to."}},"required":["name","language"],"description":"Create Voice Agent Request Configuration"},"UpdateAgent":{"type":"object","properties":{"name":{"type":"string","description":"Name of the voice agent"},"language":{"type":"string","description":"ISO 639-1 language code"},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All supported languages as ISO-639-1 codes."},"systemPrompt":{"type":"string","description":"Instructions for the AI behavior"},"firstMessage":{"type":"string","description":"Initial message spoken by the agent"},"voiceId":{"type":"string","description":"EchoCall Voice ID"},"temperature":{"type":"number","description":"Creativity level (0-100)"},"llmModel":{"type":"string","description":"LLM Model to use"},"maxTokens":{"type":"number","description":"Max tokens per response"},"ttsModel":{"type":"string","enum":["echocall-flash","echocall-flash-lite","echocall-ultra","echocall-multilingual"],"description":"Text-to-speech model. Leave unset for automatic selection based on `language`. `echocall-flash`: fast, multilingual (recommended default). `echocall-flash-lite`: fastest, English only. `echocall-ultra`: most expressive/natural, supports emotional audio tags, widest language coverage. `echocall-multilingual`: balanced multilingual quality. Each model supports a different set of languages for `language`/`supportedLanguages` - pick a model that covers your target language(s), otherwise the request may fail."},"stability":{"type":"number","description":"Voice stability (0.0-1.0)."},"speed":{"type":"number","description":"Speaking speed (0.7-1.2)."},"turnEagerness":{"type":"string","enum":["patient","normal","eager"],"description":"How quickly the agent responds."},"enableInterruptions":{"type":"boolean","description":"Allow user to interrupt the agent"},"enableBackchannel":{"type":"boolean","description":"Enable backchanneling"},"enableFillers":{"type":"boolean","description":"Enable filler words"},"backgroundVoiceDetection":{"type":"boolean","description":"Detect and ignore background noise."},"asrKeywords":{"type":"array","items":{"type":"string"},"description":"Keywords for improved ASR recognition."},"saveCallAudio":{"type":"boolean","description":"Record and save call audio"},"zeroPiiRetention":{"type":"boolean","description":"Do not store PII"},"retentionDays":{"type":"number","description":"Days to keep logs (-1 = forever)"},"status":{"type":"string","enum":["active","inactive"],"description":"Agent status"},"systemToolsConfig":{"type":"object","properties":{"endCall":{"type":"boolean"},"voicemailDetection":{"type":"boolean"},"languageDetection":{"type":"boolean"},"playKeypadTouchTone":{"type":"boolean"},"skipTurn":{"type":"boolean"}},"description":"System tool toggles."},"transferNumbers":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","default":"Transfer to this number"}},"required":["number","name"]},"description":"Phone numbers the agent can transfer calls to."}},"description":"Update Agent Request"},"Chatbot":{"type":"object","description":"The full public configuration of a chatbot, including widget appearance and retention settings.","properties":{"id":{"type":"integer","description":"Numeric chatbot identifier."},"name":{"type":"string","description":"Internal name of the chatbot."},"chatbotDisplayName":{"type":["string","null"],"description":"Name shown to visitors in the widget header."},"language":{"type":"string","description":"Primary BCP-47 language code."},"systemPrompt":{"type":["string","null"],"description":"The instruction text that defines the chatbot behaviour."},"greeting":{"type":["string","null"],"description":"Opening message shown when a visitor starts a chat."},"primaryColor":{"type":["string","null"],"description":"Widget accent colour as a CSS hex value."},"logoUrl":{"type":["string","null"],"description":"Logo shown in the widget header.","format":"uri"},"widgetPosition":{"type":["string","null"],"description":"Corner the launcher is anchored to."},"widgetSize":{"type":["string","null"],"description":"Size preset of the widget."},"widgetStyle":{"type":["string","null"],"description":"Visual style preset of the widget."},"teaserMessage":{"type":["string","null"],"description":"Message shown next to the closed launcher."},"widgetLanguage":{"type":["string","null"],"description":"Language of the widget interface, independent of the bot language."},"allowedDomains":{"type":["array","null"],"description":"Domains permitted to embed this widget. Null means no restriction.","items":{"type":"string"}},"supportedLanguages":{"type":["array","null"],"description":"Languages a visitor can switch the conversation to. Null means the primary language only.","items":{"type":"string"}},"enableVoice":{"type":"boolean","description":"Whether visitors can talk to the chatbot through the widget microphone."},"voiceId":{"type":["string","null"],"description":"The voice the chatbot speaks with, from GET /voices or GET /voices/available. Null means the engine default voice."},"voiceName":{"type":["string","null"],"description":"Display name of that voice, as it was known when the voice was chosen."},"textOnlyMode":{"type":"boolean","deprecated":true,"description":"The inverse of `enableVoice`, kept for clients written against 1.12. Read `enableVoice` instead."},"saveConversations":{"type":"boolean","description":"Whether transcripts are stored."},"zeroPiiRetention":{"type":"boolean","description":"When true, no personally identifying data is retained."},"retentionDays":{"type":["integer","null"],"description":"How many days transcripts are kept."},"llmModel":{"type":["string","null"],"description":"Identifier of the language model behind the chatbot."},"status":{"type":"string","enum":["active","inactive"],"description":"Whether the chatbot answers."},"publicUrl":{"type":["string","null"],"description":"Hosted chat page for this bot.","format":"uri"},"createdAt":{"type":"string","format":"date-time","description":"When the chatbot was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the chatbot was last changed."}},"required":["id","name","status"]},"ChatbotSummary":{"type":"object","description":"A chatbot as it appears in a list.","properties":{"id":{"type":"integer","description":"Numeric chatbot identifier. Unlike agents, chatbot ids carry no prefix."},"name":{"type":"string","description":"Internal name of the chatbot."},"chatbotDisplayName":{"type":["string","null"],"description":"Name shown to visitors in the widget header."},"language":{"type":"string","description":"Primary BCP-47 language code."},"systemPrompt":{"type":["string","null"],"description":"The instruction text that defines the chatbot behaviour."},"greeting":{"type":["string","null"],"description":"Opening message shown when a visitor starts a chat."},"status":{"type":"string","enum":["active","inactive"],"description":"Whether the chatbot answers."},"publicUrl":{"type":["string","null"],"description":"Hosted chat page for this bot.","format":"uri"},"createdAt":{"type":"string","format":"date-time","description":"When the chatbot was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the chatbot was last changed."}},"required":["id","name","status"]},"CreateChatbot":{"type":"object","properties":{"name":{"type":"string","description":"Internal name of the chatbot for identification in the dashboard."},"chatbotDisplayName":{"type":"string","description":"The name shown to end-users in the chat widget header."},"language":{"type":"string","default":"de","description":"Primary language for the chatbot (ISO 639-1). Affects NLP and default greetings."},"systemPrompt":{"type":"string","description":"Base instructions defining the chatbot's personality and scope of knowledge."},"greeting":{"type":"string","description":"The first message a user sees when opening the chat widget."},"primaryColor":{"type":"string","default":"#3B82F6","description":"The main brand color (Hex) used for the chat bubble and header."},"logoUrl":{"type":"string","description":"URL to a custom logo image for the chat widget."},"widgetPosition":{"type":"string","enum":["bottom-right","bottom-left","top-right","top-left"],"default":"bottom-right","description":"Where the chat bubble appears on the user's screen."},"widgetSize":{"type":"string","enum":["small","medium","large"],"default":"medium","description":"The relative size of the chat window."},"widgetStyle":{"type":"string","enum":["standard","rounded","compact"],"default":"rounded","description":"The visual variant of the chat interface."},"teaserMessage":{"type":"string","description":"Marketing hook shown in the proactive teaser bubble before the visitor opens the chat. Independent from `greeting`. Empty = no teaser bubble."},"widgetLanguage":{"type":"string","description":"Overrides the widget UI chrome language (buttons, placeholders, aria-labels), independent of the conversation `language`. Empty/omitted = automatic, follows `language`."},"allowedDomains":{"type":"array","items":{"type":"string"},"description":"Hostnames allowed to embed this widget (Referer-checked, fail-closed). Empty/omitted = unrestricted."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All languages the chatbot can converse in, as ISO-639-1 codes. First element = primary language."},"enableVoice":{"type":"boolean","description":"Lets visitors talk to the chatbot through the widget microphone. A spoken conversation runs on the voice allowance of the account. Defaults to false."},"voiceId":{"type":"string","description":"The voice the chatbot speaks with, from GET /voices or GET /voices/available. Omit for the engine default voice. Only used while `enableVoice` is true."},"textOnlyMode":{"type":"boolean","description":"Deprecated: the inverse of `enableVoice`, kept for clients written against 1.12. `false` switches the voice on. Ignored when `enableVoice` is present."},"saveConversations":{"type":"boolean","default":true,"description":"Whether to store chat history in the EchoCall database."},"zeroPiiRetention":{"type":"boolean","default":false,"description":"Strict privacy mode. Stored data will be anonymized."},"retentionDays":{"type":"number","default":30,"description":"Auto-deletion period for chat logs in days. Defaults to 30. -1 for infinity."},"llmModel":{"type":"string","default":"EchoCall-Smart","description":"The LLM brain powering the chat. Options: EchoCall-Smart, EchoCall-Voice."},"clientEvents":{"type":"object","properties":{"audio":{"type":"boolean","default":true,"description":"Enable audio events for voice-capable widgets."},"interruption":{"type":"boolean","default":true,"description":"Enable interruption handling."},"user_transcript":{"type":"boolean","default":true,"description":"Stream user transcripts back to the client."},"agent_response":{"type":"boolean","default":true,"description":"Stream agent responses in real-time."}},"description":"Real-time event configuration for SDK/Frontend integration."}},"required":["name"],"description":"Create Chatbot Request Configuration"},"UpdateChatbot":{"type":"object","properties":{"name":{"type":"string","description":"Name of the chatbot"},"chatbotDisplayName":{"type":"string","description":"Display name shown in the chat widget"},"language":{"type":"string","description":"Primary language for the chatbot (ISO 639-1). Affects NLP and default greetings."},"systemPrompt":{"type":"string","description":"System instructions for the chatbot behavior"},"greeting":{"type":"string","description":"Initial greeting message"},"primaryColor":{"type":"string","description":"Primary accent color"},"logoUrl":{"type":"string","description":"URL to chatbot logo"},"widgetPosition":{"type":"string","enum":["bottom-right","bottom-left","top-right","top-left"],"description":"Position on screen"},"widgetSize":{"type":"string","enum":["small","medium","large"],"description":"Size of the chat widget"},"widgetStyle":{"type":"string","enum":["standard","rounded","compact"],"description":"Visual style variant"},"teaserMessage":{"type":"string","description":"Marketing hook shown in the proactive teaser bubble before the visitor opens the chat. Independent from `greeting`. Empty = no teaser bubble."},"widgetLanguage":{"type":"string","description":"Overrides the widget UI chrome language (buttons, placeholders, aria-labels), independent of the conversation `language`. Empty/omitted = automatic, follows `language`."},"allowedDomains":{"type":"array","items":{"type":"string"},"description":"Hostnames allowed to embed this widget (Referer-checked, fail-closed). Empty/omitted = unrestricted."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All languages the chatbot can converse in, as ISO-639-1 codes. First element = primary language."},"status":{"type":"string","enum":["active","inactive"],"description":"Chatbot status"},"enableVoice":{"type":"boolean","description":"Switches the widget microphone on or off. The chosen voice is kept while it is off."},"voiceId":{"type":"string","description":"The voice the chatbot speaks with, from GET /voices or GET /voices/available. An empty string returns to the engine default voice."},"textOnlyMode":{"type":"boolean","description":"Deprecated: the inverse of `enableVoice`, kept for clients written against 1.12. Ignored when `enableVoice` is present."},"saveConversations":{"type":"boolean","description":"Save chat history"},"zeroPiiRetention":{"type":"boolean","description":"Do not store PII"},"retentionDays":{"type":"number","description":"Days to keep logs (-1 = forever)"},"llmModel":{"type":"string","description":"LLM Model for the brain"}},"description":"Update Chatbot Request"},"KnowledgeEntry":{"type":"object","description":"A knowledge source attached to a voice agent.","properties":{"id":{"type":"integer","description":"Identifier of the knowledge entry."},"type":{"type":"string","enum":["url","text"],"description":"Whether the source was supplied as a link to crawl or as literal text."},"title":{"type":["string","null"],"description":"Label for the entry."},"sourceUrl":{"type":"string","description":"The crawled URL. Omitted for text entries.","format":"uri"},"status":{"type":"string","description":"Indexing state of the entry."},"createdAt":{"type":"string","format":"date-time","description":"When the entry was added."}},"required":["id","type","status"]},"ChatbotKnowledgeEntry":{"type":"object","description":"A knowledge source attached to a chatbot.","properties":{"id":{"type":"integer","description":"Identifier of the knowledge entry."},"type":{"type":"string","enum":["url","text","file"],"description":"How the source was supplied."},"title":{"type":["string","null"],"description":"Label for the entry."},"contentSnippet":{"type":"string","description":"The first 100 characters of the stored content. The full text is not returned by the list endpoint."},"fileName":{"type":["string","null"],"description":"Original file name, for entries added as a file."},"createdAt":{"type":"string","format":"date-time","description":"When the entry was added."}},"required":["id","type"]},"AddKnowledge":{"type":"object","properties":{"type":{"type":"string","enum":["url","text","file"]},"content":{"type":"string"},"title":{"type":"string"},"fileName":{"type":"string"},"mimeType":{"type":"string"}},"required":["type","content"]},"PhoneNumber":{"type":"object","description":"A phone number on the account. Wholesale cost columns, carrier identifiers and SIP trunk credentials are never included.","properties":{"id":{"type":"integer","description":"Identifier of the phone number record."},"phoneNumber":{"type":"string","description":"The number itself, in E.164 form."},"friendlyName":{"type":["string","null"],"description":"Label shown in the dashboard."},"country":{"type":["string","null"],"description":"Country name."},"countryCode":{"type":["string","null"],"description":"Dialling country code."},"areaCode":{"type":["string","null"],"description":"Area or city code, where the number type has one."},"numberType":{"type":["string","null"],"description":"Category of the number, for example `local`, `national`, `mobile` or `tollfree`."},"voiceAgentId":{"type":["integer","null"],"description":"Numeric id of the voice agent answering on this number, or null when unassigned."},"ownerId":{"type":["integer","null"],"description":"User the number is assigned to. Null for reseller inventory that is not yet handed out."},"supportsInbound":{"type":"boolean","description":"Whether the number can receive calls."},"supportsOutbound":{"type":"boolean","description":"Whether the number can place calls."},"monthlyPrice":{"type":"string","description":"What the account is charged per month for this number. Returned as a decimal string, not a float."},"status":{"type":"string","description":"Lifecycle state of the number, for example `active` or `cancelled`."},"kycStatus":{"type":"string","enum":["not_required","pending","approved","rejected"],"description":"Regulatory verification state. Numbers in some countries cannot carry traffic until this is `approved`."},"purchasedAt":{"type":["string","null"],"format":"date-time","description":"When the number was bought."},"canceledAt":{"type":["string","null"],"format":"date-time","description":"When cancellation was requested, if it was."},"cancelationEndDate":{"type":["string","null"],"format":"date-time","description":"Last day the number stays usable after cancellation."},"createdAt":{"type":"string","format":"date-time","description":"When the record was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the record last changed."}},"required":["id","phoneNumber","status"]},"MarketplaceNumber":{"type":"object","description":"A number offered for purchase. Pass the whole object back to POST /phone-numbers/purchase; the purchase prices the number itself and ignores any price field in the body. `setupPrice` and `monthlyPrice` are what the account will be charged, net of VAT. Wholesale costs are not published.","properties":{"availableDidId":{"type":"string","description":"Marketplace identifier of this listing. Required to purchase."},"number":{"type":"string","description":"The number in E.164 form."},"countryIso":{"type":"string","description":"Two-letter ISO country code.","minLength":2,"maxLength":2},"prefix":{"type":"string","description":"Dialling prefix of the country."},"areaName":{"type":["string","null"],"description":"City or region the number belongs to."},"features":{"type":"array","description":"Capabilities of the number, for example `voice_in` or `sms_in`.","items":{"type":"string"}},"setupPrice":{"type":"number","description":"One-off charge to buy the number, in EUR net."},"monthlyPrice":{"type":"number","description":"Recurring monthly charge, in EUR net."},"skuId":{"type":"string","description":"Marketplace SKU. Required to purchase."},"didGroupId":{"type":"string","description":"Marketplace group the number belongs to."},"numberType":{"type":"string","enum":["local","national","mobile","tollfree","shared_cost","uifn"],"description":"Category of the number."},"requirementId":{"type":["string","null"],"description":"Identifier of the regulatory requirement set that must be satisfied before the number carries traffic. Null when no verification is needed."}},"required":["availableDidId","number","countryIso","setupPrice","monthlyPrice","skuId","numberType"]},"Country":{"type":"object","description":"A country whose numbers can be bought through the marketplace.","properties":{"id":{"type":"string","description":"Marketplace identifier of the country."},"name":{"type":"string","description":"English country name."},"iso":{"type":"string","description":"Two-letter ISO country code. This is the value to pass as `countryIso`.","minLength":2,"maxLength":2},"prefix":{"type":"string","description":"International dialling prefix."}},"required":["id","name","iso"]},"KycRequirement":{"type":"object","description":"What a buyer must prove before a regulated number can carry traffic. Null when the requirement set could not be read from the registry.","properties":{"requirementId":{"type":"string","description":"Identifier of the requirement set."},"identityType":{"type":"string","description":"Which identity kinds the requirement accepts."},"personalAreaLevel":{"type":["string","null"],"description":"Geographic scope demanded of a personal identity."},"businessAreaLevel":{"type":["string","null"],"description":"Geographic scope demanded of a business identity."},"addressAreaLevel":{"type":["string","null"],"description":"Geographic scope demanded of the address."},"personalProofQty":{"type":["integer","null"],"description":"How many personal identity documents are required."},"businessProofQty":{"type":["integer","null"],"description":"How many business documents are required."},"addressProofQty":{"type":["integer","null"],"description":"How many address documents are required."},"personalMandatoryFields":{"type":"array","description":"Fields that must be filled for a personal identity.","items":{"type":"string"}},"businessMandatoryFields":{"type":"array","description":"Fields that must be filled for a business identity.","items":{"type":"string"}},"serviceDescriptionRequired":{"type":"boolean","description":"Whether a description of the intended service is demanded."},"restrictionMessage":{"type":["string","null"],"description":"Registry note about who may hold this number."},"personalProofTypes":{"type":"array","description":"Accepted personal document types.","items":{"type":"object","description":"A document type.","properties":{"id":{"type":"string","description":"Identifier to pass as `proofTypeId`."},"name":{"type":"string","description":"Human-readable name."}}}},"businessProofTypes":{"type":"array","description":"Accepted business document types.","items":{"type":"object","description":"A document type.","properties":{"id":{"type":"string","description":"Identifier to pass as `proofTypeId`."},"name":{"type":"string","description":"Human-readable name."}}}},"addressProofTypes":{"type":"array","description":"Accepted address document types.","items":{"type":"object","description":"A document type.","properties":{"id":{"type":"string","description":"Identifier to pass as `proofTypeId`."},"name":{"type":"string","description":"Human-readable name."}}}},"personalPermanentDocument":{"type":["object","null"],"description":"A form that must be signed once for personal identities.","properties":{"id":{"type":"string","description":"Document id."},"name":{"type":"string","description":"Document name."},"url":{"type":"string","description":"Where to download it.","format":"uri"}}},"businessPermanentDocument":{"type":["object","null"],"description":"A form that must be signed once for business identities.","properties":{"id":{"type":"string","description":"Document id."},"name":{"type":"string","description":"Document name."},"url":{"type":"string","description":"Where to download it.","format":"uri"}}},"personalOnetimeDocument":{"type":["object","null"],"description":"A form that must be signed per number for personal identities.","properties":{"id":{"type":"string","description":"Document id."},"name":{"type":"string","description":"Document name."},"url":{"type":"string","description":"Where to download it.","format":"uri"}}},"businessOnetimeDocument":{"type":["object","null"],"description":"A form that must be signed per number for business identities.","properties":{"id":{"type":"string","description":"Document id."},"name":{"type":"string","description":"Document name."},"url":{"type":"string","description":"Where to download it.","format":"uri"}}}}},"Conversation":{"type":"object","description":"A voice call. These are the columns of the call record that are public: the carrier call identifier and the internal credit cost are not.","properties":{"type":{"type":"string","const":"voice","description":"Discriminator. Always `voice` for a call record."},"id":{"type":"integer","description":"Identifier of the call."},"voiceAgentId":{"type":["integer","null"],"description":"Numeric id of the agent that handled the call."},"phoneNumberId":{"type":["integer","null"],"description":"Identifier of the phone number the call used."},"direction":{"type":"string","enum":["inbound","outbound"],"description":"Who placed the call."},"fromNumber":{"type":["string","null"],"description":"Calling party, in E.164 form."},"toNumber":{"type":["string","null"],"description":"Called party, in E.164 form."},"duration":{"type":["integer","null"],"description":"Length of the call in seconds. Null while the call is still running."},"status":{"type":["string","null"],"description":"Outcome of the call as reported by the carrier."},"recordingUrl":{"type":["string","null"],"description":"Where the recording can be fetched. Null when recording is off or retention has expired.","format":"uri"},"transcription":{"type":["string","null"],"description":"Full transcript of the call, when one was produced."},"startedAt":{"type":["string","null"],"format":"date-time","description":"When the call connected."},"endedAt":{"type":["string","null"],"format":"date-time","description":"When the call finished."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."}},"required":["type","id","direction"]},"LiveConversation":{"type":"object","description":"A chat conversation, from the widget or from WhatsApp.","properties":{"type":{"type":"string","const":"chat","description":"Discriminator. Always `chat` for a conversation record."},"id":{"type":"integer","description":"Identifier of the conversation."},"chatbotId":{"type":["integer","null"],"description":"Chatbot handling the conversation. Null for WhatsApp threads with no bot attached."},"channel":{"type":"string","enum":["widget","whatsapp"],"description":"Where the conversation is taking place."},"integrationId":{"type":["integer","null"],"description":"Integration the WhatsApp thread arrived through. Null for widget conversations."},"status":{"type":"string","enum":["waiting","active","closed"],"description":"Where the conversation stands."},"visitorId":{"type":"string","description":"Anonymous session identifier for the visitor."},"visitorName":{"type":["string","null"],"description":"Name the visitor gave, if any."},"visitorEmail":{"type":["string","null"],"description":"Email the visitor gave, if any."},"visitorLocation":{"type":["string","null"],"description":"City and country derived from the visitor IP."},"visitorBrowser":{"type":["string","null"],"description":"Browser parsed from the user agent."},"visitorDevice":{"type":["string","null"],"description":"Device class parsed from the user agent."},"assignedAgentId":{"type":["integer","null"],"description":"Human agent handling the conversation. Null while unassigned."},"firstResponseTime":{"type":["integer","null"],"description":"Seconds between the first visitor message and the first reply."},"avgResponseTime":{"type":["integer","null"],"description":"Mean seconds between a visitor message and its reply."},"totalMessages":{"type":"integer","description":"Number of messages exchanged."},"duration":{"type":["integer","null"],"description":"Length of the conversation in seconds, set when it closes."},"isArchived":{"type":"boolean","description":"Whether the conversation has been archived out of the inbox."},"startedAt":{"type":"string","format":"date-time","description":"When the visitor opened the conversation."},"assignedAt":{"type":["string","null"],"format":"date-time","description":"When a human agent took it over."},"closedAt":{"type":["string","null"],"format":"date-time","description":"When it was closed."},"lastActivityAt":{"type":["string","null"],"format":"date-time","description":"Last message, heartbeat or disconnect."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When the record last changed."}},"required":["type","id","channel","status"]},"LiveMessage":{"type":"object","description":"One message in a chat conversation.","properties":{"id":{"type":"integer","description":"Identifier of the message."},"conversationId":{"type":"integer","description":"Conversation the message belongs to."},"senderType":{"type":"string","enum":["visitor","agent","bot"],"description":"Who sent it."},"senderId":{"type":["integer","null"],"description":"Human agent id, when a human sent it."},"senderName":{"type":["string","null"],"description":"Display name of the sender."},"message":{"type":"string","description":"The message text."},"fileUrl":{"type":["string","null"],"description":"Attachment location, when the message carries one.","format":"uri"},"fileName":{"type":["string","null"],"description":"Original attachment file name."},"fileType":{"type":["string","null"],"description":"Attachment MIME type."},"fileSize":{"type":["integer","null"],"description":"Attachment size in bytes."},"isRead":{"type":"boolean","description":"Whether the message has been read."},"createdAt":{"type":"string","format":"date-time","description":"When it was sent."}},"required":["id","conversationId","senderType","message","createdAt"]},"ConversationTranscriptMessage":{"type":"object","description":"A message as returned inside a conversation detail response. Narrower than the LiveMessage returned by the messages endpoint: no read flag, sender id or file size.","properties":{"id":{"type":"integer","description":"Identifier of the message."},"senderType":{"type":"string","enum":["visitor","agent","bot"],"description":"Who sent it."},"senderName":{"type":["string","null"],"description":"Display name of the sender."},"message":{"type":"string","description":"The message text."},"fileUrl":{"type":["string","null"],"description":"Attachment location.","format":"uri"},"fileName":{"type":["string","null"],"description":"Original attachment file name."},"fileType":{"type":["string","null"],"description":"Attachment MIME type."},"createdAt":{"type":"string","format":"date-time","description":"When it was sent."}},"required":["id","senderType","message","createdAt"]},"SendMessage":{"type":"object","properties":{"message":{"type":"string","description":"The text sent to the visitor as a human agent reply."}},"required":["message"],"description":"Send a message to a conversation"},"UpdateConversation":{"type":"object","properties":{"status":{"type":"string","enum":["active","closed","waiting"],"description":"New conversation status."},"isArchived":{"type":"boolean","description":"Archive or unarchive the conversation."},"assignedAgentId":{"type":["number","null"],"description":"Live chat agent to assign, or null to unassign."}},"description":"Update conversation status or assignment"},"Integration":{"type":"object","description":"A single integration, with its stored configuration. Every secret value inside `config` is replaced by `********`, so this response tells you which fields are set without revealing them. A value counts as secret when its field is declared with type `password` for the integration type, when its key looks like a secret (the name contains `secret`, `password`, `passwd`, `token`, `apiKey`, `api_key`, `authorization`, `authValue`, `credential`, `bearer` or `cookie`, or ends in `Key`), or when it is a URL that carries its secret inside it, such as the webhook URL of a Discord integration. Booleans are never masked. Internal connector and health bookkeeping is not returned, and neither are the connection keys only the server writes into `config`. For an integration type with tool permissions, `config` may carry `toolPermissions`, the stored choice of GET /integrations/{id}/permissions; it is read-only here and dropped when sent with POST or PATCH.","properties":{"id":{"type":"integer","description":"Identifier of the integration."},"type":{"type":"string","description":"Which third-party service it connects to."},"name":{"type":"string","description":"Name given to this connection."},"description":{"type":["string","null"],"description":"Free-text description."},"config":{"type":["string","null"],"description":"The stored configuration, as a JSON string. Secret values are masked to `********`, at any depth; null when the stored value could not be parsed and therefore could not be checked for secrets. Sending a masked value back with PATCH keeps the stored secret."},"setupGuideSteps":{"type":["string","null"],"description":"Setup instructions, as a JSON string."},"providerSetupUrl":{"type":["string","null"],"description":"Where to obtain the credentials this integration needs.","format":"uri"},"estimatedSetupTime":{"type":["integer","null"],"description":"Rough setup time in minutes."},"isActive":{"type":"boolean","description":"Whether agents may call it."},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"Last time an agent called it."},"createdAt":{"type":"string","format":"date-time","description":"When it was connected."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","type","name","isActive"]},"IntegrationSummary":{"type":"object","description":"An integration as it appears in a list. The stored configuration is not included.","properties":{"id":{"type":"integer","description":"Identifier of the integration."},"name":{"type":"string","description":"Name given to this connection."},"type":{"type":"string","description":"Which third-party service it connects to."},"isActive":{"type":"boolean","description":"Whether agents may call it."},"description":{"type":["string","null"],"description":"Free-text description."},"createdAt":{"type":"string","format":"date-time","description":"When it was connected."},"lastUsedAt":{"type":["string","null"],"format":"date-time","description":"Last time an agent called it."}},"required":["id","name","type","isActive"]},"IntegrationType":{"type":"object","description":"One entry of the catalog of supported integration types, including the fields its configuration needs.","properties":{"type":{"type":"string","description":"Value to pass as `type` when creating an integration of this kind."},"name":{"type":"string","description":"Display name of the service."},"description":{"type":"string","description":"What the integration does."},"icon":{"type":["string","null"],"description":"Icon identifier."},"category":{"type":["string","null"],"description":"Grouping used in the dashboard, for example `calendar` or `crm`."},"requiresApiKey":{"type":"boolean","description":"Whether a credential must be supplied in `config`."},"configFields":{"type":"array","description":"The fields this type expects inside the `config` JSON string.","items":{"type":"object","description":"One configuration field.","properties":{"name":{"type":"string","description":"Key to use inside `config`."},"label":{"type":"string","description":"Human-readable label."},"type":{"type":"string","enum":["text","password","url","select","number","textarea"],"description":"How the value should be collected. Fields of type `password` are masked in responses, together with every other key that looks like a secret."},"placeholder":{"type":["string","null"],"description":"Example value."},"required":{"type":"boolean","description":"Whether the field must be set."},"options":{"type":["array","null"],"description":"Allowed values, for fields of type `select`.","items":{"type":"object","description":"One option.","properties":{"label":{"type":"string","description":"Display text."},"value":{"type":"string","description":"Stored value."}}}}}}},"documentation":{"type":["string","null"],"description":"Link to the service documentation.","format":"uri"}},"required":["type","name","description"]},"CatalogTool":{"type":"object","description":"One action an agent can be allowed to perform through an integration.","properties":{"name":{"type":"string","description":"Tool name. This is the value to put into `enabledTools`."},"description":{"type":"string","description":"What the tool does."},"category":{"type":"string","description":"Grouping within the integration."},"access":{"type":"string","enum":["read","write","delete","admin"],"description":"How far the tool can go. Grant `write`, `delete` and `admin` deliberately."},"permitted":{"type":"boolean","description":"Whether the permissions of the integration (GET /integrations/{id}/permissions) allow agents to use this tool. Only a permitted tool can be put into `enabledTools`; any other is refused with 422 `tool_not_permitted`. Present for every integration type with tool permissions."}},"required":["name","description","access"]},"IntegrationPermissions":{"type":"object","description":"Which tools the agents of the account may use through one integration. The account owner keeps full access through the dashboard and the calendar endpoints; these permissions limit what agents may do in conversations.","properties":{"integrationId":{"type":"integer","description":"Identifier of the integration."},"type":{"type":"string","description":"Which third-party service the integration connects to."},"preset":{"type":"string","enum":["read","read_write","full","custom","legacy"],"description":"The stored choice. `legacy` means nothing was chosen yet, and agents keep the tools they had before permissions existed. `custom` together with `invalid` true means the stored choice could not be read, and no tool is allowed."},"invalid":{"type":"boolean","description":"Whether the stored choice could not be read. When true, agents may use no tool of this integration until a new choice is saved."},"tools":{"type":"array","description":"Every tool of the integration, with whether agents may use it now.","items":{"type":"object","description":"One tool.","properties":{"key":{"type":"string","description":"The tool key. This is the value to send in `tools` with a `custom` choice."},"label":{"type":"string","description":"Readable name of the tool."},"description":{"type":"string","description":"What the tool does."},"descriptionSource":{"type":"string","enum":["catalog","agent"],"description":"Where `description` comes from. `catalog` is a short text about the tool. `agent` is the instruction the agent receives for the tool, which can be long and may be written in German."},"access":{"type":"string","enum":["read","write","delete","admin"],"description":"How far the tool can go."},"recommended":{"type":"boolean","description":"Whether the tool is part of the recommended default set."},"allowed":{"type":"boolean","description":"Whether agents may use the tool with the stored choice."}},"required":["key","label","description","descriptionSource","access","recommended","allowed"]}},"presets":{"type":"object","description":"How many tools each preset allows for this integration. A preset that allows no tool is `disabled` and cannot be saved.","properties":{"read":{"type":"object","description":"The `read` preset.","properties":{"count":{"type":"integer","description":"Number of tools the preset allows."},"disabled":{"type":"boolean","description":"Whether the preset allows no tool and therefore cannot be chosen."}},"required":["count","disabled"]},"read_write":{"type":"object","description":"The `read_write` preset.","properties":{"count":{"type":"integer","description":"Number of tools the preset allows."},"disabled":{"type":"boolean","description":"Whether the preset allows no tool and therefore cannot be chosen."}},"required":["count","disabled"]},"full":{"type":"object","description":"The `full` preset.","properties":{"count":{"type":"integer","description":"Number of tools the preset allows."},"disabled":{"type":"boolean","description":"Whether the preset allows no tool and therefore cannot be chosen."}},"required":["count","disabled"]}},"required":["read","read_write","full"]},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the choice was last saved; null when nothing was chosen yet or the stored choice could not be read."},"defaultPreset":{"type":"string","enum":["read","read_write","full"],"description":"The preset a new connection of this type starts with."}},"required":["integrationId","type","preset","invalid","tools","presets","updatedAt","defaultPreset"]},"UpdateIntegrationPermissions":{"type":"object","properties":{"preset":{"type":"string","enum":["read","read_write","full","custom"],"description":"`read` allows the reading tools, `read_write` adds the writing tools, `full` allows every tool, `custom` allows exactly the tools listed in `tools`."},"tools":{"type":"array","items":{"type":"string"},"description":"The tool keys agents may use, as listed under `tools[].key` by GET /integrations/{id}/permissions. Required and non-empty for `custom`, ignored otherwise."}},"required":["preset"],"description":"Which tools the agents of the account may use through this integration"},"CalendarBooking":{"type":"object","description":"One booking in the calendar of an integration.","properties":{"id":{"type":"string","description":"Identifier of the booking at the calendar provider. This is the value the confirm, decline and cancel paths take: the booking uid for Cal.com, the event id for the other calendars."},"integrationId":{"type":"integer","description":"Identifier of the integration the booking comes from."},"provider":{"type":"string","description":"Integration type the booking comes from, for example `calcom` or `google_calendar`."},"title":{"type":"string","description":"Title of the booking."},"description":{"type":["string","null"],"description":"Description of the booking, if any."},"startTime":{"type":"string","format":"date-time","description":"Start of the booking."},"endTime":{"type":"string","format":"date-time","description":"End of the booking."},"status":{"type":"string","enum":["accepted","pending","cancelled","rejected"],"description":"State of the booking. `pending` is a request that waits for the host to confirm or decline it."},"attendees":{"type":"array","description":"The people booked in, as the provider reports them.","items":{"type":"object","description":"One attendee.","properties":{"name":{"type":"string","description":"Name of the attendee."},"email":{"type":"string","description":"Email address of the attendee."},"timeZone":{"type":["string","null"],"description":"Time zone of the attendee, for example `Europe/Berlin`."}},"required":["name","email","timeZone"]}},"location":{"type":["string","null"],"description":"Where the booking takes place: an address, a phone number or a meeting link."},"meetingUrl":{"type":["string","null"],"description":"Link to the video meeting, if the booking has one."},"responses":{"type":["object","null"],"description":"Answers the attendee gave in the booking form, keyed by field. The fields depend on how the event type is set up.","additionalProperties":true}},"required":["id","integrationId","provider","title","description","startTime","endTime","status","attendees","location","meetingUrl","responses"]},"CalendarEventType":{"type":"object","description":"One kind of appointment that can be booked in the calendar of an integration.","properties":{"id":{"type":["integer","string"],"description":"Identifier of the event type at the calendar provider."},"title":{"type":"string","description":"Name of the event type."},"description":{"type":["string","null"],"description":"Description of the event type, if any."},"length":{"type":"integer","description":"Duration of an appointment in minutes."},"slug":{"type":["string","null"],"description":"Short name of the event type in booking links, if the provider has one."}},"required":["id","title","length"]},"AssignedIntegration":{"type":"object","description":"An integration assigned to one agent or chatbot, with that agent tool selection. It always belongs to the account that owns the agent or chatbot. The stored configuration is not included.","properties":{"assignmentId":{"type":"integer","description":"Identifier of the assignment itself, not of the integration."},"assignedAt":{"type":"string","format":"date-time","description":"When the integration was assigned."},"id":{"type":"integer","description":"Identifier of the integration."},"type":{"type":"string","description":"Which third-party service it connects to."},"name":{"type":"string","description":"Name given to this connection."},"description":{"type":["string","null"],"description":"Free-text description."},"isActive":{"type":"boolean","description":"Whether the integration itself is enabled."},"healthStatus":{"type":["string","null"],"description":"Last known connectivity state of the integration."},"enabledTools":{"type":["array","null"],"description":"The exact tools this agent may call, or null when the recommended default set applies.","items":{"type":"string"}},"toolsMode":{"type":"string","enum":["recommended","custom","none"],"description":"How to read `enabledTools`: `recommended` means the default set for this integration type, `custom` means exactly the listed tools, `none` means the agent may call nothing."}},"required":["assignmentId","id","type","name","toolsMode"]},"CreateIntegration":{"type":"object","properties":{"type":{"type":"string","enum":["calcom","calendly","google_calendar","google_meet","google","microsoft","google_drive","google_sheets","gmail","hubspot","salesforce","webhook","zapier","make","slack","discord","zendesk","servicenow","twilio","whatsapp","shopify","amazon","ebay","msteams","outlook","smtp","notion","zoom","airtable","mailchimp","n8n","jira","confluence","shopware","freshdesk","custom"]},"name":{"type":"string"},"description":{"type":"string"},"config":{"type":"string"}},"required":["type","name","config"],"description":"Schema for creating a new integration"},"UpdateIntegration":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"config":{"type":"string"},"isActive":{"type":"boolean"}},"description":"Schema for updating an existing integration"},"Ticket":{"type":"object","description":"A support ticket with its full message thread.","properties":{"id":{"type":"string","description":"Public ticket identifier, of the form `ticket_<number>`.","pattern":"^ticket_[0-9]+$"},"subject":{"type":"string","description":"One-line summary of the issue."},"description":{"type":"string","description":"The full text the reporter wrote."},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"],"description":"Where the ticket stands."},"category":{"type":"string","enum":["technical","billing","feature","other"],"description":"What the ticket is about."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"How urgent it is."},"createdAt":{"type":"string","format":"date-time","description":"When it was opened."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."},"messages":{"type":"array","description":"The public thread, oldest first.","items":{"$ref":"#/components/schemas/TicketMessage"}}},"required":["id","subject","status","messages"]},"TicketSummary":{"type":"object","description":"A support ticket as it appears in a list.","properties":{"id":{"type":"string","description":"Public ticket identifier, of the form `ticket_<number>`. Path parameters accept it with or without the prefix.","pattern":"^ticket_[0-9]+$"},"subject":{"type":"string","description":"One-line summary of the issue."},"description":{"type":"string","description":"The full text the reporter wrote."},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"],"description":"Where the ticket stands."},"category":{"type":"string","enum":["technical","billing","feature","other"],"description":"What the ticket is about."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"How urgent it is."},"createdAt":{"type":"string","format":"date-time","description":"When it was opened."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","subject","status"]},"TicketMessage":{"type":"object","description":"One reply on a support ticket. Internal notes are never returned.","properties":{"id":{"type":"integer","description":"Identifier of the message."},"message":{"type":"string","description":"The reply text."},"attachments":{"type":"array","description":"Files attached to the reply. Empty when there are none.","items":{"type":"object","description":"An attachment.","properties":{"filename":{"type":"string","description":"Original file name."},"url":{"type":"string","description":"Where to download it.","format":"uri"},"mimeType":{"type":"string","description":"Content type."},"size":{"type":"integer","description":"Size in bytes."}}}},"createdAt":{"type":"string","format":"date-time","description":"When the reply was posted."},"sender":{"type":"object","description":"Who wrote it.","properties":{"name":{"type":"string","description":"Display name, or `System` for platform-generated messages."},"role":{"type":["string","null"],"description":"Role of the sender in the account."}}}},"required":["id","message","createdAt","sender"]},"CreateTicket":{"type":"object","properties":{"subject":{"type":"string","description":"A concise summary of the issue or request."},"description":{"type":"string","description":"Full details of the support request. Use this to provide context, error messages, or steps to reproduce."},"category":{"type":"string","enum":["technical","billing","feature","other"],"default":"other","description":"The functional area this ticket relates to."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium","description":"The urgency level. Urgent tickets are prioritized for immediate reseller/admin attention."}},"required":["subject","description"],"description":"Create Support Ticket Configuration"},"AddTicketMessage":{"type":"object","properties":{"message":{"type":"string","description":"The content of your reply or update."},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","description":"The name of the uploaded file."},"url":{"type":"string","description":"The public URL where the attachment is hosted."},"mimeType":{"type":"string","description":"The logical file type (e.g., image/png, application/pdf)."},"size":{"type":"number","description":"File size in bytes."}},"required":["filename","url","mimeType","size"]},"description":"Optional list of file attachments supporting the message."}},"required":["message"],"description":"Add Message to Ticket Configuration"},"Webhook":{"type":"object","description":"A registered outbound webhook endpoint. The signing secret is returned once, at creation, and never again.","properties":{"id":{"type":"integer","description":"Identifier of the endpoint."},"userId":{"type":"integer","description":"Account the endpoint belongs to."},"url":{"type":"string","description":"Where deliveries are sent.","format":"uri"},"events":{"type":"string","description":"The subscription list, stored and returned as a JSON array string, for example `[\"call.ended\",\"*\"]`. Parse it before use."},"isActive":{"type":"boolean","description":"Whether deliveries are being attempted. Set to false automatically after 25 consecutive failures."},"failedDeliveries":{"type":"integer","description":"Consecutive failures. Reset to 0 by the next success."},"lastDeliveryAt":{"type":["string","null"],"format":"date-time","description":"When the last delivery was attempted."},"lastDeliveryStatus":{"type":["string","null"],"description":"Outcome of the last delivery: `success`, or `failed:<status or reason>`."},"createdAt":{"type":"string","format":"date-time","description":"When the endpoint was registered."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","url","events","isActive"]},"WebhookEvent":{"type":"object","description":"One entry of the outbound event catalog.","properties":{"event":{"type":"string","enum":["call.ended","conversation.message.received","conversation.message.sent","conversation.closed","ticket.created","ticket.updated","webhook.test"],"description":"The event name to put in a subscription list."},"description":{"type":"string","description":"When the platform emits it."}},"required":["event","description"]},"Notification":{"type":"object","description":"One entry in the account notification inbox.","properties":{"id":{"type":"integer","description":"Identifier of the notification."},"type":{"type":"string","description":"What kind of event produced it."},"title":{"type":"string","description":"Headline."},"message":{"type":"string","description":"Body text."},"isRead":{"type":"boolean","description":"Whether it has been marked read."},"relatedType":{"type":["string","null"],"description":"Kind of resource it points at, for example `ticket`."},"relatedId":{"type":["integer","null"],"description":"Identifier of that resource."},"createdAt":{"type":"string","format":"date-time","description":"When it was raised."},"readAt":{"type":["string","null"],"format":"date-time","description":"When it was marked read."}},"required":["id","type","title","message","isRead"]},"Voice":{"type":"object","description":"A voice stored on the account.","properties":{"id":{"type":"string","description":"Voice identifier, of the form `voice_<number>`.","pattern":"^voice_[0-9]+$"},"name":{"type":"string","description":"Display name."},"description":{"type":["string","null"],"description":"Free-text description."},"type":{"type":"string","enum":["cloned","premade","generated"],"description":"Where the voice came from."},"status":{"type":"string","enum":["ready","processing","failed"],"description":"Whether the voice can be used yet."},"createdAt":{"type":"string","format":"date-time","description":"When it was added."}},"required":["id","name","type","status"]},"AvailableVoice":{"type":"object","description":"A voice from the shared library that an agent can be pointed at.","properties":{"id":{"type":"string","description":"Voice identifier to set on an agent."},"name":{"type":"string","description":"Display name."},"category":{"type":["string","null"],"description":"Where the voice came from."},"previewUrl":{"type":["string","null"],"description":"Where to hear this voice: the path of GET /voices/{voiceId}/preview for it, relative to the API host. Add a `language` parameter to hear it in the language the agent will speak."},"labels":{"type":["object","null"],"description":"Descriptive attributes such as language, accent, age and use case.","additionalProperties":true},"description":{"type":["string","null"],"description":"Free-text description."}},"required":["id","name"]},"Language":{"type":"object","description":"A user interface locale the platform ships.","properties":{"code":{"type":"string","description":"BCP-47 locale code, for example `de`, `en` or `fr`."}},"required":["code"]},"AgentLanguage":{"type":"object","description":"A language an agent can speak or write, named for display.","properties":{"code":{"type":"string","description":"The value to send as `language` or inside `supportedLanguages`, for example `pt-br`."},"name":{"type":"string","description":"English name of the language, for example `Portuguese (Brazil)`."},"nativeName":{"type":"string","description":"The name the language calls itself, for example `Português (Brasil)`."},"flag":{"type":"string","description":"ISO 3166-1 alpha-2 region code for a flag image, for example `br`. Empty when no region fits."}},"required":["code","name","nativeName","flag"]},"UserProfile":{"type":"object","description":"The account behind the API key.","properties":{"id":{"type":"integer","description":"User identifier."},"email":{"type":["string","null"],"description":"Login email.","format":"email"},"name":{"type":["string","null"],"description":"Display name."},"role":{"type":"string","description":"Role of the user in the platform."},"avatarUrl":{"type":["string","null"],"description":"Always null. No avatar is stored for API consumers."},"defaultLanguage":{"type":["string","null"],"description":"Preferred interface language."},"resellerId":{"type":["integer","null"],"description":"The reseller this account is a customer of, when it is one."},"ownResellerId":{"type":["integer","null"],"description":"The reseller this account operates, when it operates one."}},"required":["id","role"]},"UserProfileInput":{"type":"object","properties":{"id":{"type":"number"},"email":{"type":"string"},"name":{"type":["string","null"]},"role":{"type":"string","enum":["admin","user","reseller"]},"avatarUrl":{"type":["string","null"]},"defaultLanguage":{"type":["string","null"]},"resellerId":{"type":["number","null"]},"ownResellerId":{"type":["number","null"]}},"required":["id","email","role"],"description":"User Profile"},"UsageSummary":{"type":"object","description":"Consumption over a date window.","properties":{"period":{"type":"object","description":"The window that was measured.","properties":{"from":{"type":"string","format":"date-time","description":"Start of the window, inclusive."},"to":{"type":"string","format":"date-time","description":"End of the window, inclusive."}}},"voiceMinutesUsed":{"type":"number","description":"Voice minutes consumed in the window."},"chatSessionsUsed":{"type":"number","description":"Chat sessions consumed in the window."}},"required":["period","voiceMinutesUsed","chatSessionsUsed"]},"Limits":{"type":"object","description":"What the account has left to spend.","properties":{"accountStatus":{"type":["string","null"],"description":"State of the account, for example `active`, `trial` or `suspended`."},"balanceEur":{"type":"number","description":"Prepaid wallet balance in EUR."},"voiceMinutesRemaining":{"type":["integer","null"],"description":"Voice minutes left in the current period. Null when the plan sets no cap."},"chatConversationsRemaining":{"type":["integer","null"],"description":"Chat conversations left in the current period. Null when the plan sets no cap."},"voiceMinutesUsed":{"type":"integer","description":"Voice minutes used in the current period."},"chatConversationsUsed":{"type":"integer","description":"Chat conversations used in the current period."},"voiceMinutesAllowance":{"type":["integer","null"],"description":"Voice minutes the period started with, plan plus anything bought on top. Null when the plan sets no cap."},"chatConversationsAllowance":{"type":["integer","null"],"description":"Chat conversations the period started with, plan plus anything bought on top. Null when the plan sets no cap."},"plan":{"type":["object","null"],"description":"The subscribed plan and its monthly allowances. Null when the account is on no plan.","properties":{"name":{"type":"string","description":"Plan name."},"voiceMinutesPerMonth":{"type":["integer","null"],"description":"Monthly voice-minute allowance."},"chatConversationsPerMonth":{"type":["integer","null"],"description":"Monthly chat-conversation allowance."}}}},"required":["accountStatus","balanceEur"]},"Plan":{"type":"object","description":"A billing plan that can be subscribed to. Stripe product and price identifiers are internal and not returned.","properties":{"id":{"type":"integer","description":"Plan identifier."},"name":{"type":"string","description":"Plan name."},"description":{"type":["string","null"],"description":"What the plan includes."},"type":{"type":"string","enum":["voice","chat","addon"],"description":"What the plan sells."},"priceEur":{"type":"string","description":"Price per billing cycle in EUR. Returned as a decimal string, not a float."},"billingCycle":{"type":"string","enum":["monthly","yearly","one-time"],"description":"How often it is charged."},"contractDuration":{"type":["integer","null"],"description":"Minimum contract length in months."},"customDurationDays":{"type":["integer","null"],"description":"Contract length in days, where the plan is not measured in months."},"voiceMinutesPerMonth":{"type":["integer","null"],"description":"Monthly voice-minute allowance. Null means unmetered."},"voiceCallsPerMonth":{"type":["integer","null"],"description":"Monthly call allowance."},"chatConversationsPerMonth":{"type":["integer","null"],"description":"Monthly chat-conversation allowance."},"chatMessagesPerMonth":{"type":["integer","null"],"description":"Monthly chat-message allowance."},"includesSetup":{"type":"boolean","description":"Whether setup is included in the price."},"includesOnboarding":{"type":"boolean","description":"Whether onboarding is included."},"includesSupportPriority":{"type":"boolean","description":"Whether the plan carries priority support."},"maxAgents":{"type":["integer","null"],"description":"How many agents the plan allows."},"isActive":{"type":"boolean","description":"Whether the plan can still be subscribed to."},"isVisible":{"type":"boolean","description":"Whether the plan is offered in the price list. A hidden plan can still be subscribed to by a reseller on behalf of a customer."},"features":{"type":["array","null"],"description":"Marketing feature list, parsed from its stored JSON.","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time","description":"When the plan was created."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","name","priceEur","billingCycle"]},"Subscription":{"type":"object","description":"The active subscription of the account.","properties":{"id":{"type":"integer","description":"Subscription identifier."},"status":{"type":"string","description":"State of the subscription, for example `active`, `paused` or `canceled`."},"contractDuration":{"type":["integer","null"],"description":"Contract length in months."},"startDate":{"type":["string","null"],"format":"date-time","description":"When the subscription began."},"renewalDate":{"type":["string","null"],"format":"date-time","description":"When it next renews."},"endDate":{"type":["string","null"],"format":"date-time","description":"When it ends, once an end has been set."},"autoRenew":{"type":"boolean","description":"Whether it renews without action."},"canceledAt":{"type":["string","null"],"format":"date-time","description":"When cancellation was requested."},"plan":{"type":"object","description":"The plan being paid for.","properties":{"id":{"type":"integer","description":"Plan identifier."},"name":{"type":"string","description":"Plan name."},"description":{"type":["string","null"],"description":"What the plan includes."},"priceEur":{"type":"string","description":"Price per billing cycle in EUR. Returned as a decimal string, not a float."},"billingCycle":{"type":["string","null"],"description":"How often it is charged."},"voiceMinutesPerMonth":{"type":["integer","null"],"description":"Monthly voice-minute allowance."},"chatConversationsPerMonth":{"type":["integer","null"],"description":"Monthly chat-conversation allowance."}}}},"required":["id","status","plan"]},"Balance":{"type":"object","description":"Prepaid balance and remaining allowances.","properties":{"balanceEur":{"type":"number","description":"Wallet balance in EUR."},"voiceMinutesRemaining":{"type":["integer","null"],"description":"Voice minutes left."},"chatConversationsRemaining":{"type":["integer","null"],"description":"Chat conversations left."}},"required":["balanceEur"]},"Transaction":{"type":"object","description":"One movement on the prepaid wallet.","properties":{"id":{"type":"integer","description":"Transaction identifier."},"amount":{"type":"number","description":"Signed amount in EUR. Negative for a charge."},"balanceAfter":{"type":"number","description":"Wallet balance after the movement."},"type":{"type":["string","null"],"description":"What caused it, for example `topup`, `purchase` or `adjustment`."},"description":{"type":["string","null"],"description":"Free-text note."},"paymentMethod":{"type":["string","null"],"description":"How it was paid, where a payment was involved."},"createdAt":{"type":"string","format":"date-time","description":"When it was booked."}},"required":["id","amount","createdAt"]},"Invoice":{"type":"object","description":"An invoice on the account. The Stripe invoice identifier is internal and not returned.","properties":{"id":{"type":"integer","description":"Invoice identifier."},"invoiceNumber":{"type":["string","null"],"description":"Human-readable invoice number."},"description":{"type":["string","null"],"description":"What the invoice covers."},"subtotalEur":{"type":"string","description":"Net total in EUR. Returned as a decimal string, not a float."},"taxEur":{"type":"string","description":"Tax in EUR. Returned as a decimal string, not a float."},"totalEur":{"type":"string","description":"Gross total in EUR. Returned as a decimal string, not a float."},"status":{"type":"string","description":"Payment state, for example `draft`, `pending` or `paid`."},"issuedDate":{"type":["string","null"],"format":"date-time","description":"Invoice date."},"dueDate":{"type":["string","null"],"format":"date-time","description":"When payment is due."},"paidDate":{"type":["string","null"],"format":"date-time","description":"When it was paid."},"billingPeriodStart":{"type":["string","null"],"format":"date-time","description":"Start of the period billed."},"billingPeriodEnd":{"type":["string","null"],"format":"date-time","description":"End of the period billed."},"paymentMethod":{"type":["string","null"],"description":"How it was or will be paid."},"lineItems":{"type":["string","null"],"description":"The line items, as a JSON string."},"pdfUrl":{"type":["string","null"],"description":"Stored PDF, when one exists. Always null for reseller invoices, which are rendered on demand by GET /billing/invoices/{id}/pdf.","format":"uri"},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","totalEur","status"]},"AddonPackage":{"type":"object","description":"A bundle of extra credits that can be bought on top of a plan.","properties":{"id":{"type":"integer","description":"Package identifier."},"name":{"type":"string","description":"Package name."},"description":{"type":["string","null"],"description":"What the package includes."},"addonType":{"type":["string","null"],"description":"What the package adds, for example `voice_minutes` or `chat_conversations`."},"quantity":{"type":["integer","null"],"description":"How many units it grants."},"unitPriceEur":{"type":"number","description":"Price per unit in EUR."},"totalPriceEur":{"type":"number","description":"Price of the whole package in EUR."}},"required":["id","name"]},"CheckoutRequest":{"type":"object","properties":{"planId":{"type":"number","description":"ID of the plan to purchase"}},"required":["planId"],"description":"Checkout request body"},"AnalyticsSummary":{"type":"object","description":"Usage aggregated per usage type over a date window. Cost and margin are included so a reseller can reconcile its own pricing.","properties":{"period":{"type":"object","description":"The window that was measured.","properties":{"from":{"type":"string","format":"date-time","description":"Start of the window."},"to":{"type":"string","format":"date-time","description":"End of the window."}}},"metrics":{"type":"array","description":"One row per usage type seen in the window.","items":{"type":"object","description":"Totals for one usage type.","properties":{"usageType":{"type":"string","description":"What was consumed, for example `voice_minute` or `chat_session`."},"totalQuantity":{"type":"number","description":"Units consumed."},"totalRevenue":{"type":"number","description":"What was charged, in EUR."},"totalCost":{"type":"number","description":"Platform cost basis, in EUR."},"totalMargin":{"type":"number","description":"Revenue minus cost, in EUR."}}}}},"required":["period","metrics"]},"DailyStat":{"type":"object","description":"Metrics for one calendar day. A voice call, a chat session and an avatar session each count as one call.","properties":{"date":{"type":"string","description":"The day, as `YYYY-MM-DD`.","format":"date"},"agentId":{"type":["string","null"],"description":"Agent the row belongs to."},"callCount":{"type":["integer","null"],"description":"Calls placed or received."},"successfulCalls":{"type":["integer","null"],"description":"Calls that completed normally."},"failedCalls":{"type":["integer","null"],"description":"Calls that failed."},"totalDuration":{"type":["integer","null"],"description":"Total call seconds."},"averageDuration":{"type":"number","description":"Mean call length in seconds."},"totalCost":{"type":"number","description":"Cost of the day in USD, as the voice engine reports it."},"successRate":{"type":"number","description":"Successful calls as a fraction of all calls."},"peakHour":{"type":["integer","null"],"description":"Hour of day, 0 to 23, with the most calls."}},"required":["date"]},"AgentStats":{"type":"object","description":"Lifetime aggregate metrics for one voice agent. Null when the agent has no recorded activity yet.","properties":{"totalCalls":{"type":["integer","null"],"description":"Calls handled."},"successfulCalls":{"type":["integer","null"],"description":"Calls that completed normally."},"failedCalls":{"type":["integer","null"],"description":"Calls that failed."},"abandonedCalls":{"type":["integer","null"],"description":"Calls the caller dropped."},"totalDuration":{"type":["integer","null"],"description":"Total call seconds."},"averageDuration":{"type":"number","description":"Mean call length in seconds."},"longestCall":{"type":["integer","null"],"description":"Longest call in seconds."},"shortestCall":{"type":["integer","null"],"description":"Shortest call in seconds."},"totalCost":{"type":"number","description":"Total cost in USD, as the voice engine reports it."},"averageCostPerCall":{"type":"number","description":"Mean cost per call in USD."},"totalTtsTokens":{"type":["integer","null"],"description":"Speech synthesis units consumed."},"totalLlmTokens":{"type":["integer","null"],"description":"Language model tokens consumed."},"successRate":{"type":"number","description":"Successful calls as a fraction of all calls."},"averageSatisfactionScore":{"type":["number","null"],"description":"Mean satisfaction rating. Null while no rating was collected."},"averageEvaluationScore":{"type":["number","null"],"description":"Mean automatic evaluation score. Null while no evaluation was recorded."},"totalMessages":{"type":["integer","null"],"description":"Messages exchanged across all calls."},"lastCallAt":{"type":["string","null"],"format":"date-time","description":"When the agent last handled a call."}}},"ChatbotStats":{"type":"object","description":"Lifetime aggregate metrics for one chatbot. Null when the chatbot has no recorded activity yet. Counts chat sessions and spoken widget conversations alike. Same fields as the agent statistics minus `longestCall` and `shortestCall`.","properties":{"totalCalls":{"type":["integer","null"],"description":"Sessions handled."},"successfulCalls":{"type":["integer","null"],"description":"Sessions that completed normally."},"failedCalls":{"type":["integer","null"],"description":"Sessions that failed."},"abandonedCalls":{"type":["integer","null"],"description":"Sessions the visitor dropped."},"totalDuration":{"type":["integer","null"],"description":"Total session seconds."},"averageDuration":{"type":"number","description":"Mean session length in seconds."},"totalCost":{"type":"number","description":"Total cost in USD, as the voice engine reports it."},"averageCostPerCall":{"type":"number","description":"Mean cost per session in USD."},"totalTtsTokens":{"type":["integer","null"],"description":"Speech synthesis units consumed."},"totalLlmTokens":{"type":["integer","null"],"description":"Language model tokens consumed."},"successRate":{"type":"number","description":"Successful sessions as a fraction of all sessions."},"averageSatisfactionScore":{"type":["number","null"],"description":"Mean satisfaction rating. Null while no rating was collected."},"averageEvaluationScore":{"type":["number","null"],"description":"Mean automatic evaluation score. Null while no evaluation was recorded."},"totalMessages":{"type":["integer","null"],"description":"Messages exchanged."},"lastCallAt":{"type":["string","null"],"format":"date-time","description":"When the chatbot last handled a session."}}},"Campaign":{"type":"object","description":"An outbound calling campaign with its counters.","properties":{"id":{"type":"integer","description":"Campaign identifier."},"name":{"type":"string","description":"Campaign name."},"status":{"type":"string","enum":["draft","pending","scheduled","running","completed","cancelled","failed"],"description":"Where the campaign stands."},"batchId":{"type":["string","null"],"description":"Identifier of the dispatched batch. Null until the campaign is submitted."},"totalRecipients":{"type":["integer","null"],"description":"How many numbers the campaign will call."},"successfulCalls":{"type":["integer","null"],"description":"Calls that connected."},"failedCalls":{"type":["integer","null"],"description":"Calls that did not."},"pendingCalls":{"type":["integer","null"],"description":"Calls not yet attempted."},"scheduledTime":{"type":["string","null"],"format":"date-time","description":"When the campaign is due to start."},"createdAt":{"type":"string","format":"date-time","description":"When the campaign was created."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","name","status"]},"CampaignSummary":{"type":"object","description":"An outbound calling campaign as it appears in a list.","properties":{"id":{"type":"integer","description":"Campaign identifier."},"name":{"type":"string","description":"Campaign name."},"status":{"type":"string","enum":["draft","pending","scheduled","running","completed","cancelled","failed"],"description":"Where the campaign stands. Only a `draft` can be submitted."},"totalRecipients":{"type":["integer","null"],"description":"How many numbers the campaign will call."},"successfulCalls":{"type":["integer","null"],"description":"Calls that connected."},"failedCalls":{"type":["integer","null"],"description":"Calls that did not."},"pendingCalls":{"type":["integer","null"],"description":"Calls not yet attempted."},"scheduledTime":{"type":["string","null"],"format":"date-time","description":"When the campaign is due to start. Null means it starts on submit."},"createdAt":{"type":"string","format":"date-time","description":"When the campaign was created."}},"required":["id","name","status"]},"Recipient":{"type":"object","description":"One number in a campaign, with the outcome of its call.","properties":{"id":{"type":"integer","description":"Recipient identifier."},"phoneNumber":{"type":"string","description":"The number called, in E.164 form."},"callStatus":{"type":["string","null"],"description":"Outcome of the call, for example `pending`, `completed` or `failed`."},"durationSeconds":{"type":["integer","null"],"description":"Length of the call in seconds."},"callStartedAt":{"type":["string","null"],"format":"date-time","description":"When the call connected."},"callEndedAt":{"type":["string","null"],"format":"date-time","description":"When it finished."},"callCost":{"type":"number","description":"What the call cost, in EUR."},"conversationId":{"type":["string","null"],"description":"Identifier of the resulting conversation, when one was recorded."}},"required":["id","phoneNumber"]},"ResellerStats":{"type":"object","description":"Headline counters for the reseller account.","properties":{"totalCustomers":{"type":"integer","description":"Customers linked to this reseller."},"totalVoiceAgents":{"type":"integer","description":"Voice agents across all of them."},"totalChatbots":{"type":"integer","description":"Chatbots across all of them."},"voiceMinutesAvailable":{"type":"number","description":"Prepaid voice minutes the reseller still holds."},"chatMessagesAvailable":{"type":"number","description":"Prepaid chat messages the reseller still holds."},"thisMonthUsageCost":{"type":"number","description":"Cost of the last 30 days of customer usage, in EUR."}},"required":["totalCustomers","totalVoiceAgents","totalChatbots"]},"ResellerCustomer":{"type":"object","description":"A customer account belonging to the reseller.","properties":{"id":{"type":"integer","description":"Identifier of the link record."},"resellerId":{"type":"integer","description":"The reseller."},"userId":{"type":"integer","description":"The customer user."},"customerReference":{"type":["string","null"],"description":"Free-text reference the reseller keeps for this customer."},"notes":{"type":["string","null"],"description":"Free-text note the reseller keeps for this customer."},"createdAt":{"type":"string","format":"date-time","description":"When the customer was linked."},"user":{"type":"object","description":"The customer account itself.","properties":{"id":{"type":"integer","description":"User identifier."},"email":{"type":["string","null"],"description":"Login email.","format":"email"},"firstName":{"type":["string","null"],"description":"First name."},"lastName":{"type":["string","null"],"description":"Last name."},"company":{"type":["string","null"],"description":"Company name."},"balanceEur":{"type":"string","description":"Prepaid wallet balance in EUR. Returned as a decimal string, not a float."},"voiceMinutes":{"type":["integer","null"],"description":"Voice minutes remaining."},"chatConversations":{"type":["integer","null"],"description":"Chat conversations remaining."},"accountStatus":{"type":"string","enum":["active","suspended","trial"],"description":"Whether the account can sign in. A suspended account is locked out."},"createdAt":{"type":"string","format":"date-time","description":"When the account was created."}}}},"required":["id","userId"]},"OwnerRef":{"type":"object","description":"The customer a resource belongs to. Null when it belongs to nobody yet.","properties":{"id":{"type":"integer","description":"User identifier."},"name":{"type":["string","null"],"description":"Display name."},"email":{"type":["string","null"],"description":"Login email.","format":"email"}}},"ResellerSettings":{"type":"object","description":"Payment configuration status and invoicing profile. Secrets are reduced to booleans and never returned.","properties":{"stripeConfigured":{"type":"boolean","description":"Whether a Stripe secret key is stored."},"stripePublishableKey":{"type":["string","null"],"description":"The publishable key, which is safe to expose."},"paypalConfigured":{"type":"boolean","description":"Whether PayPal credentials are stored."},"companyName":{"type":["string","null"],"description":"Trading name."},"companyAddress":{"type":["string","null"],"description":"Postal address used on invoices."},"companyVatId":{"type":["string","null"],"description":"VAT identification number."},"companyPhone":{"type":["string","null"],"description":"Contact phone number."},"companyEmail":{"type":["string","null"],"description":"Contact email.","format":"email"}},"required":["stripeConfigured","paypalConfigured"]},"ResellerCompany":{"type":"object","description":"Company and branding profile of the reseller.","properties":{"companyName":{"type":["string","null"],"description":"Trading name."},"companyLegalName":{"type":["string","null"],"description":"Registered legal name."},"companyAddress":{"type":["string","null"],"description":"Postal address."},"companyVatId":{"type":["string","null"],"description":"VAT identification number."},"companyLogo":{"type":["string","null"],"description":"Logo used on invoices.","format":"uri"},"brandPrimaryColor":{"type":["string","null"],"description":"Accent colour of the branded portal, as a CSS hex value."},"brandLogo":{"type":["string","null"],"description":"Logo used in the branded portal.","format":"uri"},"companyPostalCode":{"type":["string","null"],"description":"Postal code of the sender address."},"companyCity":{"type":["string","null"],"description":"City of the sender address."},"companyCountry":{"type":["string","null"],"description":"Country of the sender address."},"companyTaxNumber":{"type":["string","null"],"description":"Tax number, for a sender that has no VAT identification number."},"companyBankName":{"type":["string","null"],"description":"Bank the payment is to reach."},"companyIban":{"type":["string","null"],"description":"IBAN printed on an unpaid invoice."},"companyBic":{"type":["string","null"],"description":"BIC printed alongside the IBAN."},"companyPaymentTermsDays":{"type":"integer","description":"Days an invoice is given to be paid. 14 when it was never set."},"companyInvoiceFooter":{"type":["string","null"],"description":"Free text printed at the foot of every invoice."},"missingCompanyFields":{"type":"array","description":"Which of the details an invoice has to carry are still missing. While this is not empty, raising an invoice and rendering one are both refused with 409 `company_details_incomplete`.","items":{"type":"string"}}}},"ResellerPricing":{"type":"object","description":"What the platform charges the reseller, what the reseller charges its customers, and the difference.","properties":{"baseChatSessionPrice":{"type":"number","description":"Platform price per chat session, in EUR."},"baseVoiceMinutePrice":{"type":"number","description":"Platform price per voice minute, in EUR."},"resellerChatSessionPrice":{"type":"number","description":"Reseller price per chat session, in EUR."},"resellerVoiceMinutePrice":{"type":"number","description":"Reseller price per voice minute, in EUR."},"chatSessionMargin":{"type":"number","description":"Margin per chat session, in EUR."},"voiceMinuteMargin":{"type":"number","description":"Margin per voice minute, in EUR."}},"required":["baseChatSessionPrice","baseVoiceMinutePrice","resellerChatSessionPrice","resellerVoiceMinutePrice"]},"ResellerCredits":{"type":"object","description":"Prepaid voice and chat credits held by the reseller.","properties":{"resellerId":{"type":"integer","description":"The reseller."},"voiceMinutesAvailable":{"type":"number","description":"Voice minutes left."},"chatMessagesAvailable":{"type":"number","description":"Chat messages left."},"voiceMinutesPurchased":{"type":"number","description":"Voice minutes ever bought."},"chatMessagesPurchased":{"type":"number","description":"Chat messages ever bought."},"voiceMinutesUsed":{"type":"number","description":"Voice minutes ever consumed."},"chatMessagesUsed":{"type":"number","description":"Chat messages ever consumed."},"lowBalanceAlertSent":{"type":"boolean","description":"Whether a low-balance warning has already gone out."},"lowBalanceThreshold":{"type":"number","description":"Level at which that warning is raised."},"lastPurchase":{"type":["string","null"],"format":"date-time","description":"Most recent credit purchase."},"lastUsage":{"type":["string","null"],"format":"date-time","description":"Most recent consumption."}},"required":["voiceMinutesAvailable","chatMessagesAvailable"]},"ResellerCreditTransaction":{"type":"object","description":"One movement on the reseller credit ledger.","properties":{"id":{"type":"integer","description":"Transaction identifier."},"resellerId":{"type":"integer","description":"The reseller."},"type":{"type":["string","null"],"description":"Direction of the movement, `purchase` or `deduction`."},"voiceMinutes":{"type":["integer","null"],"description":"Voice minutes moved."},"chatMessages":{"type":["integer","null"],"description":"Chat messages moved."},"voiceMinutesBalance":{"type":["integer","null"],"description":"Voice-minute balance after the movement."},"chatMessagesBalance":{"type":["integer","null"],"description":"Chat-message balance after the movement."},"amountEur":{"type":"string","description":"Value of the movement in EUR. Returned as a decimal string, not a float."},"paymentMethod":{"type":["string","null"],"description":"How a purchase was paid."},"customerId":{"type":["integer","null"],"description":"Customer the deduction was booked against."},"description":{"type":["string","null"],"description":"Free-text note."},"createdAt":{"type":"string","format":"date-time","description":"When it was booked."}},"required":["id","createdAt"]},"ResellerActivity":{"type":"object","description":"One entry in the reseller audit trail. The field names are those of the stored row: the affected resource is named by `targetType` and `targetId`.","properties":{"id":{"type":"integer","description":"Entry identifier."},"resellerId":{"type":"integer","description":"The reseller the entry belongs to."},"userId":{"type":"integer","description":"The reseller-side user account that performed the action."},"action":{"type":["string","null"],"description":"What was done, for example `customer_created`, `pricing_updated` or `payment_keys_updated`."},"targetType":{"type":["string","null"],"description":"Kind of resource that was affected, for example `user`."},"targetId":{"type":["integer","null"],"description":"Identifier of that resource."},"description":{"type":["string","null"],"description":"Human-readable summary written when the entry was recorded."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string."},"createdAt":{"type":"string","format":"date-time","description":"When it happened."}},"required":["id","action","createdAt"]},"ProvisioningResult":{"type":"object","description":"The acknowledgement every provisioning lifecycle call answers with.","properties":{"result":{"type":"string","const":"success","description":"Always `success`. Failures use the error envelope and a non-2xx status."},"userId":{"type":"integer","description":"The customer account that was acted on."},"status":{"type":"string","description":"The account state that was stored, one of `active`, `suspended` or `trial`."},"terminated":{"type":"boolean","description":"Present and true on the terminate call only. A terminated account is stored as `suspended`, because the account-status field has no `terminated` value; this flag is what tells the two apart."}},"required":["result","userId"]}},"responses":{"ValidationError":{"description":"The request body or query string failed validation. `error.code` is `validation_error` (a few endpoints use `invalid_id` or `invalid_date` for a malformed path or date parameter), and `error.details` carries the per-field report. A malformed `X-EchoCall-Customer` header answers `invalid_customer_header`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"No usable API key. The `Authorization` header was missing or not a `Bearer` header (`error.code` `unauthorized`), the key did not match the `eck_live_...` format (`invalid_api_key`), the key is unknown, revoked or expired, or the account has no active subscription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InsufficientScope":{"description":"The API key is valid but was not issued the scope this operation requires. `error.code` is `insufficient_permissions` and `error.requiredScope` names the missing scope. Reseller-only endpoints answer `forbidden` when the key does not belong to a reseller. With `X-EchoCall-Customer`, a non-reseller key is refused with `act_as_requires_reseller` and a suspended customer with `customer_suspended`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No resource with that identifier is visible to this API key. Resources belonging to another account are reported as missing rather than forbidden. `error.code` is `not_found`. With `X-EchoCall-Customer`, an id that is not a customer of the reseller answers `customer_not_found`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The resource exists but its current state does not allow this operation, for example submitting a campaign that is already running or assigning an integration twice. `error.code` is `conflict`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"The per-key, per-second request limit was exceeded. `error.code` is `rate_limit_exceeded`. Wait for the interval named by `Retry-After` and retry.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Always 1, because the limit is measured per second.","schema":{"type":"integer","example":1}},"X-RateLimit-Limit":{"description":"Requests per second allowed for this API key.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the current second. 0 on this response.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"The request was understood but could not be completed: an unexpected server fault (`error.code` `internal_error`) or the database being unreachable (`database_error`). Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"ApiKeyAuth":[]}],"paths":{"/agents":{"get":{"tags":["Agents"],"summary":"List voice agents","description":"Returns the voice agents visible to the key, one page at a time, with a real total from a separate count query. An ordinary account key sees only its own agents; a reseller key sees every agent belonging to that reseller and can narrow the result to one customer with `userId`. Each item is the list form of an agent: no prompt, no model settings and no privacy settings, so fetch the individual agent when you need those. Note that the `status` query parameter is read but never applied, so passing it does not filter anything at present.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"status","in":"query","required":false,"description":"Currently ignored. The handler parses this value but does not filter on it, so the response is the same whether or not it is sent.","schema":{"type":"string","enum":["active","inactive"]}},{"name":"userId","in":"query","required":false,"description":"Reseller keys only: restrict the result to a single customer account. The key must be a reseller key and the account must be one of that reseller customers, otherwise the request is rejected with 403. Ignored for ordinary account keys, which always see only their own records.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"A page of voice agents. `pagination.total` is the true number of matching agents.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentSummary"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]},"post":{"tags":["Agents"],"summary":"Create a voice agent","description":"Creates a voice agent and provisions it in the voice engine in the same request, so the agent is ready to take calls as soon as the call succeeds. Only `name` and `language` are required; every other field falls back to a platform default, and `language` is normalised to a two-letter code, so a value like \"german\" is stored as `de`. The response is a short acknowledgement rather than the full agent, so read the agent back if you need the stored configuration. The new agent is created with status `active` and is owned by the account behind the key, or by the reseller for a reseller key.","requestBody":{"required":true,"description":"The agent configuration. `systemToolsConfig` switches on the built-in call-control tools, and `transferNumbers` lists the numbers the agent may hand a call over to.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The unique identifier name for your voice agent. Shown in the EchoCall dashboard."},"language":{"type":"string","description":"ISO 639-1 language code (e.g., \"en\", \"de\"). This determines the base model and pronunciation rules."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All supported languages as ISO-639-1 codes. First element = primary language. E.g. [\"de\", \"en\", \"fr\"]."},"systemPrompt":{"type":"string","description":"Instructions that define the agent's personality, knowledge, and behavior. Use clear, concise commands."},"firstMessage":{"type":"string","description":"The very first sentence the agent speaks when a call/chat begins. Supports SSML-like cues."},"voiceId":{"type":"string","description":"The unique ID of the EchoCall voice to be used for speech synthesis."},"temperature":{"type":"number","description":"Controls randomness. 0 is deterministic/focused, 100 is highly creative/varied."},"llmModel":{"type":"string","default":"EchoCall-Voice","description":"The advanced LLM \"brain\" powering the agent. Options: EchoCall-Voice, EchoCall-Smart."},"maxTokens":{"type":"number","default":1000,"description":"Upper limit on the number of tokens the agent can generate per response."},"ttsModel":{"type":"string","enum":["echocall-flash","echocall-flash-lite","echocall-ultra","echocall-multilingual"],"description":"Text-to-speech model. Leave unset for automatic selection based on `language`. `echocall-flash`: fast, multilingual (recommended default). `echocall-flash-lite`: fastest, English only. `echocall-ultra`: most expressive/natural, supports emotional audio tags, widest language coverage. `echocall-multilingual`: balanced multilingual quality. Each model supports a different set of languages for `language`/`supportedLanguages` - pick a model that covers your target language(s), otherwise the request may fail."},"stability":{"type":"number","description":"Voice stability (0.0-1.0). 0.3 = expressive/varied, 0.7 = stable/consistent. Default: 0.5."},"speed":{"type":"number","description":"Speaking speed multiplier (0.7-1.2). 0.9 = slower/clearer, 1.0 = normal, 1.1 = faster."},"turnEagerness":{"type":"string","enum":["patient","normal","eager"],"description":"How quickly the agent responds: patient (waits longer), normal, eager (responds fast)."},"enableInterruptions":{"type":"boolean","default":true,"description":"If true, the agent will stop speaking immediately when the user starts talking."},"enableBackchannel":{"type":"boolean","default":true,"description":"Enables realistic listening sounds like \"mhm\", \"yeah\", \"I see\" during user speech."},"enableFillers":{"type":"boolean","default":true,"description":"Allows the agent to use natural filler words like \"well\", \"actually\" to sound more human."},"backgroundVoiceDetection":{"type":"boolean","description":"If true, the agent detects and ignores background noise/voices."},"asrKeywords":{"type":"array","items":{"type":"string"},"description":"Keywords for improved ASR recognition (product names, brand names, etc.)."},"saveCallAudio":{"type":"boolean","default":false,"description":"Whether to record and store the full audio of the conversation for later review."},"zeroPiiRetention":{"type":"boolean","default":false,"description":"Privacy mode. If enabled, PII data is stripped and logs are not permanently stored."},"retentionDays":{"type":"number","default":30,"description":"Number of days to keep conversation logs. Defaults to 30. Set to -1 for permanent storage."},"systemToolsConfig":{"type":"object","properties":{"endCall":{"type":"boolean","default":false},"voicemailDetection":{"type":"boolean","default":false},"languageDetection":{"type":"boolean","default":false},"playKeypadTouchTone":{"type":"boolean","default":false},"skipTurn":{"type":"boolean","default":false}},"description":"System tool toggles (endCall, voicemailDetection, languageDetection, playKeypadTouchTone, skipTurn)."},"transferNumbers":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","default":"Transfer to this number"}},"required":["number","name"]},"description":"Phone numbers the agent can transfer calls to."}},"required":["name","language"],"description":"Create Voice Agent Request Configuration"}}}},"x-required-scope":"write:agents","responses":{"201":{"description":"The agent was created in the voice engine and stored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Public identifier of the new agent, of the form `agent_<number>`.","pattern":"^agent_[0-9]+$"},"name":{"type":"string","description":"Name the agent was created with."},"language":{"type":"string","description":"Normalised two-letter language code that was stored."},"status":{"type":"string","enum":["active"],"description":"New agents are always created active."},"createdAt":{"type":"string","format":"date-time","description":"When the agent was created."}},"required":["id","name","language","status","createdAt"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"412":{"description":"The account has no voice engine credentials, so no assistant can be created. An administrator has to configure the account before this endpoint can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/agents/{agentId}":{"get":{"tags":["Agents"],"summary":"Get a voice agent","description":"Returns the full public configuration of one voice agent, including the system prompt, the opening line, model settings and the recording and retention settings. Internal voice engine identifiers are never included. A reseller key may read any agent belonging to that reseller; an ordinary key only its own. An identifier that is not a number after the `agent_` prefix is removed is rejected with 400.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The agent configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Agent"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]},"patch":{"tags":["Agents"],"summary":"Update a voice agent","description":"Changes one or more fields on a voice agent. Every field is optional and only the fields you send are written, so this is a partial update. The change is pushed to the voice engine first and then stored; if the engine rejects the push the request still succeeds and the new values are stored, so a caller that depends on live behaviour should verify the agent afterwards. The response carries the agent id and a confirmation message, not the updated agent.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. Sending an empty object is accepted and touches only the updated timestamp.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name of the voice agent"},"language":{"type":"string","description":"ISO 639-1 language code"},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All supported languages as ISO-639-1 codes."},"systemPrompt":{"type":"string","description":"Instructions for the AI behavior"},"firstMessage":{"type":"string","description":"Initial message spoken by the agent"},"voiceId":{"type":"string","description":"EchoCall Voice ID"},"temperature":{"type":"number","description":"Creativity level (0-100)"},"llmModel":{"type":"string","description":"LLM Model to use"},"maxTokens":{"type":"number","description":"Max tokens per response"},"ttsModel":{"type":"string","enum":["echocall-flash","echocall-flash-lite","echocall-ultra","echocall-multilingual"],"description":"Text-to-speech model. Leave unset for automatic selection based on `language`. `echocall-flash`: fast, multilingual (recommended default). `echocall-flash-lite`: fastest, English only. `echocall-ultra`: most expressive/natural, supports emotional audio tags, widest language coverage. `echocall-multilingual`: balanced multilingual quality. Each model supports a different set of languages for `language`/`supportedLanguages` - pick a model that covers your target language(s), otherwise the request may fail."},"stability":{"type":"number","description":"Voice stability (0.0-1.0)."},"speed":{"type":"number","description":"Speaking speed (0.7-1.2)."},"turnEagerness":{"type":"string","enum":["patient","normal","eager"],"description":"How quickly the agent responds."},"enableInterruptions":{"type":"boolean","description":"Allow user to interrupt the agent"},"enableBackchannel":{"type":"boolean","description":"Enable backchanneling"},"enableFillers":{"type":"boolean","description":"Enable filler words"},"backgroundVoiceDetection":{"type":"boolean","description":"Detect and ignore background noise."},"asrKeywords":{"type":"array","items":{"type":"string"},"description":"Keywords for improved ASR recognition."},"saveCallAudio":{"type":"boolean","description":"Record and save call audio"},"zeroPiiRetention":{"type":"boolean","description":"Do not store PII"},"retentionDays":{"type":"number","description":"Days to keep logs (-1 = forever)"},"status":{"type":"string","enum":["active","inactive"],"description":"Agent status"},"systemToolsConfig":{"type":"object","properties":{"endCall":{"type":"boolean"},"voicemailDetection":{"type":"boolean"},"languageDetection":{"type":"boolean"},"playKeypadTouchTone":{"type":"boolean"},"skipTurn":{"type":"boolean"}},"description":"System tool toggles."},"transferNumbers":{"type":"array","items":{"type":"object","properties":{"number":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","default":"Transfer to this number"}},"required":["number","name"]},"description":"Phone numbers the agent can transfer calls to."}},"description":"Update Agent Request"}}}},"x-required-scope":"write:agents","responses":{"200":{"description":"The agent was updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Public identifier of the agent, of the form `agent_<number>`.","pattern":"^agent_[0-9]+$"},"message":{"type":"string","description":"Confirmation text."}},"required":["id","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]},"delete":{"tags":["Agents"],"summary":"Delete a voice agent","description":"Deletes a voice agent permanently and removes it from the voice engine. This cannot be undone, and any phone number still routed to this agent stops being answered by it. The removal in the voice engine is attempted first, but a failure there does not stop the local deletion, so the agent always disappears from the account. This endpoint takes no request body.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The agent was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/integrations":{"get":{"tags":["Agents"],"summary":"List integrations assigned to an agent","description":"Returns every integration currently assigned to this voice agent, together with the tools the agent may call through it. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. The response is a plain list with no paging, and it includes the health state of each integration so you can see whether a tool would actually work today. Only integrations of the account that owns the agent are listed, which for a reseller key is the customer account: an assignment that points at an integration of another account is left out. The stored configuration of an integration is not part of this list.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The integrations assigned to this agent.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AssignedIntegration"},"description":"The assignments."}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]},"post":{"tags":["Agents"],"summary":"Assign an integration to an agent","description":"Gives a voice agent access to one integration, so its tools become callable during a call. The integration must belong to the account that owns the agent, which for a reseller key is the customer account, not the reseller. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. Assigning an integration that is already assigned returns 409. After the row is written the new tool set is pushed to the voice engine; if that push fails the assignment is rolled back and the request returns 502, so the response always reflects the live agent.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The integration to assign, and optionally the tools to enable. Omit `enabledTools` to accept the recommended default set. Only catalogue-backed integration types accept a tool list.","content":{"application/json":{"schema":{"type":"object","properties":{"integrationId":{"type":"integer","description":"Identifier of the integration to assign. It must already exist on the account that owns the assistant."},"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}}},"required":["integrationId"]}}}},"x-required-scope":"write:agents","responses":{"201":{"description":"The integration was assigned and the agent tool set was pushed live.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":["integer","null"],"description":"Identifier of the new assignment row."},"agentType":{"type":"string","description":"Which kind of assistant the integration was assigned to."},"agentId":{"type":"integer","description":"Numeric identifier of the assistant."},"integrationId":{"type":"integer","description":"Identifier of the integration that was assigned."}},"required":["agentType","agentId","integrationId"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"description":"The `enabledTools` list names a tool the permissions of the integration do not allow. Error code `tool_not_permitted`; the message lists the refused tools. Widen the permissions with PUT /integrations/{id}/permissions or leave the tool out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/integrations/{integrationId}":{"patch":{"tags":["Agents"],"summary":"Change the tool selection for an assigned integration","description":"Replaces the tool selection for one integration already assigned to a voice agent. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. The field is required: a body without an `enabledTools` key is rejected with 400, while an explicit null restores the recommended default set. The new selection is saved and then pushed to the voice engine; if the push fails the request returns 502 but the saved selection is kept, so a retry only has to repeat the push. An integration of another account answers 404 as if it were not assigned.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"name":"integrationId","in":"path","required":true,"description":"Numeric identifier of the integration whose assignment is being changed. This is the integration id, not the assignment id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The complete new tool selection. This replaces the previous selection rather than adding to it.","content":{"application/json":{"schema":{"type":"object","properties":{"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}}},"required":["enabledTools"]}}}},"x-required-scope":"write:agents","responses":{"200":{"description":"The tool selection was saved and pushed live.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the assignment row that was updated."},"agentType":{"type":"string","enum":["voice"],"description":"Which kind of assistant the assignment belongs to."},"agentId":{"type":"integer","description":"Numeric identifier of the assistant."},"integrationId":{"type":"integer","description":"Identifier of the integration."},"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}},"toolsMode":{"type":"string","enum":["recommended","custom","none"],"description":"How to read `enabledTools`: `recommended` for the default set, `custom` for exactly the listed tools, `none` when no tool may be called."}},"required":["id","agentType","agentId","integrationId","toolsMode"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The `enabledTools` list names a tool the permissions of the integration do not allow. Error code `tool_not_permitted`; the message lists the refused tools. Widen the permissions with PUT /integrations/{id}/permissions or leave the tool out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:agents"]}]},"delete":{"tags":["Agents"],"summary":"Remove an integration from an agent","description":"Unassigns an integration from a voice agent, so the agent can no longer call any of its tools. The integration itself is untouched and stays available to assign elsewhere. The reduced tool set is pushed to the voice engine after the row is removed; if that push fails the request returns 502 and the integration is already unassigned locally but may still be callable until a later change syncs. This endpoint takes no request body.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"name":"integrationId","in":"path","required":true,"description":"Numeric identifier of the integration whose assignment is being changed. This is the integration id, not the assignment id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The integration was unassigned.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/preview":{"post":{"tags":["Agents"],"summary":"Start a browser test call with an agent","description":"Opens a test call to this agent from a browser, so a customer can hear their agent before it answers a real caller. The response carries no address of the speech engine: `streamPath` is a websocket path on this API host, and this server relays the audio in a format of its own, which is what keeps a portal built on this API free of any third-party name. Join `streamPath` to the API host and swap the scheme for `wss:` to get the address to open. The path may be opened once and within `expiresInSeconds`; afterwards it is refused with 403 and a new call has to be claimed here. On the socket the browser receives `ready` with the audio formats to use, then `audio`, `agent`, `user` and `interrupt` frames, and finally `end`; it sends `audio` frames of microphone data and `end` when the caller hangs up. A test call reaches the real agent and is billed like any other call, so the voice minutes of the account owning the agent are checked first and the call is cut off after `maxSeconds`. An agent that has never been published answers 409: save it once, then try again. This endpoint takes no request body.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The call was claimed. Open the websocket within the stated seconds.","content":{"application/json":{"schema":{"type":"object","properties":{"streamPath":{"type":"string","description":"Websocket path on this API host, including the one-time ticket. Relative on purpose: join it to the host you call this API on."},"maxSeconds":{"type":"integer","description":"How long the call may run before the server hangs up. Count it down in the interface as well."},"expiresInSeconds":{"type":"integer","description":"How long the path stays open for the first connection."},"agent":{"type":"object","properties":{"id":{"type":"string","description":"Identifier of the agent, in the form `agent_<number>`."},"name":{"type":"string","description":"Name of the agent being called."},"firstMessage":{"type":"string","nullable":true,"description":"What the agent opens the call with, when one is configured."},"language":{"type":"string","nullable":true,"description":"The language the agent speaks, as a code from GET /languages/agent."}},"required":["id","name"]}},"required":["streamPath","maxSeconds","expiresInSeconds","agent"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The account owning this agent has no voice minutes left. The error body carries the code `quota_exceeded`. Buy an add-on or wait for the next period, then retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/knowledge":{"get":{"tags":["Knowledge"],"summary":"List agent knowledge sources","description":"Returns every knowledge source attached to one voice agent, newest first. These are the documents the agent may draw on when answering, added either as a crawled web page or as literal text. The response is a plain list with no paging. Entries are scoped to the account behind the key, so a reseller key that reads a customer agent sees only the entries its own account added.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The knowledge sources on this agent, ordered by creation date descending.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeEntry"},"description":"The knowledge entries."}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]},"post":{"tags":["Knowledge"],"summary":"Add a knowledge source to an agent","description":"Attaches one knowledge source to a voice agent. The body is one of two shapes chosen by `type`: `url` takes a `url` to crawl and index, `text` takes up to 200000 characters of `text` to store verbatim. `title` is optional and defaults to the URL, or to \"Text Knowledge\" for a text entry. The document is created in the voice engine first, but a failure there is logged and does not fail the request, so an entry can end up stored locally and not yet indexed; it is always reported with status `ready`.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"A URL source or a text source. Set `type` to `url` and send `url`, or set `type` to `text` and send `text`.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"url":{"type":"string","description":"Web URL to crawl and index as knowledge"},"title":{"type":"string","description":"Human-readable title for this knowledge source"}},"required":["type","url"]}}}},"x-required-scope":"write:agents","responses":{"201":{"description":"The knowledge source was stored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new knowledge entry."},"type":{"type":"string","enum":["url","text"],"description":"How the source was supplied."},"title":{"type":"string","description":"Label stored for the entry, defaulted when none was sent."},"status":{"type":"string","enum":["ready"],"description":"New entries are always reported as ready."},"createdAt":{"type":"string","format":"date-time","description":"When the entry was added."}},"required":["id","type","title","status","createdAt"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/knowledge/{knowledgeId}":{"delete":{"tags":["Knowledge"],"summary":"Delete an agent knowledge source","description":"Removes one knowledge source from a voice agent and deletes the indexed document from the voice engine. The entry must belong to this agent and to the account behind the key, otherwise the request is rejected with 404. A failure to delete the indexed copy is logged and does not stop the local deletion. This endpoint takes no request body.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"name":"knowledgeId","in":"path","required":true,"description":"Identifier of the knowledge entry to remove.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The knowledge source was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/agents/{agentId}/knowledge/{knowledgeId}/refresh":{"post":{"tags":["Knowledge"],"summary":"Refresh an agent knowledge source","description":"Re-reads a URL knowledge source from its source page immediately and replaces the stored content with what the page says now. Website sources are also re-read on a schedule, so this is for the case where a page changed and the agent should know about it at once. Only entries of type `url` can be refreshed; text and file entries are rejected with 400. The entry must belong to this agent and to the account behind the key, otherwise the request is rejected with 404. This endpoint takes no request body.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent. Accepted either as the public form `agent_12` or as the bare number `12`; the prefix is stripped before the lookup.","schema":{"type":"string"}},{"name":"knowledgeId","in":"path","required":true,"description":"Identifier of the knowledge entry to refresh.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The source page was read again.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the knowledge entry."},"lastUpdatedAt":{"type":"string","format":"date-time","description":"When the stored content was last updated."},"message":{"type":"string","description":"Confirmation text."}},"required":["id","lastUpdatedAt","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/chatbots":{"get":{"tags":["Chatbots"],"summary":"List chatbots","description":"Returns the chatbots visible to the key, newest first. An ordinary account key sees only its own chatbots; a reseller key sees every chatbot belonging to that reseller and can narrow the result to one customer with `userId`. Paging defaults to 100 items per page, which is also the maximum. `pagination.total` is a real count of every chatbot matching the request, produced by a separate count query, so a page count can be worked out from it.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"userId","in":"query","required":false,"description":"Reseller keys only: restrict the result to a single customer account. The key must be a reseller key and the account must be one of that reseller customers, otherwise the request is rejected with 403. Ignored for ordinary account keys, which always see only their own records.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:chatbots","responses":{"200":{"description":"A page of chatbots. `pagination.total` is the true number of matching chatbots.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChatbotSummary"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:chatbots"]}]},"post":{"tags":["Chatbots"],"summary":"Create a chatbot","description":"Creates a chatbot and provisions it in the voice engine in the same request, then derives its hosted chat page URL from the new identifier. Only `name` is required; the language defaults to `de` and the widget appearance fields all have platform defaults. The widget microphone stays off until `enableVoice` is true; a spoken conversation then runs on the voice allowance of the account, and `voiceId` chooses the voice from GET /voices or GET /voices/available. Unset text fields are stored with sensible fallbacks, so an omitted `greeting` becomes a generic opening line and an omitted display name repeats `name`. The response carries the new identifier and the public URL, not the full chatbot.","requestBody":{"required":true,"description":"The chatbot configuration, covering behaviour, widget appearance and retention. `allowedDomains` restricts which sites may embed the widget; leave it out to allow any.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Internal name of the chatbot for identification in the dashboard."},"chatbotDisplayName":{"type":"string","description":"The name shown to end-users in the chat widget header."},"language":{"type":"string","default":"de","description":"Primary language for the chatbot (ISO 639-1). Affects NLP and default greetings."},"systemPrompt":{"type":"string","description":"Base instructions defining the chatbot's personality and scope of knowledge."},"greeting":{"type":"string","description":"The first message a user sees when opening the chat widget."},"primaryColor":{"type":"string","default":"#3B82F6","description":"The main brand color (Hex) used for the chat bubble and header."},"logoUrl":{"type":"string","description":"URL to a custom logo image for the chat widget."},"widgetPosition":{"type":"string","enum":["bottom-right","bottom-left","top-right","top-left"],"default":"bottom-right","description":"Where the chat bubble appears on the user's screen."},"widgetSize":{"type":"string","enum":["small","medium","large"],"default":"medium","description":"The relative size of the chat window."},"widgetStyle":{"type":"string","enum":["standard","rounded","compact"],"default":"rounded","description":"The visual variant of the chat interface."},"teaserMessage":{"type":"string","description":"Marketing hook shown in the proactive teaser bubble before the visitor opens the chat. Independent from `greeting`. Empty = no teaser bubble."},"widgetLanguage":{"type":"string","description":"Overrides the widget UI chrome language (buttons, placeholders, aria-labels), independent of the conversation `language`. Empty/omitted = automatic, follows `language`."},"allowedDomains":{"type":"array","items":{"type":"string"},"description":"Hostnames allowed to embed this widget (Referer-checked, fail-closed). Empty/omitted = unrestricted."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All languages the chatbot can converse in, as ISO-639-1 codes. First element = primary language."},"enableVoice":{"type":"boolean","description":"Lets visitors talk to the chatbot through the widget microphone. A spoken conversation runs on the voice allowance of the account. Defaults to false."},"voiceId":{"type":"string","description":"The voice the chatbot speaks with, from GET /voices or GET /voices/available. Omit for the engine default voice. Only used while `enableVoice` is true."},"textOnlyMode":{"type":"boolean","description":"Deprecated: the inverse of `enableVoice`, kept for clients written against 1.12. `false` switches the voice on. Ignored when `enableVoice` is present."},"saveConversations":{"type":"boolean","default":true,"description":"Whether to store chat history in the EchoCall database."},"zeroPiiRetention":{"type":"boolean","default":false,"description":"Strict privacy mode. Stored data will be anonymized."},"retentionDays":{"type":"number","default":30,"description":"Auto-deletion period for chat logs in days. Defaults to 30. -1 for infinity."},"llmModel":{"type":"string","default":"EchoCall-Smart","description":"The LLM brain powering the chat. Options: EchoCall-Smart, EchoCall-Voice."},"clientEvents":{"type":"object","properties":{"audio":{"type":"boolean","default":true,"description":"Enable audio events for voice-capable widgets."},"interruption":{"type":"boolean","default":true,"description":"Enable interruption handling."},"user_transcript":{"type":"boolean","default":true,"description":"Stream user transcripts back to the client."},"agent_response":{"type":"boolean","default":true,"description":"Stream agent responses in real-time."}},"description":"Real-time event configuration for SDK/Frontend integration."}},"required":["name"],"description":"Create Chatbot Request Configuration"}}}},"x-required-scope":"write:chatbots","responses":{"201":{"description":"The chatbot was created in the voice engine and stored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Numeric identifier of the new chatbot."},"publicUrl":{"type":"string","format":"uri","description":"Hosted chat page for the new chatbot."},"message":{"type":"string","description":"Confirmation text."}},"required":["id","publicUrl","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"412":{"description":"The account has no voice engine credentials, so no assistant can be created. An administrator has to configure the account before this endpoint can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/chatbots/{chatbotId}":{"get":{"tags":["Chatbots"],"summary":"Get a chatbot","description":"Returns the full public configuration of one chatbot, including the system prompt, the widget appearance, the widget voice, the embedding restrictions and the retention settings. `allowedDomains` and `supportedLanguages` come back as arrays, or as null when nothing is configured. Internal voice engine identifiers are never included. A reseller key may read any chatbot belonging to that reseller; an ordinary key only its own.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:chatbots","responses":{"200":{"description":"The chatbot configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Chatbot"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:chatbots"]}]},"patch":{"tags":["Chatbots"],"summary":"Update a chatbot","description":"Changes one or more fields on a chatbot. Every field is optional, but the body must contain at least one recognised field: an empty object is rejected with 400. `enableVoice` switches the widget microphone, `voiceId` changes the voice and an empty `voiceId` returns to the engine default; the chosen voice is kept while the microphone is off. The older `textOnlyMode` is still read as the inverse of `enableVoice`. The change is pushed to the voice engine first and then stored; if the engine rejects the push the request still succeeds and the new values are stored, so verify the chatbot afterwards when live behaviour matters. The response is a confirmation message only, not the updated chatbot.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. At least one must be present.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Name of the chatbot"},"chatbotDisplayName":{"type":"string","description":"Display name shown in the chat widget"},"language":{"type":"string","description":"Primary language for the chatbot (ISO 639-1). Affects NLP and default greetings."},"systemPrompt":{"type":"string","description":"System instructions for the chatbot behavior"},"greeting":{"type":"string","description":"Initial greeting message"},"primaryColor":{"type":"string","description":"Primary accent color"},"logoUrl":{"type":"string","description":"URL to chatbot logo"},"widgetPosition":{"type":"string","enum":["bottom-right","bottom-left","top-right","top-left"],"description":"Position on screen"},"widgetSize":{"type":"string","enum":["small","medium","large"],"description":"Size of the chat widget"},"widgetStyle":{"type":"string","enum":["standard","rounded","compact"],"description":"Visual style variant"},"teaserMessage":{"type":"string","description":"Marketing hook shown in the proactive teaser bubble before the visitor opens the chat. Independent from `greeting`. Empty = no teaser bubble."},"widgetLanguage":{"type":"string","description":"Overrides the widget UI chrome language (buttons, placeholders, aria-labels), independent of the conversation `language`. Empty/omitted = automatic, follows `language`."},"allowedDomains":{"type":"array","items":{"type":"string"},"description":"Hostnames allowed to embed this widget (Referer-checked, fail-closed). Empty/omitted = unrestricted."},"supportedLanguages":{"type":"array","items":{"type":"string"},"description":"All languages the chatbot can converse in, as ISO-639-1 codes. First element = primary language."},"status":{"type":"string","enum":["active","inactive"],"description":"Chatbot status"},"enableVoice":{"type":"boolean","description":"Switches the widget microphone on or off. The chosen voice is kept while it is off."},"voiceId":{"type":"string","description":"The voice the chatbot speaks with, from GET /voices or GET /voices/available. An empty string returns to the engine default voice."},"textOnlyMode":{"type":"boolean","description":"Deprecated: the inverse of `enableVoice`, kept for clients written against 1.12. Ignored when `enableVoice` is present."},"saveConversations":{"type":"boolean","description":"Save chat history"},"zeroPiiRetention":{"type":"boolean","description":"Do not store PII"},"retentionDays":{"type":"number","description":"Days to keep logs (-1 = forever)"},"llmModel":{"type":"string","description":"LLM Model for the brain"}},"description":"Update Chatbot Request"}}}},"x-required-scope":"write:chatbots","responses":{"200":{"description":"The chatbot was updated.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}]},"delete":{"tags":["Chatbots"],"summary":"Delete a chatbot","description":"Deletes a chatbot permanently and removes it from the voice engine. This cannot be undone, and any widget still embedded on a website stops working. The removal in the voice engine is attempted first, but a failure there does not stop the local deletion, so the chatbot always disappears from the account. This endpoint takes no request body.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:chatbots","responses":{"200":{"description":"The chatbot was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/chatbots/{chatbotId}/integrations":{"get":{"tags":["Chatbots"],"summary":"List integrations assigned to a chatbot","description":"Returns every integration currently assigned to this chatbot, together with the tools it may call through them. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. The response is a plain list with no paging, and it includes the health state of each integration so you can see whether a tool would actually work today. Only integrations of the account that owns the chatbot are listed, which for a reseller key is the customer account: an assignment that points at an integration of another account is left out. The stored configuration of an integration is not part of this list.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:chatbots","responses":{"200":{"description":"The integrations assigned to this chatbot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AssignedIntegration"},"description":"The assignments."}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:chatbots"]}]},"post":{"tags":["Chatbots"],"summary":"Assign an integration to a chatbot","description":"Gives a chatbot access to one integration, so its tools become callable during a chat. The integration must belong to the account that owns the chatbot, which for a reseller key is the customer account, not the reseller. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. Assigning an integration that is already assigned returns 409. After the row is written the new tool set is pushed to the voice engine; if that push fails the assignment is rolled back and the request returns 502, so the response always reflects the live chatbot.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The integration to assign, and optionally the tools to enable. Omit `enabledTools` to accept the recommended default set. Only catalogue-backed integration types accept a tool list.","content":{"application/json":{"schema":{"type":"object","properties":{"integrationId":{"type":"integer","description":"Identifier of the integration to assign. It must already exist on the account that owns the assistant."},"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}}},"required":["integrationId"]}}}},"x-required-scope":"write:chatbots","responses":{"201":{"description":"The integration was assigned and the chatbot tool set was pushed live.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":["integer","null"],"description":"Identifier of the new assignment row."},"agentType":{"type":"string","description":"Which kind of assistant the integration was assigned to."},"agentId":{"type":"integer","description":"Numeric identifier of the assistant."},"integrationId":{"type":"integer","description":"Identifier of the integration that was assigned."}},"required":["agentType","agentId","integrationId"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"description":"The `enabledTools` list names a tool the permissions of the integration do not allow. Error code `tool_not_permitted`; the message lists the refused tools. Widen the permissions with PUT /integrations/{id}/permissions or leave the tool out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/chatbots/{chatbotId}/integrations/{integrationId}":{"patch":{"tags":["Chatbots"],"summary":"Change the tool selection for a chatbot integration","description":"Replaces the tool selection for one integration already assigned to a chatbot. The `enabledTools` field decides which of the integration actions the assistant may call: null means the recommended default set for that integration type, an empty array means the assistant may call nothing, and a list of tool names means exactly those tools and no others. Tool names are the `name` values from the integration tool catalogue. A list may only name tools the permissions of the integration allow (`permitted` in the catalogue, see GET /integrations/{id}/permissions); a tool outside them is refused with 422 and error code `tool_not_permitted`, while a malformed list or an unknown tool name stays a 400 `validation_error`. The field is required: a body without an `enabledTools` key is rejected with 400, while an explicit null restores the recommended default set. The new selection is saved and then pushed to the voice engine; if the push fails the request returns 502 but the saved selection is kept, so a retry only has to repeat the push. An integration of another account answers 404 as if it were not assigned.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"name":"integrationId","in":"path","required":true,"description":"Numeric identifier of the integration whose assignment is being changed. This is the integration id, not the assignment id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The complete new tool selection. This replaces the previous selection rather than adding to it.","content":{"application/json":{"schema":{"type":"object","properties":{"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}}},"required":["enabledTools"]}}}},"x-required-scope":"write:chatbots","responses":{"200":{"description":"The tool selection was saved and pushed live.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the assignment row that was updated."},"agentType":{"type":"string","enum":["chat"],"description":"Which kind of assistant the assignment belongs to."},"agentId":{"type":"integer","description":"Numeric identifier of the assistant."},"integrationId":{"type":"integer","description":"Identifier of the integration."},"enabledTools":{"type":["array","null"],"description":"Exact tools the assistant may call through this integration. Null selects the recommended default set for the integration type, an empty array disables every tool, and a list selects exactly the named tools.","items":{"type":"string"}},"toolsMode":{"type":"string","enum":["recommended","custom","none"],"description":"How to read `enabledTools`: `recommended` for the default set, `custom` for exactly the listed tools, `none` when no tool may be called."}},"required":["id","agentType","agentId","integrationId","toolsMode"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The `enabledTools` list names a tool the permissions of the integration do not allow. Error code `tool_not_permitted`; the message lists the refused tools. Widen the permissions with PUT /integrations/{id}/permissions or leave the tool out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:chatbots"]}]},"delete":{"tags":["Chatbots"],"summary":"Remove an integration from a chatbot","description":"Unassigns an integration from a chatbot, so the chatbot can no longer call any of its tools. The integration itself is untouched and stays available to assign elsewhere. The reduced tool set is pushed to the voice engine after the row is removed; if that push fails the request returns 502 and the integration is already unassigned locally but may still be callable until a later change syncs. This endpoint takes no request body.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"name":"integrationId","in":"path","required":true,"description":"Numeric identifier of the integration whose assignment is being changed. This is the integration id, not the assignment id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:chatbots","responses":{"200":{"description":"The integration was unassigned.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The change was applied to our records but could not be pushed to the voice engine, so the assistant tool set is not yet live. Retry the request; for an assignment the row is rolled back first, so a retry starts from a clean state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/chatbots/{chatbotId}/knowledge":{"get":{"tags":["Knowledge"],"summary":"List chatbot knowledge sources","description":"Returns every knowledge source attached to one chatbot. Only the first 100 characters of each entry are returned, as `contentSnippet`; the full stored text is never sent by this endpoint. The response is a plain list with no paging. Entries are scoped to the account behind the key, so a reseller key that reads a customer chatbot sees only the entries its own account added.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:chatbots","responses":{"200":{"description":"The knowledge sources on this chatbot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ChatbotKnowledgeEntry"},"description":"The knowledge entries."}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:chatbots"]}]},"post":{"tags":["Knowledge"],"summary":"Add a knowledge source to a chatbot","description":"Attaches one knowledge source to a chatbot. `type` says how to read `content`: for `url` it is the address to draw from, for `text` it is the literal text, and for `file` it is the file body you supply yourself, described by `fileName` and `mimeType`. `content` is always required and must not be empty. Note that the 400 response for an invalid body carries only an error code and a `details` list, with no `message` key, unlike the other endpoints.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The knowledge source. `title`, `fileName` and `mimeType` are optional and stored as given.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["url","text","file"]},"content":{"type":"string"},"title":{"type":"string"},"fileName":{"type":"string"},"mimeType":{"type":"string"}},"required":["type","content"]}}}},"x-required-scope":"write:chatbots","responses":{"201":{"description":"The knowledge source was stored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new knowledge entry."},"message":{"type":"string","description":"Confirmation text."}},"required":["id","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/chatbots/{chatbotId}/knowledge/{knowledgeId}":{"delete":{"tags":["Knowledge"],"summary":"Delete a chatbot knowledge source","description":"Removes one knowledge source from a chatbot. The deletion is matched on the knowledge id and the account behind the key, and the chatbot in the path is not checked, so an id belonging to a different chatbot on the same account is also removed. The endpoint answers 200 whether or not an entry was actually deleted, so a repeated call and a wrong id both look like a success. This endpoint takes no request body.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"name":"knowledgeId","in":"path","required":true,"description":"Identifier of the knowledge entry to remove.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:chatbots","responses":{"200":{"description":"The request was accepted. This is also the answer when no matching entry existed.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Confirmation text."}},"required":["message"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/chatbots/{chatbotId}/knowledge/{knowledgeId}/refresh":{"post":{"tags":["Knowledge"],"summary":"Refresh a chatbot knowledge source","description":"Re-reads a URL knowledge source from its source page immediately and replaces the stored content with what the page says now. Website sources are also re-read on a schedule, so this is for the case where a page changed and the chatbot should know about it at once. Only entries of type `url` can be refreshed; text and file entries are rejected with 400. The entry must belong to this chatbot and to the account behind the key, otherwise the request is rejected with 404. This endpoint takes no request body.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"name":"knowledgeId","in":"path","required":true,"description":"Identifier of the knowledge entry to refresh.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:chatbots","responses":{"200":{"description":"The source page was read again.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the knowledge entry."},"lastUpdatedAt":{"type":"string","format":"date-time","description":"When the stored content was last updated."},"message":{"type":"string","description":"Confirmation text."}},"required":["id","lastUpdatedAt","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:chatbots"]}]}},"/phone-numbers":{"get":{"tags":["PhoneNumbers"],"summary":"List phone numbers","description":"Returns the phone numbers the account rents, newest purchase first. Wholesale costs, carrier identifiers and SIP credentials are never included, only the columns of the PhoneNumber schema. A reseller key sees every number belonging to its own customers and can narrow the result to one customer with `userId`; a direct account key ignores `userId` and always sees only its own numbers. `pagination.total` is a real count of every number matching the request, not the length of the page, so a page count can be computed from it.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"userId","in":"query","required":false,"description":"Reseller keys only: restrict the result to one customer account. The customer must belong to the calling reseller, otherwise the request is rejected with 403. Ignored for direct account keys.","schema":{"type":"integer","minimum":1}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The requested page of phone numbers.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PhoneNumber"}},"pagination":{"type":"object","properties":{"total":{"type":"integer","description":"Total number of phone numbers matching the request, across all pages."},"page":{"type":"integer","description":"Page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied."}},"required":["total","page","perPage"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}]}},"/phone-numbers/countries":{"get":{"tags":["PhoneNumbers"],"summary":"List purchasable countries","description":"Returns every country whose numbers can currently be bought through the number marketplace. Use the `iso` field of an entry as the `countryIso` argument of the number search and of a purchase. The list is read live from the marketplace and is not paginated. When the marketplace is not enabled for this deployment the endpoint answers 503 instead of an empty list.","x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The countries currently on offer.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Country"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"The number marketplace is not enabled for this deployment, so no marketplace data can be served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/phone-numbers/search":{"get":{"tags":["PhoneNumbers"],"summary":"Search available numbers","description":"Searches the number marketplace for numbers that can be bought right now in one country. Nothing is reserved: a listing can disappear between the search and the purchase, so buy promptly and be ready to retry with a different result. Each entry carries the prices, the capabilities and the identifiers a purchase needs, and should be passed back to the purchase endpoint unchanged. The marketplace applies its own rate limit; when it is hit the endpoint answers 429 and the search can be repeated after a short pause.","parameters":[{"name":"countryIso","in":"query","required":true,"description":"Two-letter ISO country code to search in, exactly 2 characters. Take it from the `iso` field of the countries endpoint.","schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"numberType","in":"query","required":false,"description":"Restrict the search to one category of number. Omit to let the marketplace decide which categories to return.","schema":{"type":"string","enum":["local","national","mobile","tollfree","shared_cost","uifn"]}},{"name":"areaCode","in":"query","required":false,"description":"Area or city code to search within. Only meaningful for number types that have one, such as `local`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of listings to return. Must be between 1 and 100; a value outside that range is rejected with 400 rather than clamped.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The listings matching the search.","content":{"application/json":{"schema":{"type":"object","required":["data","count"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MarketplaceNumber"}},"count":{"type":"integer","description":"Number of listings in `data`."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"The number marketplace is not enabled for this deployment, so no marketplace data can be served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}]}},"/phone-numbers/purchase":{"post":{"tags":["PhoneNumbers"],"summary":"Purchase a phone number","description":"Buys one of the numbers returned by the search and attaches it to the account. This charges money: the setup price plus the first month, plus 19 percent VAT, is deducted from the account wallet, a balance transaction is written and an invoice is issued. Pass the listing from the search result back unchanged. Prices are never taken from the request: the server prices the number again from the marketplace listing, or from the platform price list for the countries that carry one, so the `setupPrice`, `monthlyPrice`, `providerSetupCost` and `providerMonthlyCost` fields are accepted for compatibility and ignored. Read the `pricing` block of the response for what was actually charged. A listing that can no longer be priced, because it has been withdrawn or its SKU has changed, is answered with 502 `quote_failed`: search again and retry with a current listing. If the wallet does not cover the total the request is rejected with 402 and nothing is bought. If the billing profile of the account is incomplete (company name, street, postal code, city, country, and the VAT ID for EU countries) the request is rejected with 412 `billing_profile_incomplete` before anything is charged, because the invoice could not be issued; complete the company details under Settings and retry. When the listing carries a `requirementId` the number is created with verification pending and cannot carry traffic until the verification endpoints below have been completed.","requestBody":{"required":true,"description":"The listing to buy, exactly as returned by the search. `availableDidId` and `skuId` identify the listing at the marketplace, `features` lists the capabilities of the number, and `requirementId` names the regulatory requirement set to satisfy, if there is one. The four price fields are deprecated: they are optional, they are ignored, and the server prices the purchase itself. They remain in the schema only so integrations that echo a whole search result back keep working.","content":{"application/json":{"schema":{"type":"object","properties":{"availableDidId":{"type":"string"},"skuId":{"type":"string"},"number":{"type":"string"},"countryIso":{"type":"string"},"areaName":{"type":"string"},"setupPrice":{"type":"number"},"monthlyPrice":{"type":"number"},"providerSetupCost":{"type":"number"},"providerMonthlyCost":{"type":"number"},"numberType":{"type":"string","enum":["local","national","mobile","tollfree","shared_cost","uifn"]},"features":{"type":"array","items":{"type":"string"}},"requirementId":{"type":["string","null"]}},"required":["availableDidId","skuId","number","countryIso","numberType","features"]}}}},"x-required-scope":"write:phone-numbers","responses":{"201":{"description":"The number was bought and the wallet was charged.","content":{"application/json":{"schema":{"type":"object","required":["success","phoneNumberId","number","kycRequired","pricing","newBalance"],"properties":{"success":{"type":"boolean","description":"Always true on this response."},"phoneNumberId":{"type":"integer","description":"Identifier of the new phone number record, for use on the other endpoints of this group."},"number":{"type":"string","description":"The number that was bought, in E.164 form."},"kycRequired":{"type":"boolean","description":"Whether regulatory verification must be submitted before the number can carry traffic."},"pricing":{"type":"object","properties":{"setupPrice":{"type":"number","description":"One-off charge applied, in EUR net."},"monthlyPrice":{"type":"number","description":"Recurring monthly charge, in EUR net."},"vat":{"type":"number","description":"VAT added, in EUR, at 19 percent."},"total":{"type":"number","description":"Gross amount deducted from the wallet, in EUR."}},"required":["setupPrice","monthlyPrice","vat","total"]},"newBalance":{"type":"number","description":"Wallet balance in EUR after the deduction."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The wallet does not cover the gross total. The error body carries `required` and `available` in EUR alongside the code and message. Nothing was bought and nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/InsufficientScope"},"412":{"description":"The billing profile of the account is incomplete, so no invoice could be issued. The error body carries `missingFields`, the profile fields still empty (`companyName`, `billingStreet`, `billingPostalCode`, `billingCity`, `billingCountry`, and `vatId` for EU countries). Complete the company details under Settings and retry. Nothing was bought and nothing was charged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The number could not be bought at the carrier. The wallet was not charged; retry, or pick a different listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The number marketplace is not enabled for this deployment, so no marketplace data can be served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:phone-numbers"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/phone-numbers/{phoneNumberId}":{"get":{"tags":["PhoneNumbers"],"summary":"Get a phone number","description":"Returns one phone number of the account. The response is the PhoneNumber object itself, not wrapped in a `data` envelope. Use it to read the current assignment, the lifecycle status and the regulatory verification state of a number. A number that does not belong to the calling account answers 404 rather than 403.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The phone number.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumber"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}]},"patch":{"tags":["PhoneNumbers"],"summary":"Update a phone number","description":"Changes which agent answers on a number and the label it carries in the dashboard. Only the fields present in the body are written, everything else is left alone; a body with no known field is rejected with 400. The response is an acknowledgement message only, it does not echo the updated record: read the number back with the GET endpoint if you need the new state. Assigning an agent here does not itself provision the number at the carrier, it only changes the routing recorded for it.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. `voiceAgentId` points the number at a voice agent, or clears the routing when null, and `label` sets the display name returned as `friendlyName`.","content":{"application/json":{"schema":{"type":"object","properties":{"voiceAgentId":{"type":["number","null"]},"label":{"type":"string"}}}}}},"x-required-scope":"write:phone-numbers","responses":{"200":{"description":"The number was updated.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string","description":"Confirmation text. No record is returned."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:phone-numbers"]}]},"delete":{"tags":["PhoneNumbers"],"summary":"Cancel a phone number","description":"Cancels a number bought through the marketplace, with a 30 day grace period: the number is released at the carrier straight away but stays usable on the account until `cancelationEndDate`, and the record is kept rather than deleted. Only marketplace numbers can be cancelled here; a number that was imported by other means is rejected with 400. Cancelling twice is rejected with 409. This does not refund the setup charge or the month already paid for.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:phone-numbers","responses":{"200":{"description":"The number was cancelled and the grace period has started.","content":{"application/json":{"schema":{"type":"object","required":["success","canceledAt","cancelationEndDate","message"],"properties":{"success":{"type":"boolean","description":"Always true on this response."},"canceledAt":{"type":"string","format":"date-time","description":"When the cancellation was recorded."},"cancelationEndDate":{"type":"string","format":"date-time","description":"Last moment the number stays usable, 30 days after cancellation."},"message":{"type":"string","description":"Human-readable summary of the grace period."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:phone-numbers"]}]}},"/phone-numbers/{phoneNumberId}/kyc":{"get":{"tags":["PhoneNumbers"],"summary":"Get the verification requirement","description":"Returns what the regulator demands before this number can carry traffic. When the number needs no verification the response is `{ \"kycRequired\": false, \"requirement\": null }`. Otherwise `requirement` describes the accepted identity kinds, how many documents of which type are needed, which fields are mandatory and which forms must be signed. Read this first: it tells you exactly what to put in the submission body.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The verification requirement, if there is one.","content":{"application/json":{"schema":{"type":"object","required":["kycRequired","requirement"],"properties":{"kycRequired":{"type":"boolean","description":"Whether this number needs regulatory verification at all."},"requirement":{"allOf":[{"$ref":"#/components/schemas/KycRequirement"}],"nullable":true,"description":"The requirement set. Null when no verification is needed."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}]},"post":{"tags":["PhoneNumbers"],"summary":"Submit verification","description":"Submits the identity, address and document proofs demanded for a regulated number. Upload the document files first and pass their identifiers in `proofs`; which document types are accepted and how many are needed comes from the requirement endpoint. The submission is forwarded to the registry and the number moves to the `pending` verification state, from where the status endpoint reports progress. A number that needs no verification is rejected with 400, and a number already approved or already awaiting review is rejected with 409, so a rejected submission must be corrected and sent again rather than duplicated.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The verification package. `identityType` picks between a personal and a business identity and decides which of the optional identity fields are mandatory, per the requirement set. `phoneNumberField` is the contact number of the identity, not the number being verified. `countryId` is the registry country identifier and is used for both the identity and the address. `addressLine`, `cityName` and `postalCode` are the registered address. Each entry in `proofs` names an uploaded file, the document type it satisfies and whether it proves the identity or the address.","content":{"application/json":{"schema":{"type":"object","properties":{"identityType":{"type":"string","enum":["personal","business"]},"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumberField":{"type":"string"},"idNumber":{"type":"string"},"birthDate":{"type":"string"},"personalTaxId":{"type":"string"},"companyName":{"type":"string"},"companyRegNumber":{"type":"string"},"vatId":{"type":"string"},"contactEmail":{"type":"string"},"identityDescription":{"type":"string"},"cityName":{"type":"string"},"postalCode":{"type":"string"},"addressLine":{"type":"string"},"addressDescription":{"type":"string"},"countryId":{"type":"string"},"proofs":{"type":"array","items":{"type":"object","properties":{"encryptedFileId":{"type":"string"},"proofTypeId":{"type":"string"},"entityType":{"type":"string","enum":["identities","addresses"]}},"required":["encryptedFileId","proofTypeId","entityType"]}},"serviceDescription":{"type":"string"}},"required":["identityType","cityName","postalCode","addressLine","countryId","proofs"]}}}},"x-required-scope":"write:phone-numbers","responses":{"201":{"description":"The verification was submitted and is now awaiting review.","content":{"application/json":{"schema":{"type":"object","required":["success","addressVerificationId","status"],"properties":{"success":{"type":"boolean","description":"Always true on this response."},"addressVerificationId":{"type":"string","description":"Registry identifier of the submission, echoed by the status endpoint."},"status":{"type":"string","enum":["pending"],"description":"State right after submission. Always `pending`."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:phone-numbers"]}]}},"/phone-numbers/{phoneNumberId}/kyc/status":{"get":{"tags":["PhoneNumbers"],"summary":"Get verification status","description":"Polls the current state of the regulatory verification for a number. Once a submission exists the state is read live from the registry and the stored value on the number record is updated to match, so this endpoint is what to poll after submitting. Before any submission it answers `not_submitted` for a number that needs verification, and the number's recorded state, normally `not_required`, for one that does not. When the registry rejected the submission, `rejectReasons` says why; it is null in every other case.","parameters":[{"name":"phoneNumberId","in":"path","required":true,"description":"Identifier of the phone number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:phone-numbers","responses":{"200":{"description":"The current verification state.","content":{"application/json":{"schema":{"type":"object","required":["status","rejectReasons","addressVerificationId"],"properties":{"status":{"type":"string","description":"Verification state, for example `not_required`, `not_submitted`, `pending`, `approved` or `rejected`."},"rejectReasons":{"type":"string","nullable":true,"description":"Why the registry refused the submission. Null unless it was rejected."},"addressVerificationId":{"type":"string","nullable":true,"description":"Registry identifier of the submission. Null before anything was submitted."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:phone-numbers"]}]}},"/conversations":{"get":{"tags":["Conversations"],"summary":"List conversations","description":"Returns a page of conversations, newest first. One `type` is listed at a time: `voice` returns call records and `chat` returns widget and WhatsApp threads. Every item carries a `type` field so a client can tell the two shapes apart without remembering which query it sent. The pagination object reports `page`, `perPage`, the `type` that was listed and `total`, a real count of the conversations of that type on the account.","parameters":[{"name":"type","in":"query","required":false,"description":"Which kind of conversation to list. Any value other than `chat` is treated as `voice`.","schema":{"type":"string","enum":["voice","chat"],"default":"voice"}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:conversations","responses":{"200":{"description":"The requested page of conversations.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/Conversation"},{"$ref":"#/components/schemas/LiveConversation"}],"discriminator":{"propertyName":"type","mapping":{"voice":"#/components/schemas/Conversation","chat":"#/components/schemas/LiveConversation"}}}},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"Page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied."},"total":{"type":"integer","description":"Total number of conversations of the listed `type` on the account, across all pages."},"type":{"type":"string","description":"The `type` value that was listed, echoed back as received."}},"required":["page","perPage","total","type"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:conversations"]}]}},"/conversations/{id}":{"get":{"tags":["Conversations"],"summary":"Get a conversation","description":"Returns one conversation. The `type` query parameter selects which store the id is looked up in and must match the kind of record you are asking for, since voice calls and chat threads number themselves independently. A voice call is returned on its own. A chat conversation is returned together with its full transcript in `messages`, ordered oldest first and not paginated; for a long thread use the messages endpoint instead. An id that is not a number is rejected with 400, and one that is not found under the selected type answers 404.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the conversation, in the id space of the selected `type`.","schema":{"type":"integer"}},{"name":"type","in":"query","required":false,"description":"Which kind of conversation the id refers to. Any value other than `chat` is treated as `voice`.","schema":{"type":"string","enum":["voice","chat"],"default":"voice"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:conversations","responses":{"200":{"description":"The conversation, with its transcript when it is a chat thread.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Conversation"},{"allOf":[{"$ref":"#/components/schemas/LiveConversation"},{"type":"object","properties":{"messages":{"type":"array","description":"The whole transcript, oldest message first.","items":{"$ref":"#/components/schemas/ConversationTranscriptMessage"}}},"required":["messages"]}]}],"discriminator":{"propertyName":"type","mapping":{"voice":"#/components/schemas/Conversation","chat":"#/components/schemas/LiveConversation"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:conversations"]}]},"patch":{"tags":["Conversations"],"summary":"Update a chat conversation","description":"Changes the status, the archive flag or the assigned human agent of a chat conversation. Chat threads only: an id belonging to a voice call is rejected with 400, and a body with no updatable field is rejected with 400 as well. Assigning an agent also stamps the assignment time and, unless the same request sets a status, moves the thread to `active`, mirroring what the dashboard does. Setting the status to `closed` runs the same finalisation as the close endpoint, recomputing the conversation analytics. The updated conversation is returned in `data`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the chat conversation.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. `status` moves the thread between `waiting`, `active` and `closed`, `isArchived` moves it in and out of the inbox, and `assignedAgentId` names the human agent to hand it to, or null to unassign. At least one field must be present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConversation"}}}},"x-required-scope":"write:conversations","responses":{"200":{"description":"The conversation after the update.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/LiveConversation"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:conversations"]}]}},"/conversations/{id}/messages":{"get":{"tags":["Conversations"],"summary":"List conversation messages","description":"Returns a page of the transcript of a chat conversation, oldest message first, so paging forward follows the conversation in order. This endpoint serves chat threads only: an id that belongs to a voice call is rejected with 400 rather than a misleading 404, and the transcript of a call is carried on the call record itself. Unlike the conversation list, the pagination object here carries a real `total`, counted over the whole transcript.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the chat conversation.","schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:conversations","responses":{"200":{"description":"The requested page of messages.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LiveMessage"}},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"Page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied."},"total":{"type":"integer","description":"Total number of messages in the conversation."}},"required":["page","perPage","total"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:conversations"]}]},"post":{"tags":["Conversations"],"summary":"Reply in a conversation","description":"Sends a message into a chat conversation as a human agent, delivering it to the visitor over the same channel the thread runs on. An API key is not itself an agent session, so the message is attributed to the agent already assigned to the thread, or to the account's first live chat agent when it is unassigned. An account with no live chat agent at all cannot send and is rejected with 409. Chat threads only: a voice call id is rejected with 400. A thread whose state does not allow a reply, a closed one for example, is rejected with 412.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the chat conversation.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The reply text to deliver to the visitor. It may not be empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessage"}}}},"x-required-scope":"write:conversations","responses":{"201":{"description":"The message was stored and delivered.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new message."},"conversationId":{"type":"integer","description":"Conversation it was added to."},"agentId":{"type":"integer","description":"Live chat agent the message was attributed to."}},"required":["id","conversationId","agentId"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"412":{"description":"The conversation is not in a state that accepts an agent reply. Nothing was sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The message could not be delivered to the visitor over the conversation channel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:conversations"]}]}},"/conversations/{id}/close":{"post":{"tags":["Conversations"],"summary":"Close a chat conversation","description":"Closes a chat conversation and finalises it: the closing time is stamped and the response time and duration figures of the thread are recomputed, which is what the analytics endpoints then report. Chat threads only, a voice call id is rejected with 400. This endpoint takes no request body. Closing an already closed conversation runs the finalisation again rather than failing. The closed conversation is returned in `data`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the chat conversation.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:conversations","responses":{"200":{"description":"The conversation after it was closed.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/LiveConversation"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:conversations"]}]}},"/analytics/summary":{"get":{"tags":["Analytics"],"summary":"Get a usage summary","description":"Returns usage over a date window, aggregated into one row per usage type, with the quantity consumed and the revenue, cost and margin that go with it. Rows appear only for usage types that were actually recorded in the window, so an idle account gets an empty `metrics` array rather than rows of zeroes. Both bounds are optional: without them the window is the last 30 days ending now. The window that was actually applied is echoed in `period`.","parameters":[{"name":"from","in":"query","required":false,"description":"Start of the window, inclusive. Defaults to 30 days before now. A date-time value is accepted as well as a date.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"End of the window, inclusive. Defaults to now. A date-time value is accepted as well as a date.","schema":{"type":"string","format":"date"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:analytics","responses":{"200":{"description":"Usage aggregated over the window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:analytics"]}]}},"/analytics/daily":{"get":{"tags":["Analytics"],"summary":"Get daily call metrics","description":"Returns one row of metrics per calendar day, oldest day first, ready to plot as a time series. The figures are read live from the conversation records, so a conversation is counted as soon as it has ended, and the voice calls, chat sessions and avatar sessions of the account each count as one call. Cost is in USD, as the voice engine reports it. Days with no activity are absent from the result rather than present as zeroes. The window is the last `days` days counted back from now, optionally narrowed to a single agent. The applied `days` value and `agentId` are echoed in the response, which matters because an out-of-range or non-numeric `days` is silently replaced by 30 rather than rejected.","parameters":[{"name":"days","in":"query","required":false,"description":"Size of the window in days. Must be between 1 and 365; a value outside that range, or one that is not a number, falls back to 30 without an error. Check the `days` field of the response for what was used.","schema":{"type":"integer","minimum":1,"maximum":365,"default":30}},{"name":"agentId","in":"query","required":false,"description":"Restrict the series to one voice agent, by its `agent_42` identifier or the bare number. Omit for the whole account. No error is raised for an unknown agent, the series is simply empty.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:analytics","responses":{"200":{"description":"The daily series for the window.","content":{"application/json":{"schema":{"type":"object","required":["days","agentId","data"],"properties":{"days":{"type":"integer","description":"Window size that was actually applied, after the fallback to 30."},"agentId":{"type":"string","nullable":true,"description":"Agent the series was narrowed to, as `agent_42`, or null for the whole account."},"data":{"type":"array","description":"One entry per day that had activity, oldest first.","items":{"$ref":"#/components/schemas/DailyStat"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:analytics"]}]}},"/analytics/agents/{agentId}":{"get":{"tags":["Analytics"],"summary":"Get agent statistics","description":"Returns the lifetime aggregate metrics of one voice agent: call counts, durations, cost, token consumption and quality scores. The figures are read live from the conversation records, so a call is counted as soon as it has ended. Cost is in USD, as the voice engine reports it. An agent that exists but has no recorded activity yet answers 200 with `stats` set to null. An agent that does not belong to the calling account answers 404, the same as one that does not exist; a path segment that is not a number answers 400.","parameters":[{"name":"agentId","in":"path","required":true,"description":"Identifier of the voice agent, as `agent_42` or the bare number.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:analytics","responses":{"200":{"description":"The aggregate metrics of the agent.","content":{"application/json":{"schema":{"type":"object","required":["agentId","stats"],"properties":{"agentId":{"type":"integer","description":"The agent the figures belong to."},"stats":{"allOf":[{"$ref":"#/components/schemas/AgentStats"}],"nullable":true,"description":"The aggregate. Null when the agent has no recorded activity."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:analytics"]}]}},"/analytics/chatbots/{chatbotId}":{"get":{"tags":["Analytics"],"summary":"Get chatbot statistics","description":"Returns the lifetime aggregate metrics of one chatbot: session counts, durations, cost, token consumption and quality scores. The figures are read live from the conversation records and count a chat session and a spoken widget conversation alike, so the fields are named after calls even though most entries are chat sessions; the two call-length extremes tracked for voice agents are absent. Cost is in USD, as the voice engine reports it. A chatbot with no recorded activity yet answers 200 with `stats` set to null. A chatbot that does not belong to the calling account answers 404, the same as one that does not exist; a path segment that is not a number answers 400.","parameters":[{"name":"chatbotId","in":"path","required":true,"description":"Numeric identifier of the chatbot.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:analytics","responses":{"200":{"description":"The aggregate metrics of the chatbot.","content":{"application/json":{"schema":{"type":"object","required":["chatbotId","stats"],"properties":{"chatbotId":{"type":"integer","description":"The chatbot the figures belong to."},"stats":{"allOf":[{"$ref":"#/components/schemas/ChatbotStats"}],"nullable":true,"description":"The aggregate. Null when the chatbot has no recorded activity."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:analytics"]}]}},"/webhooks":{"get":{"tags":["Webhooks"],"summary":"List your webhook endpoints","description":"Returns every outbound webhook endpoint registered by the account, newest first and unpaged. The signing secret is not part of this response and cannot be read back at any point: it is shown once, when the endpoint is created. Use `failedDeliveries`, `lastDeliveryAt` and `lastDeliveryStatus` to spot an endpoint that has started rejecting deliveries before it is switched off automatically.","x-required-scope":"read:webhooks","responses":{"200":{"description":"Registered endpoints, newest first. `events` comes back as a JSON array string and has to be parsed.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:webhooks"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]},"post":{"tags":["Webhooks"],"summary":"Register a webhook endpoint","description":"Registers an HTTPS endpoint and subscribes it to the named events. The response contains a `secret`, and this is the only time it is ever returned: store it before you do anything else, because no endpoint anywhere in this API will show it to you again, and losing it means deleting the endpoint and registering a new one. The secret keys the HMAC-SHA256 signature sent with every delivery in the `X-EchoCall-Signature` header, which is how you prove a payload really came from us. Every name in `events` must be a name from GET /webhooks/events, or the literal `*` for all of them; an unknown name is rejected with 400 rather than silently never firing. The target URL is resolved and refused with 400 if it points at a private or internal address.","requestBody":{"required":true,"description":"The destination URL, which must be publicly resolvable, and at least one event name to subscribe to.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}}},"required":["url","events"]}}}},"x-required-scope":"write:webhooks","responses":{"201":{"description":"The endpoint was registered. Save `secret` now: it is never returned again.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new endpoint."},"secret":{"type":"string","description":"The signing secret, 64 hexadecimal characters. Returned exactly once, here. It signs the delivery payload; verify each delivery against it and reject anything that does not match."},"message":{"type":"string","description":"Human-readable confirmation."}},"required":["id","secret","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:webhooks"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"List the events you can subscribe to","description":"Returns the catalogue of outbound events with a one-line explanation of when each is emitted. These names, plus the wildcard returned in `wildcard`, are the only values accepted in the `events` list when creating or updating an endpoint. `webhook.test` appears in the catalogue but is never produced by platform activity: it is only sent when you ask for a test delivery.","x-required-scope":"read:webhooks","responses":{"200":{"description":"The event catalogue.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Every subscribable event."},"wildcard":{"type":"string","description":"The value that subscribes an endpoint to all events at once, currently `*`."}},"required":["data","wildcard"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:webhooks"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Read one webhook endpoint","description":"Returns a single registered endpoint, wrapped in a `data` object. The signing secret is not included and cannot be recovered here or anywhere else. Note `isActive` together with `failedDeliveries`: an endpoint is disabled automatically once 25 consecutive deliveries have failed, and a single successful delivery resets that counter to zero.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the endpoint. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:webhooks","responses":{"200":{"description":"The endpoint. `events` is a JSON array string and has to be parsed.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Webhook"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:webhooks"]}]},"patch":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","description":"Changes the destination URL, the subscription list or the enabled flag of an endpoint. Every field is optional and only the fields you send are touched. `events` is replaced wholesale, not merged, so send the complete list you want; each name is checked against the catalogue and an unknown one fails the whole request with 400. A new URL is resolved and refused if it points at a private or internal address. Setting `isActive` back to true is how you bring back an endpoint that was disabled automatically after repeated failures. The signing secret is not changed and cannot be rotated through this endpoint.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the endpoint.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. `events`, if sent, replaces the existing subscription list entirely.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"isActive":{"type":"boolean"}}}}}},"x-required-scope":"write:webhooks","responses":{"200":{"description":"The endpoint was updated. The response is a bare confirmation and does not return the updated endpoint.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation. There is no `success` field on this response."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:webhooks"]}]},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","description":"Removes an endpoint permanently. Deliveries stop at once and the signing secret is destroyed with the record, so this cannot be undone: registering the same URL again produces a new endpoint with a new secret. If you only want to pause deliveries, set `isActive` to false with a PATCH instead.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the endpoint.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"write:webhooks","responses":{"200":{"description":"The endpoint was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation. There is no `success` field on this response."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:webhooks"]}]}},"/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test delivery","description":"Sends one signed `webhook.test` payload to the endpoint and reports what happened, synchronously and with no retry. This is a real delivery: it is signed with the endpoint's secret, so it is the right way to check your signature verification. It updates `lastDeliveryAt` and `lastDeliveryStatus` and nothing else: a failed test never counts towards the automatic disabling that follows 25 consecutive real failures, and a successful one neither resets `failedDeliveries` nor re-enables an endpoint you disabled yourself. A transport failure is reported inside a 200 response with `delivered` false, not as an HTTP error.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the endpoint to ping.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body. The test payload is generated by the platform.","x-required-scope":"write:webhooks","responses":{"200":{"description":"The attempt finished. Read `data.delivered` to find out whether it succeeded.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"delivered":{"type":"boolean","description":"Whether the endpoint accepted the delivery."},"status":{"type":"integer","description":"HTTP status the endpoint responded with. Absent when no response was received at all. A 3xx is reported as a failure: redirects are deliberately not followed."},"error":{"type":"string","description":"Why the delivery failed. Absent when `delivered` is true."}},"required":["delivered"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:webhooks"]}]}},"/tickets":{"get":{"tags":["Tickets"],"summary":"List support tickets","description":"Returns the account's support tickets, newest first, with no paging. By default only open work is listed: tickets in state `closed` or `resolved` are left out unless you ask for them. Pass `includeArchived=true` to get exactly the closed and resolved ones instead, or `status` to filter to a single state. `includeArchived` takes precedence over `status` when both are sent. Neither parameter is validated against a fixed list, so a misspelled `status` is not an error, it simply matches no tickets and returns an empty array.","parameters":[{"name":"status","in":"query","required":false,"description":"Return only tickets in this state. The states in use are `open`, `in_progress`, `waiting`, `resolved` and `closed`. Not validated: an unrecognised value matches nothing. Ignored when `includeArchived=true` is also sent.","schema":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"]}},{"name":"includeArchived","in":"query","required":false,"description":"Set to `true` to list the archive, meaning exactly the `closed` and `resolved` tickets, instead of the open ones. Only the literal string `true` has any effect; every other value, including `false`, leaves the default behaviour in place.","schema":{"type":"string","enum":["true","false"]}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:tickets","responses":{"200":{"description":"Matching tickets, newest first. Message threads are not included; read a ticket to get them.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TicketSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:tickets"]}]},"post":{"tags":["Tickets"],"summary":"Open a support ticket","description":"Creates a support ticket on behalf of the authenticated account and returns its identifier. The ticket starts in state `open`, and `category` and `priority` fall back to `other` and `medium` when they are not given. Put everything a human needs into `description`: error messages, the identifiers involved and the steps that led to the problem, since the first reply is written by a person reading exactly this text. Creating a ticket also emits the `ticket.created` outbound webhook event to any endpoint subscribed to it.","requestBody":{"required":true,"description":"The subject line and the full description, plus the optional `category` and `priority`. `category` groups the ticket by functional area and `priority` sets how urgently it is picked up.","content":{"application/json":{"schema":{"type":"object","properties":{"subject":{"type":"string","description":"A concise summary of the issue or request."},"description":{"type":"string","description":"Full details of the support request. Use this to provide context, error messages, or steps to reproduce."},"category":{"type":"string","enum":["technical","billing","feature","other"],"default":"other","description":"The functional area this ticket relates to."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium","description":"The urgency level. Urgent tickets are prioritized for immediate reseller/admin attention."}},"required":["subject","description"],"description":"Create Support Ticket Configuration"}}}},"x-required-scope":"write:tickets","responses":{"201":{"description":"The ticket was created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","pattern":"^ticket_[0-9]+$","description":"Identifier of the new ticket, of the form `ticket_<number>`."},"message":{"type":"string","description":"Human-readable confirmation."}},"required":["id","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:tickets"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/tickets/{ticketId}":{"get":{"tags":["Tickets"],"summary":"Read a ticket and its thread","description":"Returns one ticket with its full public message thread, oldest message first. Internal notes written by staff for staff are filtered out and never appear here, so the thread you see is the correspondence you took part in. Each message carries the sender's display name and role. `System` is used where a message was generated by the platform rather than typed by a person; a person who has no name on record reports `null`, so render your own label for them rather than a placeholder. A ticket belonging to another account answers 404 rather than 403.","parameters":[{"name":"ticketId","in":"path","required":true,"description":"Ticket identifier. Accepted with or without the `ticket_` prefix: both `ticket_42` and `42` address the same ticket.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:tickets","responses":{"200":{"description":"The ticket and its public thread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:tickets"]}]}},"/tickets/{ticketId}/messages":{"post":{"tags":["Tickets"],"summary":"Reply to a ticket","description":"Adds a public reply to an existing ticket and bumps its last-changed timestamp, which is what moves the ticket back up the support queue. A ticket in state `closed` cannot be replied to and returns 400; a resolved ticket still accepts replies, so use this to reopen a conversation that was closed too early. Attachments are referenced by URL, they are not uploaded here: host the file somewhere the support team can reach and pass the link. The reply emits the `ticket.updated` outbound webhook event.","parameters":[{"name":"ticketId","in":"path","required":true,"description":"Ticket identifier, with or without the `ticket_` prefix.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The reply text and, optionally, a list of attachments. Each attachment describes an already-hosted file: its name, a URL the support team can download it from, its content type and its size in bytes.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"The content of your reply or update."},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string","description":"The name of the uploaded file."},"url":{"type":"string","description":"The public URL where the attachment is hosted."},"mimeType":{"type":"string","description":"The logical file type (e.g., image/png, application/pdf)."},"size":{"type":"number","description":"File size in bytes."}},"required":["filename","url","mimeType","size"]},"description":"Optional list of file attachments supporting the message."}},"required":["message"],"description":"Add Message to Ticket Configuration"}}}},"x-required-scope":"write:tickets","responses":{"201":{"description":"The reply was added. The response confirms the write and does not echo the message back.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation. There is no `success` field on this response."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:tickets"]}]}},"/integrations/bookings":{"get":{"tags":["Integrations"],"summary":"List the bookings in your connected calendars","description":"Returns the bookings of every active integration that has a calendar (Cal.com, Calendly, Google Calendar, Outlook), sorted by start, or of one integration when you pass `integrationId`. Each booking carries the `id` that the confirm, decline and cancel paths take. Bookings are read live from the calendar provider on every request, so poll at a calm pace (once a minute is plenty) rather than in a tight loop. Each calendar is read across every page its provider hands out, up to a fixed limit, and over a fixed window: Cal.com and Calendly from 3 months back, Google Calendar and Outlook from now, each up to 12 months ahead. `from` and `to` only narrow that window, so a range outside it comes back empty without the calendar being listed in `failed`. A calendar that could not be read in full this time is listed in `failed`. When it could not be read at all, none of its bookings are in `data`. When it was read only in part (a later page failed, or it holds more bookings than the limit), the bookings that were read are in `data` and the rest are missing. Treat a booking missing from a calendar in `failed` as unknown, not as removed or cancelled. An `integrationId` that is not yours, is switched off or has no calendar is answered with an error rather than an empty list.","parameters":[{"name":"integrationId","in":"query","required":false,"description":"Only read this integration.","schema":{"type":"integer","minimum":1}},{"name":"status","in":"query","required":false,"description":"Only bookings in these states, as a comma separated list of `accepted`, `pending`, `cancelled` and `rejected`. Case does not matter.","schema":{"type":"string","example":"pending,accepted"}},{"name":"from","in":"query","required":false,"description":"Only bookings that start at or after this moment. ISO 8601 with time and offset. It narrows the window each calendar is read over and does not widen it.","schema":{"type":"string","format":"date-time","example":"2026-10-12T00:00:00Z"}},{"name":"to","in":"query","required":false,"description":"Only bookings that start at or before this moment. ISO 8601 with time and offset. Must not be earlier than `from`.","schema":{"type":"string","format":"date-time"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The bookings, the types of the calendars that were read and the calendars that could not be read in full.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarBooking"}},"providers":{"type":"array","items":{"type":"string"},"description":"Integration types of which at least one calendar was read, in full or in part. A type can be listed here while one of its calendars is also in `failed`."},"failed":{"type":"array","description":"Calendars that could not be read in full this time. Their bookings are missing from `data`, all of them or the ones that were not read, and are not gone. A calendar read in part keeps the bookings that were read in `data`.","items":{"type":"object","properties":{"integrationId":{"type":"integer","description":"Identifier of the integration."},"provider":{"type":"string","description":"Integration type."}},"required":["integrationId","provider"]}}},"required":["data","providers","failed"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The integration is switched off. Error code `integration_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The integration has no calendar (error code `not_a_calendar`), or its calendar cannot perform this action (error code `not_supported`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}]}},"/integrations/{id}/event-types":{"get":{"tags":["Integrations"],"summary":"List the event types of a calendar integration","description":"Returns the kinds of appointment that can be booked through this integration, read live from the calendar provider: their name, their duration in minutes and their identifier. An empty list means the calendar has no event types; a provider that cannot be reached or refuses is answered with 502 `provider_error`, not with an empty list.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the calendar integration.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The event types.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEventType"}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The integration is switched off. Error code `integration_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The integration has no calendar (error code `not_a_calendar`), or its calendar cannot perform this action (error code `not_supported`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The calendar provider refused the request or could not be reached. Error code `provider_error`. The message carries the reason the provider gave, for example that the booking does not exist or no longer waits for a decision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["read:integrations"]}]}},"/integrations/{id}/bookings/{bookingId}/confirm":{"post":{"tags":["Integrations"],"summary":"Confirm a booking request","description":"Confirms a booking that waits for the host (status `pending`). The calendar provider then informs the attendee as it always does. Only calendars with booking requests support this (Cal.com). The others answer 422 `not_supported`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the calendar integration.","schema":{"type":"integer"}},{"name":"bookingId","in":"path","required":true,"description":"The `id` of the booking as GET /integrations/bookings returns it.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:integrations","responses":{"200":{"description":"The calendar provider confirmed the booking.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The integration is switched off. Error code `integration_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The integration has no calendar (error code `not_a_calendar`), or its calendar cannot perform this action (error code `not_supported`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The calendar provider refused the request or could not be reached. Error code `provider_error`. The message carries the reason the provider gave, for example that the booking does not exist or no longer waits for a decision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/integrations/{id}/bookings/{bookingId}/decline":{"post":{"tags":["Integrations"],"summary":"Decline a booking request","description":"Declines a booking that waits for the host (status `pending`). The optional `reason` is handed to the calendar provider, which may show it to the attendee. Only calendars with booking requests support this (Cal.com). The others answer 422 `not_supported`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the calendar integration.","schema":{"type":"integer"}},{"name":"bookingId","in":"path","required":true,"description":"The `id` of the booking as GET /integrations/bookings returns it.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":false,"description":"Optional `reason`, at most 500 characters. An empty object or no body declines without a reason.","content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}},"description":"Optional reason handed to the calendar provider"}}}},"x-required-scope":"write:integrations","responses":{"200":{"description":"The calendar provider declined the booking.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The integration is switched off. Error code `integration_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The integration has no calendar (error code `not_a_calendar`), or its calendar cannot perform this action (error code `not_supported`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The calendar provider refused the request or could not be reached. Error code `provider_error`. The message carries the reason the provider gave, for example that the booking does not exist or no longer waits for a decision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/integrations/{id}/bookings/{bookingId}/cancel":{"post":{"tags":["Integrations"],"summary":"Cancel a booking","description":"Cancels a booking in the calendar of the integration. The optional `reason` is handed to the calendar provider, which may show it to the attendee.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the calendar integration.","schema":{"type":"integer"}},{"name":"bookingId","in":"path","required":true,"description":"The `id` of the booking as GET /integrations/bookings returns it.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":false,"description":"Optional `reason`, at most 500 characters. An empty object or no body cancels without a reason.","content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}},"description":"Optional reason handed to the calendar provider"}}}},"x-required-scope":"write:integrations","responses":{"200":{"description":"The calendar provider cancelled the booking.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The integration is switched off. Error code `integration_inactive`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The integration has no calendar (error code `not_a_calendar`), or its calendar cannot perform this action (error code `not_supported`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The calendar provider refused the request or could not be reached. Error code `provider_error`. The message carries the reason the provider gave, for example that the booking does not exist or no longer waits for a decision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/integrations/types":{"get":{"tags":["Integrations"],"summary":"List the integration types you can connect","description":"Returns the catalogue of supported third-party services, each with the configuration fields it needs. Read this before creating an integration: `type` gives you the value to send, and `configFields` tells you exactly which keys belong inside the `config` JSON string and which of them are required. Fields declared with type `password` are masked when the integration is read back, and so is every other key that looks like a secret (see GET /integrations/{id}). The response is a bare JSON array and is the same for every account.","x-required-scope":"read:integrations","responses":{"200":{"description":"Every supported integration type.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationType"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/integrations":{"get":{"tags":["Integrations"],"summary":"List your integrations","description":"Returns the account's connected integrations, newest first, as a bare JSON array with no paging. Stored configuration is deliberately left out of the list view: read a single integration to see which of its fields are set. A reseller key may pass `userId` to list one of its own customers' integrations instead; asking for a user who is not a customer of that reseller is refused with 403. On a key that is not a reseller key, `userId` is accepted but has no effect and the caller's own integrations are returned.","parameters":[{"name":"userId","in":"query","required":false,"description":"Reseller keys only: list the integrations of this customer instead of your own. The user must be one of your customers, otherwise the request is refused with 403. Ignored on non-reseller keys.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The integrations in scope, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationSummary"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}]},"post":{"tags":["Integrations"],"summary":"Connect an integration","description":"Stores the credentials and settings for one third-party service and returns the new integration's identifier. The integration is created active, but connecting it does not by itself let an agent use it: it still has to be assigned to an agent or chatbot. `config` is a JSON string, not a nested object, so serialise it before sending; the keys it must contain are the ones listed under `configFields` for this type in GET /integrations/types. A `config` that contains the mask `********` anywhere is refused with 400 and error code `masked_without_stored_value`: the mask stands for a stored secret, and a new integration has none, so send the real values. Keys inside `config` that only the server writes (the connection keys and `toolPermissions`) are dropped before the integration is stored. Nothing is contacted at creation time, so a wrong credential is only noticed later: call the test endpoint afterwards to confirm the connection works.","requestBody":{"required":true,"description":"The service to connect, a name to recognise it by, an optional description, and `config`: a JSON string holding the fields that type requires.","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["calcom","calendly","google_calendar","google_meet","google","microsoft","google_drive","google_sheets","gmail","hubspot","salesforce","webhook","zapier","make","slack","discord","zendesk","servicenow","twilio","whatsapp","shopify","amazon","ebay","msteams","outlook","smtp","notion","zoom","airtable","mailchimp","n8n","jira","confluence","shopware","freshdesk","custom"]},"name":{"type":"string"},"description":{"type":"string"},"config":{"type":"string"}},"required":["type","name","config"],"description":"Schema for creating a new integration"}}}},"x-required-scope":"write:integrations","responses":{"201":{"description":"The integration was created.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"},"integrationId":{"type":"integer","description":"Identifier of the new integration."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:integrations"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/integrations/{id}":{"get":{"tags":["Integrations"],"summary":"Read one integration","description":"Returns a single integration including its stored `config`, with every secret value replaced by `********`. A value counts as secret when its field is declared with type `password` for the integration type, when its key contains `secret`, `password`, `passwd`, `token`, `apiKey`, `api_key`, `authorization`, `authValue`, `credential`, `bearer` or `cookie` or ends in `Key` (case-insensitive, at any depth), or when it is a URL that carries its secret inside it, such as the webhook URL of a Discord integration. A secret key covers everything below it. Booleans are never masked, and an empty value stays empty. That masking is one-way: this endpoint tells you which credential fields are populated, never what they contain, and there is no endpoint that reveals them. The masked config can be sent back unchanged with PATCH: a secret sent as `********` keeps its stored value. `config` comes back as a JSON string and has to be parsed. Internal connector and health bookkeeping is not returned, and neither are the connection keys only the server writes into `config`. For an integration type with tool permissions, `config` may carry `toolPermissions`, the stored choice that GET /integrations/{id}/permissions explains in full. An integration owned by another account answers 404.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The integration, with secrets in `config` masked to `********`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Integration"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}]},"patch":{"tags":["Integrations"],"summary":"Update an integration","description":"Changes the name, description, stored configuration or enabled flag of an integration. Only the fields you send are written. A `config` you send is merged with the stored one, one object level at a time. A field that is not secret takes the value you send, and one you leave out is removed, so send every non-secret field you want to keep. A secret field keeps its stored value when you leave it out or send it as `********`, an empty string or null clears it, and any other value replaces it. A value counts as secret when its field is declared with type `password` for the integration type, when its key contains `secret`, `password`, `passwd`, `token`, `apiKey`, `api_key`, `authorization`, `authValue`, `credential`, `bearer` or `cookie` or ends in `Key` (case-insensitive, at any depth), or when it is a URL that carries its secret inside it, such as the webhook URL of a Discord integration. A secret key covers everything below it. Booleans are never masked, and an empty value stays empty. Nested objects are merged by the same rules. In a list of objects, each item you send is matched to a stored item by the first of `id`, `name`, `key` or `url` that both carry with the same value; only a matched item keeps its stored secrets, so a new item has to carry them in full. A list of plain values is replaced. When the destination of a level changes (`host`, `url`, `baseUrl`, `endpoint`, `server`, `domain`, `subdomain`, `instanceUrl`, `smtpHost`, `webhookUrl`, `webhookUrl2`, `webhookUrl3`, `apiEndpoint`, `storeUrl`, `shopUrl`, or any field declared with type `url` for the integration type), the secrets stored on that level and on every level below it are not carried over to the new destination: send them again, or the request is refused with 400 and error code `destination_changed`. A config that still contains `********` where nothing is stored is refused with 400 and error code `masked_without_stored_value`. Keys inside `config` that only the server writes (the connection keys and `toolPermissions`) are dropped from what you send and keep their stored values; change the tool permissions with PUT /integrations/{id}/permissions instead. Setting `isActive` to false stops agents from calling the integration without deleting it. The response confirms the write and does not return the updated record.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The fields to change. `config`, if sent, is merged with the stored configuration: fields that are not secret are replaced, secret fields left out or sent as `********` keep their stored value.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"config":{"type":"string"},"isActive":{"type":"boolean"}},"description":"Schema for updating an existing integration"}}}},"x-required-scope":"write:integrations","responses":{"200":{"description":"The integration was updated.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:integrations"]}]},"delete":{"tags":["Integrations"],"summary":"Delete an integration","description":"Removes the integration and the credentials stored with it. When an account of the service was connected through the dashboard for this integration, that connection is removed as well, unless another integration still uses it; this happens on a best-effort basis and never blocks the delete. This cannot be undone, and any agent that was assigned this integration loses the ability to perform its actions from the moment of deletion. To stop use of an integration temporarily while keeping its configuration, set `isActive` to false with a PATCH instead.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"write:integrations","responses":{"200":{"description":"The integration was deleted.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/integrations/{id}/tools":{"get":{"tags":["Integrations"],"summary":"List the tools an integration offers","description":"Returns the actions an agent can be allowed to perform through this integration, plus the recommended default selection in `recommended`. Only a few integration types are catalogue-backed; the rest have a fixed tool set that cannot be picked from and answer 404 here, with the same status used for an integration that does not exist or belongs to someone else. Note each tool's `access` level before enabling it: `write` and `delete` tools let the agent change or remove data in the connected service. `permitted` says whether the permissions of the integration (GET /integrations/{id}/permissions) allow agents to use the tool; a tool that is not permitted cannot be put into `enabledTools` of an agent or chatbot.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The selectable tools and the recommended default selection.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CatalogTool"},"description":"Every tool this integration can expose."},"recommended":{"type":"array","items":{"type":"string"},"description":"Tool names that make up the sensible default selection for this integration type."}},"required":["data","recommended"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}]}},"/integrations/{id}/permissions":{"get":{"tags":["Integrations"],"summary":"Read the tool permissions of an integration","description":"Returns which tools the agents of the account may use through this integration: the stored choice in `preset`, every tool of the integration with `allowed`, and how many tools each preset would allow. The permissions limit agents only; the account owner keeps full access through the dashboard and the calendar endpoints. Only the integration types connected through the dashboard sign-in have tool permissions; every other type answers 404, with the same status used for an integration that does not exist or belongs to someone else. `preset` is `legacy` while nothing was chosen: agents then keep the tools they had before permissions existed.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:integrations","responses":{"200":{"description":"The permissions of the integration.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/IntegrationPermissions"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The stored configuration of the integration cannot be read (error code `validation_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:integrations"]}]},"put":{"tags":["Integrations"],"summary":"Set the tool permissions of an integration","description":"Stores which tools the agents of the account may use through this integration, exactly as the dashboard does. Choose a preset (`read`, `read_write`, `full`) or `custom` with the exact tool keys from `tools[].key` of the GET of this path. Only the permissions change; the rest of the stored configuration stays as it is. Every agent and chatbot the integration is assigned to then gets its tool list rebuilt, so the change takes effect in their next conversation: `agentsResynced` counts the agents that were updated, `agentsFailed` the ones that could not be updated now (the saved permissions still apply to them). A tool selection of an agent (`enabledTools`) that names a tool outside the new permissions is not changed, but the tool stays unavailable to the agent. An unusable choice is refused with 422 and error code `validation_error`. Integration types without tool permissions answer 404.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"requestBody":{"required":true,"description":"The new choice: a preset, or `custom` with the exact tool keys agents may use.","content":{"application/json":{"schema":{"type":"object","properties":{"preset":{"type":"string","enum":["read","read_write","full","custom"],"description":"`read` allows the reading tools, `read_write` adds the writing tools, `full` allows every tool, `custom` allows exactly the tools listed in `tools`."},"tools":{"type":"array","items":{"type":"string"},"description":"The tool keys agents may use, as listed under `tools[].key` by GET /integrations/{id}/permissions. Required and non-empty for `custom`, ignored otherwise."}},"required":["preset"],"description":"Which tools the agents of the account may use through this integration"}}}},"x-required-scope":"write:integrations","responses":{"200":{"description":"The permissions were saved and the linked agents rebuilt.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IntegrationPermissions"},"agentsResynced":{"type":"integer","description":"Agents and chatbots whose tool list was rebuilt."},"agentsFailed":{"type":"integer","description":"Agents and chatbots whose tool list could not be rebuilt now. The saved permissions still apply to them."}},"required":["data","agentsResynced","agentsFailed"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The choice cannot be saved (error code `validation_error`): the body is malformed (then `error.details` carries the per-field report), the preset is unknown or allows no tool of this integration, a `custom` choice has no tool, a duplicate or a tool key this integration does not offer, or the stored configuration of the integration cannot be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/integrations/{id}/test":{"post":{"tags":["Integrations"],"summary":"Test an integration","description":"Calls the connected service with the stored credentials and reports what came back. Use it after creating or reconfiguring an integration, since nothing is verified at write time. The shape of the result depends on the integration type: each connector builds its own diagnostic payload, so read it as free-form JSON rather than a fixed schema, and expect at least an indication of success and a message when it failed. Anything resembling a credential is stripped from the result before it is returned. A connection that fails is still reported inside a 200 response, not as an HTTP error.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the integration to test.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body. The stored configuration is used as it is.","x-required-scope":"write:integrations","responses":{"200":{"description":"The diagnostic result from the connector. The keys vary by integration type; credential-like values are removed.","content":{"application/json":{"schema":{"type":"object","description":"Connector-specific diagnostic payload. Typically carries a success flag and a human-readable message, plus whatever details that connector can report.","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:integrations"]}]}},"/users/me":{"get":{"tags":["Users"],"summary":"Get the current account profile","description":"Returns the account that the presented API key belongs to, which is the simplest way to confirm a key is valid and to discover the user id every other endpoint scopes to. `resellerId` is set when this account is a customer of a reseller, and `ownResellerId` is set when the account operates a reseller itself; both are null for a plain direct account. `avatarUrl` is always null: no avatar image is stored for API consumers, and the field exists only so the shape stays stable. The profile is returned as a bare object, not wrapped in a `data` envelope.","x-required-scope":"read:profile","responses":{"200":{"description":"The profile of the account behind the API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:profile"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/users/me/usage":{"get":{"tags":["Users"],"summary":"Get consumption for a date window","description":"Sums the metered consumption recorded for this account over a date window, broken down into voice minutes and chat sessions. With no query parameters the window runs from midnight on the first day of the current month in server local time up to the moment of the request, which makes the bare call a current-month total. Both bounds are inclusive. The two counters are returned as 0 when nothing was metered in the window rather than being omitted, and the echoed `period` tells you exactly which window was measured. This reports usage only; for what is left to spend, call GET /users/me/limits.","parameters":[{"name":"from","in":"query","required":false,"description":"Start of the window, inclusive. Defaults to the first day of the current month. Parsed as a date; an unparseable value yields an empty result rather than an error.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"End of the window, inclusive. Defaults to the current server time.","schema":{"type":"string","format":"date"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:profile","responses":{"200":{"description":"Consumption over the requested window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:profile"]}]}},"/users/me/limits":{"get":{"tags":["Users"],"summary":"Get plan allowances and remaining credit","description":"Returns what the account still has available: its status, its prepaid wallet balance in EUR, the voice minutes and chat conversations left, and the subscribed plan with its monthly allowances. `plan` is null when the account is on no plan, in which case the remaining counters are whatever credit has been granted directly. Poll this before starting a batch of calls or a campaign to avoid running an account to zero mid-run. The answer is a bare object, not wrapped in a `data` envelope.","x-required-scope":"read:profile","responses":{"200":{"description":"Current allowances and remaining credit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Limits"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:profile"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/languages":{"get":{"tags":["Utilities"],"summary":"List available interface locales","description":"Returns the user interface locales the platform ships, for populating a language picker or for validating a value before writing it to a profile or an agent. The list is read from the locale directories on disk, so it reflects the running build rather than a database table; reseller-specific locale folders are excluded. If that directory cannot be read the handler falls back to `en` and `de` instead of failing. Note two things about the shape: the response is a bare JSON array, not the usual `{ data: [...] }` envelope, and each entry carries only a `code`, with no display name. The route requires a valid API key but checks no scope.","responses":{"200":{"description":"The available locales, as a bare array. Ordering follows the directory listing and is not guaranteed.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Language"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/languages/agent":{"get":{"tags":["Utilities"],"summary":"List the languages an agent can speak or write","description":"Returns the languages you can assign to an agent, each with an English name, the name the language calls itself, and a region code for a flag image, so a language picker needs no table of its own. This is a different list from `GET /languages`, which returns the locales the dashboard interface is translated into. Populating an agent picker from the locale list is a common mistake and leaves a customer with three choices where the platform supports dozens. Set `type` to `chat` for a text agent, which has no speech synthesis and therefore the longer list. For `voice`, language support depends on the speech model, so pass the same `ttsModel` value you will create the agent with and the answer matches what that agent will accept; leaving it unset answers for the default model. The voice list is read from the live model catalogue, and `meta.source` reports `catalog` when it came from there or `defaults` when a built-in list was used instead. Set `type` to `all` to name any language code the platform may have stored, which is what a list of agents needs to print a language instead of a bare code. The route requires a valid API key but checks no scope.","parameters":[{"name":"type","in":"query","required":false,"description":"Which kind of agent the languages are for, or `all` to name every language the platform knows.","schema":{"type":"string","enum":["voice","chat","all"],"default":"voice"}},{"name":"ttsModel","in":"query","required":false,"description":"Speech model the voice agent will use, for example `echocall-ultra`. Ignored for `type=chat` and `type=all`. Unset means the default model.","schema":{"type":"string","enum":["echocall-flash","echocall-flash-lite","echocall-ultra","echocall-multilingual"]}},{"$ref":"#/components/parameters/ActAsCustomer"}],"responses":{"200":{"description":"The languages available for that kind of agent, sorted by English name.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentLanguage"},"description":"The available languages."},"meta":{"type":"object","properties":{"type":{"type":"string","enum":["voice","chat","all"],"description":"The kind of agent the list applies to."},"ttsModel":{"type":"string","nullable":true,"description":"Speech model the answer applies to. Null for `type=chat` and `type=all`."},"count":{"type":"integer","description":"How many languages the list holds."},"source":{"type":"string","enum":["catalog","defaults"],"description":"Whether the list came from the live model catalogue or from the built-in tables."}},"required":["type","ttsModel","count","source"]}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":[]}]}},"/notifications":{"get":{"tags":["Notifications"],"summary":"List account notifications","description":"Returns the notification inbox of the account behind the API key, newest first by creation time. Notifications are raised by the platform for events such as ticket replies or billing changes, and each entry may point at the resource that caused it through `relatedType` and `relatedId`. Pass `unreadOnly=true` to see only unread entries: the handler compares the raw query string against the literal text `true`, so any other value, including `1` or `TRUE`, is treated as not set. The `pagination` object carries `page`, `perPage` and `total`, the last being a real count of the notifications matching the same `unreadOnly` filter as the page.","parameters":[{"name":"unreadOnly","in":"query","required":false,"description":"Set to the literal string `true` to return only unread notifications. Any other value is ignored and the full inbox is returned.","schema":{"type":"string","enum":["true","false"]}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:profile","responses":{"200":{"description":"A page of notifications, newest first, with the full count in `pagination.total`.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"The page that was returned."},"perPage":{"type":"integer","description":"Items per page that was applied after clamping."},"total":{"type":"integer","description":"Total number of matching notifications, across all pages."}},"required":["page","perPage","total"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:profile"]}]}},"/notifications/{id}/read":{"patch":{"tags":["Notifications"],"summary":"Mark one notification as read","description":"Marks a single notification as read and stamps its `readAt` with the current server time. Ownership is verified first: a notification belonging to another account answers 404 exactly as an unknown id does, so the endpoint never reveals that a foreign id exists. Calling it again on an already read notification succeeds and refreshes `readAt`. This endpoint takes no request body. It answers with a short confirmation message rather than the updated notification.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the notification to mark as read. Must be an integer; a non-numeric value answers 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:profile","responses":{"200":{"description":"The notification was marked as read.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation text.","example":"Notification marked as read"}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:profile"]}]}},"/notifications/read-all":{"patch":{"tags":["Notifications"],"summary":"Mark every notification as read","description":"Marks every unread notification of this account as read in one statement and stamps each with the current server time. Entries that are already read are left untouched, so their original `readAt` is preserved. The call succeeds even when there is nothing unread, and it does not report how many rows it touched. This endpoint takes no request body.","x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:profile","responses":{"200":{"description":"Every unread notification was marked as read.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation text.","example":"All notifications marked as read"}},"required":["message"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:profile"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/voices":{"get":{"tags":["Voices"],"summary":"List the account voices","description":"Returns the voices stored on this account, including cloned and generated ones, with the status that tells you whether a voice is ready to be assigned to an agent. The identifier is the platform form `voice_<number>`, which is what agent endpoints on this account expect. The response is not paginated and takes no query parameters: the full set is returned in one call, in insertion order. Access is governed by the `read:agents` scope rather than a voice-specific scope, because voices are only useful in the context of an agent.","x-required-scope":"read:agents","responses":{"200":{"description":"Every voice on the account. No pagination object is returned.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Voice"},"description":"The account voices."}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/voices/available":{"get":{"tags":["Voices"],"summary":"List selectable library voices","description":"Returns the voices from the shared library that an agent can be pointed at, each with descriptive labels and the address of a spoken sample. Without `language` the whole library is returned, which is what a portal serving several countries needs; pass a language code to narrow it to the voices offered for that language, plus the account's own cloned and generated voices, which are returned whatever the filter says. Identifiers here are the library ids used when setting a voice on an agent, which differ in form from the `voice_<number>` ids of GET /voices. The response is not paginated, and access is governed by the `read:agents` scope rather than a voice-specific one. If the platform has no voice engine credential configured the call answers 500 with the code `config_error`.","parameters":[{"name":"language","in":"query","required":false,"description":"Narrow the list to voices offered for this language, as a code from GET /languages/agent, for example `de` or `pt-br`. Omit it for the full library.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The library voices. No pagination object is returned.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AvailableVoice"},"description":"The selectable library voices."}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]}},"/voices/{voiceId}/preview":{"get":{"tags":["Voices"],"summary":"Hear a voice","description":"Speaks one sample sentence in the requested language with the given voice and answers with the audio itself, as MP3. Use it next to a voice picker: choosing from a list of names is guesswork, and a voice that sounds right in one language can sound wrong in another. The sample is synthesized on each call, which is why the answer may take a second, and why it carries a long cache header: for one voice and one language it never changes. A language the platform has no sentence for is sampled in English, and the language actually spoken is named in the `Content-Language` header of the response. Both the library ids from GET /voices/available and the `voice_<number>` ids of GET /voices are accepted. A voice the account cannot reach answers 404.","parameters":[{"name":"voiceId","in":"path","required":true,"description":"Identifier of the voice, either a library id or the form `voice_<number>`.","schema":{"type":"string"}},{"name":"language","in":"query","required":false,"description":"The language the sample is spoken in, as a code from GET /languages/agent. Defaults to `en`.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The sample was spoken. The body is the audio.","content":{"audio/mpeg":{"schema":{"type":"string","format":"binary","description":"The spoken sample."}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]}},"/billing/plans":{"get":{"tags":["Billing"],"summary":"List the plans you can subscribe to","description":"Returns the billing plans on offer to the authenticated account. The list is picked for you: an account that belongs to a reseller sees only that reseller's plans, and a direct account sees only the platform's own plans. There is no way to ask for the other set, and attempting to check out a plan from the wrong set is refused with 403. The response is a bare JSON array, not a `{ data: [...] }` envelope, and it is not paged.","x-required-scope":"read:billing","responses":{"200":{"description":"Every plan available to this account, in database order.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/billing/plans/{id}/checkout":{"post":{"tags":["Billing"],"summary":"Start a checkout for a plan","description":"Creates a hosted card-checkout session for the plan named in the path and returns the URL to send the customer to. Nothing is charged and no subscription exists until the customer completes checkout at that URL; the resulting subscription is created by the payment provider callback, not by this call. The plan must belong to the same commercial context as the account: a reseller customer buying a platform plan, or a direct customer buying a reseller plan, is refused with 403. Despite the plan id also appearing in the request body of some clients, this endpoint reads the id from the path only and ignores the body entirely.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the plan to buy, as returned by GET /billing/plans. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body. The plan is identified by the path parameter alone.","x-required-scope":"write:billing","responses":{"200":{"description":"The checkout session was created.","content":{"application/json":{"schema":{"type":"object","properties":{"checkoutUrl":{"type":"string","format":"uri","description":"Hosted checkout page. Redirect the customer here. The link is short-lived and single-use."}},"required":["checkoutUrl"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:billing"]}]}},"/billing/invoices":{"get":{"tags":["Billing"],"summary":"List your invoices","description":"Returns the invoices raised against the account, newest first. Without paging the body is a bare JSON array rather than a `{ data: [...] }` envelope, and the whole history comes back on each call; send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total`. There is no date filter either way. Reseller customers are served from the reseller invoice ledger, which has no stored PDF: for those rows `pdfUrl` is always null and the document has to be rendered on demand through GET /billing/invoices/{id}/pdf.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:billing","responses":{"200":{"description":"Invoices for this account, newest first.","content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}]}},"/billing/invoices/{id}/pdf":{"get":{"tags":["Billing"],"summary":"Download an invoice as PDF","description":"Renders the invoice and answers in whichever form the `Accept` header asks for. Send `Accept: application/pdf` and the response body is the document itself, which is what an API key should use: the `pdfUrl` returned by the JSON form points at a route that only a signed-in browser session can read. The default, `application/json`, keeps that URL for the web app. The document is generated on each call rather than served from a cache, so expect this endpoint to be noticeably slower than the rest of the billing group, and treat the URL as temporary: download it promptly instead of storing it. Ownership is checked first, so an invoice belonging to another account answers 404 rather than 403.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the invoice, as returned by GET /billing/invoices. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"name":"Accept","in":"header","required":false,"description":"Set to `application/pdf` to receive the document itself. Anything else returns the JSON download link.","schema":{"type":"string","enum":["application/json","application/pdf"],"default":"application/json"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:billing","responses":{"200":{"description":"The PDF was rendered: the document itself, or a link to it.","content":{"application/json":{"schema":{"type":"object","properties":{"pdfUrl":{"type":"string","format":"uri","description":"Where to download the rendered invoice. Readable only with a browser session, not with an API key."}},"required":["pdfUrl"]}},"application/pdf":{"schema":{"type":"string","format":"binary","description":"The rendered invoice."}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}]}},"/billing/subscription":{"get":{"tags":["Billing"],"summary":"Get the active subscription","description":"Returns the account's current active subscription together with the plan it pays for. Only subscriptions in state `active` are considered; if several exist the most recently created one is returned. An account with no active subscription is not an empty response but a 404, so treat that status as \"on no plan\" rather than as an error worth retrying.","x-required-scope":"read:billing","responses":{"200":{"description":"The active subscription and its plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/billing/balance":{"get":{"tags":["Billing"],"summary":"Get the prepaid balance and remaining allowances","description":"Returns the account's wallet balance in EUR alongside the voice minutes and chat conversations still available in the current period. These are live counters: they fall as calls and conversations are consumed and are topped back up by a plan renewal or an add-on purchase. Use this before starting a batch campaign to confirm there is enough left to pay for it.","x-required-scope":"read:billing","responses":{"200":{"description":"Current balance and remaining allowances.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/billing/transactions":{"get":{"tags":["Billing"],"summary":"List wallet transactions","description":"Returns movements on the prepaid wallet, newest first: top-ups, charges and manual adjustments, each with the balance that resulted. The `pagination` block echoes the `page` and `perPage` that were used and carries `total`, a real count of the movements on the wallet, so the number of pages can be worked out before fetching them.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:billing","responses":{"200":{"description":"One page of wallet movements, newest first, with the full count in `pagination.total`.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transaction"}},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"The page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied after clamping."},"total":{"type":"integer","description":"Total number of matching records, across all pages."}},"required":["page","perPage","total"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}]}},"/billing/addons":{"get":{"tags":["Billing"],"summary":"List purchasable add-on packages","description":"Returns the add-on packages the account may buy on top of its plan, for example extra voice minutes or extra chat conversations. Only packages that are both active and visible are listed, and, as with plans, a reseller customer sees that reseller's packages while a direct customer sees the platform's own. The order is the catalogue sort order chosen by whoever maintains the packages, not price or name. The response has a `data` array and no pagination block.","x-required-scope":"read:billing","responses":{"200":{"description":"Add-on packages available to this account, in catalogue order.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AddonPackage"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:billing"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]}},"/resellers/stats":{"get":{"tags":["Reseller"],"summary":"Get reseller dashboard statistics","description":"Returns the headline counters of the reseller account: how many customers are linked to it, how many voice agents and chatbots those customers hold, the prepaid credit the reseller still has, and a usage figure for the last 30 days. Requires an API key issued to a reseller account; a key belonging to any other kind of account is answered with 403 `forbidden`. No API scope is checked on this route, only the reseller role. One caveat before you build on this: `thisMonthUsageCost` covers a rolling 30-day window rather than a calendar month, and is computed in cents at fixed internal rates of 30 per voice minute and 40 per chat message, so it is not a EUR amount. The response is a bare object, not wrapped in `data`.","responses":{"200":{"description":"The counters for this reseller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerStats"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/customers":{"get":{"tags":["Reseller"],"summary":"List reseller customers","description":"Lists the customer accounts linked to the reseller, newest link first, together with the balance and remaining allowances of each account. Accounts whose email has been rewritten to the terminated-account form are left out. Without paging the whole book comes back as a bare JSON array, not wrapped in `data`. Each element also carries the two link columns that are not part of the shared customer schema, `customerReference` and `notes`. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole result set; send neither and the body stays exactly as described above. Requires an API key issued to a reseller account; a key belonging to any other kind of account is answered with 403 `forbidden`. No API scope is checked on this route, only the reseller role.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"The linked customers.","content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ResellerCustomer"}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResellerCustomer"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"post":{"tags":["Reseller"],"summary":"Create a customer account","description":"Creates a new end-customer account owned by the reseller, links it to the reseller and writes an activity-log entry. The account is created active, with passwordless login and the plain user role. When `sendPasswordEmail` is true, which is the default, a login code is mailed to the address; a failure to send that mail is logged and swallowed, so the customer still exists and can request a code later. An email that already belongs to any account is refused with 400 `validation_error`. `language` (de, en or fr, default de) sets the portal and e-mail language of the account. `hubLoginEnabled` (default true) decides whether the customer may sign in on echocall.de at all; a partner running their own portal sends false, and the account is then refused at both login paths here. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","requestBody":{"required":true,"description":"The identity of the customer to create. Only `email` is required; `sendPasswordEmail` decides whether a login code is mailed out immediately, `language` sets the portal and e-mail language, and `hubLoginEnabled` decides whether the account may sign in on echocall.de.","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"company":{"type":"string"},"sendPasswordEmail":{"type":"boolean","default":true},"hubLoginEnabled":{"type":"boolean","default":true},"language":{"type":"string","enum":["de","en","fr"],"default":"de"}},"required":["email"],"description":"Create Customer Request"}}}},"responses":{"201":{"description":"The customer was created.","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"integer","description":"Identifier of the new customer account. Use it as the `{id}` of the other customer routes."},"success":{"type":"boolean","description":"Always true."}},"required":["userId","success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}":{"get":{"tags":["Reseller"],"summary":"Get one reseller customer","description":"Returns one linked customer in the same shape the list reports, link columns included, so a caller that already knows the id does not have to page the whole book. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The linked customer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerCustomer"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"patch":{"tags":["Reseller"],"summary":"Update a customer account","description":"Changes the name, company, email or account status of one of the reseller customers, and writes an activity-log entry. Every field is optional, but note that `firstName`, `lastName`, `company` and `accountStatus` are written straight through: omitting one of those clears the stored value rather than leaving it alone. Only `email` is handled conditionally and is skipped when absent. Moving the email to an address another account already uses is refused with 409 `conflict`. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The fields to write. Name, company and account status are overwritten with whatever is sent, including nothing at all.","content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"company":{"type":"string"},"email":{"type":"string"},"accountStatus":{"type":"string","enum":["active","suspended","trial"]}},"description":"Update Customer Request"}}}},"responses":{"200":{"description":"The customer was updated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"},"delete":{"tags":["Reseller"],"summary":"Delete a customer account","description":"Deletes one of the reseller customers and writes an activity-log entry. Everything the account owns goes with it: voice agents, chatbots, knowledge entries, tickets, campaigns, webhooks, notifications, the reseller link and the subscription history. It cannot be undone, so prefer suspending an account over deleting it unless removal is really what you want. A customer that still holds a subscription in `active`, `paused` or `past_due` is refused with 400 `subscription_active` and nothing is deleted: cancel the subscriptions first. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The customer was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/credits":{"post":{"tags":["Reseller"],"summary":"Grant customer voice or chat credits","description":"Grants voice minutes or chat conversations to one customer on top of its plan and writes the supplied `reason` into the reseller activity log. The grant is booked in the customer credits ledger, which the usage pool reads, so it shows in the customer dashboard at once. `amount` is a whole number greater than 0: this endpoint only adds. To reduce what a customer may use, change its plan. This moves the customer allowance only: it does not touch the reseller own credit pool and it books no wallet transaction. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"Which allowance to grant to, how many units, and why. `reason` is required and is stored in the activity log.","content":{"application/json":{"schema":{"type":"object","properties":{"creditType":{"type":"string","enum":["voice_minutes","chat_conversations"],"description":"Which allowance the grant goes to."},"amount":{"type":"number","description":"Units to add: whole voice minutes or chat conversations. Must be greater than 0; this endpoint only grants."},"reason":{"type":"string","description":"Why the grant is made. Stored in the activity log and on the ledger entry."}},"required":["creditType","amount","reason"],"description":"Grant Customer Credits"}}}},"responses":{"200":{"description":"The grant was booked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"creditType":{"type":"string","enum":["voice_minutes","chat_conversations"],"description":"The allowance the grant went to."},"granted":{"type":"integer","description":"The units that were added."},"balance":{"type":"number","description":"The ledger balance of that allowance after the grant."}},"required":["success","creditType","granted","balance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/balance":{"post":{"tags":["Reseller"],"summary":"Adjust a customer wallet balance","description":"Moves the EUR wallet balance of one customer by a signed `amount`, floored at 0, and records an `admin_adjustment` transaction plus an activity-log entry. The transaction carries the balance the wallet actually holds afterwards, read back from the account, and the response repeats it as `newBalance`. One thing to know before using a negative amount: a deduction larger than the balance is not refused, it is clamped, so the wallet lands on 0 and the difference is lost rather than reported. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The signed EUR amount to apply and the reason recorded against it.","content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number"},"reason":{"type":"string"}},"required":["amount","reason"],"description":"Adjust Customer Balance"}}}},"responses":{"200":{"description":"The wallet was adjusted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"newBalance":{"type":"number","description":"The EUR balance the wallet holds after the adjustment."}},"required":["success","newBalance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"},"get":{"tags":["Reseller"],"summary":"Get a customer wallet balance","description":"Returns the prepaid EUR wallet balance of one customer of the reseller, together with the email of that account so a caller can confirm it has the right customer. The balance is a number, parsed from the stored decimal, and is 0 when the column is empty. Voice and chat allowances are not part of this response. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The wallet balance of that customer.","content":{"application/json":{"schema":{"type":"object","properties":{"balance":{"type":"number","description":"Wallet balance in EUR."},"email":{"type":["string","null"],"description":"Login email of the customer.","format":"email"}},"required":["balance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/plans":{"get":{"tags":["Reseller"],"summary":"List the plans owned by the reseller","description":"Lists the billing plans this reseller has created, in the reseller own sort order, including the inactive and hidden ones. The response is a bare JSON array, not wrapped in `data`. `features` is parsed from its stored JSON into an array here; the joined plan objects returned by the subscription endpoints leave it as a string. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","responses":{"200":{"description":"Every plan owned by this reseller.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"post":{"tags":["Reseller"],"summary":"Create a plan","description":"Creates a billing plan owned by the reseller, which can then be sold to its customers through POST /resellers/subscriptions, or, for a plan of type `addon`, through POST /resellers/addons/sell. New plans are always created active, visible and with sort order 0; visibility and activity can be changed afterwards with the update endpoint. `features` is stored as JSON and defaults to an empty list. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","requestBody":{"required":true,"description":"The plan definition. `priceEur` must be positive. `voiceMinutesPerMonth` and `chatConversationsPerMonth` are the monthly allowances the plan grants, and on an add-on plan they double as the size of the credit package.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"type":{"type":"string","enum":["voice","chat","addon"]},"priceEur":{"type":"number"},"billingCycle":{"type":"string","enum":["monthly","yearly","one-time"]},"features":{"type":"array","items":{"type":"string"}},"voiceMinutesPerMonth":{"type":["number","null"]},"chatConversationsPerMonth":{"type":["number","null"]}},"required":["name","type","priceEur","billingCycle"],"description":"Create Reseller Plan Request"}}}},"responses":{"201":{"description":"The plan was created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new plan."},"success":{"type":"boolean","description":"Always true."}},"required":["id","success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/plans/{id}":{"patch":{"tags":["Reseller"],"summary":"Update a plan","description":"Changes a plan owned by the reseller. Only the fields present in the body are written, and `name`, `priceEur` and `features` are additionally ignored when sent empty or zero, so a plan cannot be renamed to an empty string or repriced to 0 through this route. A plan belonging to another reseller answers 404 `not_found`. Changing the price does not reprice subscriptions that already exist. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the plan. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The fields to change. `isVisible` controls whether the plan is offered, `isActive` whether it can still be subscribed to.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"priceEur":{"type":"number"},"isActive":{"type":"boolean"},"isVisible":{"type":"boolean"},"features":{"type":"array","items":{"type":"string"}}},"description":"Update Reseller Plan Request"}}}},"responses":{"200":{"description":"The plan was updated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"},"delete":{"tags":["Reseller"],"summary":"Delete a plan","description":"Deletes a plan owned by the reseller. A plan that any subscription still points at cannot be deleted: the handler counts subscriptions on the plan in every status, not only the active ones, and answers 400 `plan_in_use` with \"Cannot delete plan with active subscriptions\" when that count is above zero. A plan belonging to another reseller answers 404 `not_found`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the plan. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The plan was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/tickets":{"get":{"tags":["Reseller"],"summary":"List tickets raised by reseller customers","description":"Lists the support tickets opened by the customers of this reseller, newest first, without their message threads: fetch a single ticket for the conversation. Two filters are available, and a `customerId` that is not a positive integer is rejected with 400 `invalid_id`; a `customerId` that is not a customer of this reseller simply matches nothing. Without paging the response is a bare JSON array of ticket rows, and a reseller with no customers gets an empty array. These are the raw ticket records, so the identifier is a plain integer rather than the `ticket_<number>` form used by the /tickets endpoints, and they carry the escalation and reseller-scope columns as well. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole result set; send neither and the body stays exactly as described above. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"status","in":"query","required":false,"description":"Return only tickets in this state. Omit for all of them.","schema":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"]}},{"name":"customerId","in":"query","required":false,"description":"Return only tickets of this customer. A non-numeric or zero value is rejected with 400; an empty value means no filter.","schema":{"type":"integer","minimum":1}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Tickets of the customers of this reseller.","content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Ticket identifier. Numeric here, unlike the `ticket_<number>` form used by the /tickets endpoints."},"userId":{"type":"integer","description":"The customer who raised the ticket."},"resellerId":{"type":["integer","null"],"description":"The reseller the ticket is scoped to."},"subject":{"type":"string","description":"One-line summary of the issue."},"description":{"type":"string","description":"The full text the reporter wrote."},"category":{"type":"string","enum":["technical","billing","feature","other"],"description":"What the ticket is about."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"How urgent it is."},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"],"description":"Where the ticket stands."},"assignedToAdmin":{"type":["integer","null"],"description":"Platform administrator the ticket was escalated to. Set to 1 by the escalate endpoint."},"resolvedAt":{"type":["string","null"],"format":"date-time","description":"When it was resolved."},"closedAt":{"type":["string","null"],"format":"date-time","description":"When it was closed."},"createdAt":{"type":"string","format":"date-time","description":"When it was opened."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","userId","subject","status"]}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Ticket identifier. Numeric here, unlike the `ticket_<number>` form used by the /tickets endpoints."},"userId":{"type":"integer","description":"The customer who raised the ticket."},"resellerId":{"type":["integer","null"],"description":"The reseller the ticket is scoped to."},"subject":{"type":"string","description":"One-line summary of the issue."},"description":{"type":"string","description":"The full text the reporter wrote."},"category":{"type":"string","enum":["technical","billing","feature","other"],"description":"What the ticket is about."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"How urgent it is."},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"],"description":"Where the ticket stands."},"assignedToAdmin":{"type":["integer","null"],"description":"Platform administrator the ticket was escalated to. Set to 1 by the escalate endpoint."},"resolvedAt":{"type":["string","null"],"format":"date-time","description":"When it was resolved."},"closedAt":{"type":["string","null"],"format":"date-time","description":"When it was closed."},"createdAt":{"type":"string","format":"date-time","description":"When it was opened."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","userId","subject","status"]}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/tickets/{id}":{"get":{"tags":["Reseller"],"summary":"Get one customer ticket with its conversation","description":"Returns a single ticket raised by one of the reseller customers, together with the customer who raised it and the whole message thread in the order it was written. Internal notes are part of the thread here, flagged by `isInternal`, because the reseller is both their author and their only audience. A ticket that does not exist answers 404, and one belonging to a customer of another reseller answers 403 `forbidden`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric ticket identifier, as returned by GET /resellers/tickets. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The ticket, its customer and its messages. Note the top-level shape: `ticket`, `customer` and `messages` are siblings, and the response is not wrapped in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"ticket":{"type":"object","properties":{"id":{"type":"integer","description":"Ticket identifier. Numeric here, unlike the `ticket_<number>` form used by the /tickets endpoints."},"userId":{"type":"integer","description":"The customer who raised the ticket."},"resellerId":{"type":["integer","null"],"description":"The reseller the ticket is scoped to."},"subject":{"type":"string","description":"One-line summary of the issue."},"description":{"type":"string","description":"The full text the reporter wrote."},"category":{"type":"string","enum":["technical","billing","feature","other"],"description":"What the ticket is about."},"priority":{"type":"string","enum":["low","medium","high","urgent"],"description":"How urgent it is."},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"],"description":"Where the ticket stands."},"assignedToAdmin":{"type":["integer","null"],"description":"Platform administrator the ticket was escalated to. Set to 1 by the escalate endpoint."},"resolvedAt":{"type":["string","null"],"format":"date-time","description":"When it was resolved."},"closedAt":{"type":["string","null"],"format":"date-time","description":"When it was closed."},"createdAt":{"type":"string","format":"date-time","description":"When it was opened."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","userId","subject","status"]},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer who raised the ticket. Null when the user row could not be read."},"messages":{"type":"array","description":"The conversation, oldest first. Empty when nobody has replied yet: the opening text lives on the ticket itself, in `description`.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Message identifier."},"message":{"type":"string","description":"The text that was written."},"isInternal":{"type":"boolean","description":"True for a note meant for the reseller only. The customer never sees these; they are returned here because the reseller wrote them."},"createdAt":{"type":"string","format":"date-time","description":"When it was written."},"attachments":{"type":"array","description":"Files attached to the message, as metadata only: the platform stores no file, it stores where the file is. Empty when there are none, and also empty when the stored value could not be read back.","items":{"type":"object","additionalProperties":true}},"sender":{"type":"object","properties":{"id":{"type":["integer","null"],"description":"Account that wrote the message. Null for a message written by the platform itself."},"name":{"type":["string","null"],"description":"Display name of the writer, `System` for a platform message, and null for an account that carries no name on record."},"role":{"type":["string","null"],"description":"Role of the writer, as stored on the account."}},"required":["id","name"]}},"required":["id","message","isInternal","createdAt","attachments","sender"]}}},"required":["ticket","messages"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"patch":{"tags":["Reseller"],"summary":"Set the status of a customer ticket","description":"Moves a ticket raised by one of the reseller customers to a new status and emits a `ticket.updated` webhook. Status is the only field this route changes. Any of the five statuses can be set from any other, there is no state machine. A ticket that does not exist answers 404, and one belonging to a customer of another reseller answers 403 `forbidden`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric ticket identifier. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The status to set.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"]}},"required":["status"],"description":"Update Ticket Status Request"}}}},"responses":{"200":{"description":"The status was changed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/tickets/{id}/reply":{"post":{"tags":["Reseller"],"summary":"Reply to a customer ticket","description":"Posts a message onto a ticket raised by one of the reseller customers. A public reply, which is the default, also moves the ticket to status `waiting` and emits a `ticket.updated` webhook; an internal note, sent with `isInternal` true, is stored against the ticket but changes no status and fires no webhook. A ticket that does not exist answers 404, and one belonging to a customer of another reseller answers 403 `forbidden`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric ticket identifier, as returned by GET /resellers/tickets. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The reply text, whether it is internal, and any files to attach. Attachments are metadata only: host the file yourself and pass its URL, size and MIME type here.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"isInternal":{"type":"boolean","default":false},"attachments":{"type":"array","items":{"type":"object","properties":{"filename":{"type":"string"},"url":{"type":"string"},"mimeType":{"type":"string"},"size":{"type":"number"}},"required":["filename","url","mimeType","size"]}}},"required":["message"],"description":"Reply to Ticket Request"}}}},"responses":{"200":{"description":"The reply was posted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"messageId":{"type":"integer","description":"Identifier of the message that was written."}},"required":["success","messageId"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/tickets/{id}/escalate":{"post":{"tags":["Reseller"],"summary":"Escalate a customer ticket to platform support","description":"Hands a ticket over to platform support: the ticket is assigned to the platform administrator, its status is reset to `open` whatever it was before, and a `ticket.updated` webhook is emitted. Use it when a customer problem is not something the reseller can resolve itself. The call is idempotent in effect, escalating an already escalated ticket simply reopens it again. A ticket that does not exist answers 404, and one belonging to a customer of another reseller answers 403 `forbidden`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric ticket identifier. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The ticket was escalated.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/subscriptions":{"get":{"tags":["Reseller"],"summary":"List subscriptions across all reseller customers","description":"Lists the subscriptions of the customers of this reseller, newest first, each with its plan and a short reference to the customer. A reseller with no customers gets an empty list. Without paging there is no total in the response: use `limit` to size the result and GET /resellers/subscriptions/count for the total. `status` is passed to the query without being checked, so a value outside the enum simply matches nothing. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole result set; send neither and the body stays exactly as described above. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"status","in":"query","required":false,"description":"Return only subscriptions in this status. Omit for all of them.","schema":{"type":"string","enum":["active","past_due","canceled","paused","expired"]}},{"name":"limit","in":"query","required":false,"description":"Maximum number of subscriptions to return. Defaults to 100 and is clamped to the range 1 to 500. Ignored once page or perPage is present.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 500; larger values are reduced to 500.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"The matching subscriptions.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"integer","description":"Subscription identifier."},"customerId":{"type":"integer","description":"The customer account the subscription belongs to."},"planId":{"type":"integer","description":"The billing plan being paid for."},"contractDuration":{"type":"integer","description":"Contract length in months."},"isPrimary":{"type":"boolean","description":"Whether this is the main subscription of the customer. Subscriptions created through this API are never marked primary."},"status":{"type":"string","enum":["active","past_due","canceled","paused","expired"],"description":"Where the subscription stands."},"startDate":{"type":"string","format":"date-time","description":"When the subscription began."},"renewalDate":{"type":["string","null"],"format":"date-time","description":"Next billing date. Not set by this API."},"canceledAt":{"type":["string","null"],"format":"date-time","description":"When cancellation was recorded."},"endDate":{"type":["string","null"],"format":"date-time","description":"When the subscription ends. Set on creation to the start date plus the contract duration, and rewritten on cancellation."},"autoRenew":{"type":"boolean","description":"Whether the subscription renews without action."},"pendingPlanId":{"type":["integer","null"],"description":"A plan change scheduled for the end of the contract."},"pendingPlanEffectiveDate":{"type":["string","null"],"format":"date-time","description":"When that scheduled plan change takes effect."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","customerId","planId","status"]},"plan":{"anyOf":[{"$ref":"#/components/schemas/Plan"},{"type":"null"}],"description":"The plan the subscription is for, read through a left join. Null when the plan row no longer exists. Note that on this response `features` is the stored JSON string, not a parsed array: only GET /resellers/plans parses it."},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer holding the subscription. Null when the user row could not be joined."}},"required":["subscription"]}}}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"integer","description":"Subscription identifier."},"customerId":{"type":"integer","description":"The customer account the subscription belongs to."},"planId":{"type":"integer","description":"The billing plan being paid for."},"contractDuration":{"type":"integer","description":"Contract length in months."},"isPrimary":{"type":"boolean","description":"Whether this is the main subscription of the customer. Subscriptions created through this API are never marked primary."},"status":{"type":"string","enum":["active","past_due","canceled","paused","expired"],"description":"Where the subscription stands."},"startDate":{"type":"string","format":"date-time","description":"When the subscription began."},"renewalDate":{"type":["string","null"],"format":"date-time","description":"Next billing date. Not set by this API."},"canceledAt":{"type":["string","null"],"format":"date-time","description":"When cancellation was recorded."},"endDate":{"type":["string","null"],"format":"date-time","description":"When the subscription ends. Set on creation to the start date plus the contract duration, and rewritten on cancellation."},"autoRenew":{"type":"boolean","description":"Whether the subscription renews without action."},"pendingPlanId":{"type":["integer","null"],"description":"A plan change scheduled for the end of the contract."},"pendingPlanEffectiveDate":{"type":["string","null"],"format":"date-time","description":"When that scheduled plan change takes effect."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","customerId","planId","status"]},"plan":{"anyOf":[{"$ref":"#/components/schemas/Plan"},{"type":"null"}],"description":"The plan the subscription is for, read through a left join. Null when the plan row no longer exists. Note that on this response `features` is the stored JSON string, not a parsed array: only GET /resellers/plans parses it."},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer holding the subscription. Null when the user row could not be joined."}},"required":["subscription"]}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"post":{"tags":["Reseller"],"summary":"Put a customer on a plan","description":"Creates a subscription linking one of the reseller customers to one of the reseller plans. It starts immediately, is written as `active`, and its end date is set to today plus the contract duration in months. No payment is taken and no payment-provider subscription is created: this records the commercial relationship, billing the customer is up to the reseller. Nothing prevents a customer from holding several subscriptions at once. A plan that does not belong to this reseller answers 404. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. One quirk of the error shape here: when the body fails validation this route answers 400 with `error.message` set to the raw list of validation issues, an array rather than a string, and without `error.details`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","requestBody":{"required":true,"description":"The customer, the plan, and the contract duration in months. `contractDuration` is a string, one of \"3\", \"6\" or \"12\".","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"number"},"planId":{"type":"number"},"contractDuration":{"type":"string","enum":["3","6","12"]}},"required":["customerId","planId","contractDuration"]}}}},"responses":{"201":{"description":"The subscription was created. Its identifier is not returned; read it back from the list endpoint.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/subscriptions/count":{"get":{"tags":["Reseller"],"summary":"Count subscriptions across all reseller customers","description":"Returns the number of subscription records held by customers of this reseller, in every status, cancelled and expired ones included. It accepts no filter, so it does not count only what a filtered list call would return. A reseller with no customers gets 0. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","responses":{"200":{"description":"The count.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","description":"Total subscription records across all customers of this reseller."}},"required":["count"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/subscriptions/{id}/cancel":{"post":{"tags":["Reseller"],"summary":"Cancel a subscription with a 30-day grace period","description":"Cancels a customer subscription with notice: the status becomes `canceled`, the cancellation time is recorded, and the end date is rewritten to 30 days from now, so the customer keeps the plan until then. The 30 days run from the moment of the call, not from the end of the contract term. A subscription that does not exist, or that is held by a customer of another reseller, answers 404. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the subscription. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The subscription was cancelled with notice.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/subscriptions/{id}/cancel-immediate":{"post":{"tags":["Reseller"],"summary":"Cancel a subscription immediately","description":"Cancels a customer subscription without notice: the status becomes `canceled` and both the cancellation time and the end date are set to now, so the plan stops at once. Use it for termination for cause; for an ordinary cancellation use the grace-period variant instead. There is no undo, and nothing is refunded. A subscription that does not exist, or that is held by a customer of another reseller, answers 404. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the subscription. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The subscription was cancelled at once.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/subscriptions":{"get":{"tags":["Reseller"],"summary":"List the subscriptions of one customer","description":"Lists every subscription of one customer of the reseller, newest first, each with the plan it points at. Cancelled and expired subscriptions are included, so read `subscription.status` rather than assuming the first entry is the live one. This route has no limit and no paging. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The subscriptions of that customer.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","description":"One entry per subscription, newest first.","items":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"integer","description":"Subscription identifier."},"customerId":{"type":"integer","description":"The customer account the subscription belongs to."},"planId":{"type":"integer","description":"The billing plan being paid for."},"contractDuration":{"type":"integer","description":"Contract length in months."},"isPrimary":{"type":"boolean","description":"Whether this is the main subscription of the customer. Subscriptions created through this API are never marked primary."},"status":{"type":"string","enum":["active","past_due","canceled","paused","expired"],"description":"Where the subscription stands."},"startDate":{"type":"string","format":"date-time","description":"When the subscription began."},"renewalDate":{"type":["string","null"],"format":"date-time","description":"Next billing date. Not set by this API."},"canceledAt":{"type":["string","null"],"format":"date-time","description":"When cancellation was recorded."},"endDate":{"type":["string","null"],"format":"date-time","description":"When the subscription ends. Set on creation to the start date plus the contract duration, and rewritten on cancellation."},"autoRenew":{"type":"boolean","description":"Whether the subscription renews without action."},"pendingPlanId":{"type":["integer","null"],"description":"A plan change scheduled for the end of the contract."},"pendingPlanEffectiveDate":{"type":["string","null"],"format":"date-time","description":"When that scheduled plan change takes effect."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","customerId","planId","status"]},"plan":{"anyOf":[{"$ref":"#/components/schemas/Plan"},{"type":"null"}],"description":"The plan the subscription is for, read through a left join. Null when the plan row no longer exists. Note that on this response `features` is the stored JSON string, not a parsed array: only GET /resellers/plans parses it."}},"required":["subscription"]}}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/customers/{id}/balance/add":{"post":{"tags":["Reseller"],"summary":"Top up a customer wallet","description":"Adds EUR to the prepaid wallet of one customer of the reseller and books a matching `admin_adjustment` transaction carrying the correct resulting balance, which is why this is the endpoint to prefer over the older POST /resellers/customers/{id}/balance. `amount` must be positive. No money changes hands here: collecting it from the customer is the responsibility of the reseller. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. One quirk of the error shape here: when the body fails validation this route answers 400 with `error.message` set to the raw list of validation issues, an array rather than a string, and without `error.details`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The positive EUR amount to add, and an optional note stored on the transaction. Without a note a default one naming the amount is written.","content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number"},"description":{"type":"string"}},"required":["amount"]}}}},"responses":{"200":{"description":"The wallet was topped up.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"newBalance":{"type":"number","description":"Wallet balance in EUR after the top-up."}},"required":["success","newBalance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/balance/subtract":{"post":{"tags":["Reseller"],"summary":"Deduct from a customer wallet","description":"Takes EUR off the prepaid wallet of one customer of the reseller and books a matching negative `admin_adjustment` transaction. `amount` must be positive, and it is refused rather than clamped when it exceeds the current balance: that case answers 400 with `error.code` `insufficient_balance` and a message naming the amount available. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. One quirk of the error shape here: when the body fails validation this route answers 400 with `error.message` set to the raw list of validation issues, an array rather than a string, and without `error.details`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The positive EUR amount to deduct, and an optional note stored on the transaction.","content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number"},"description":{"type":"string"}},"required":["amount"]}}}},"responses":{"200":{"description":"The wallet was reduced.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"newBalance":{"type":"number","description":"Wallet balance in EUR after the deduction."}},"required":["success","newBalance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/transactions":{"get":{"tags":["Reseller"],"summary":"List the wallet transactions of one customer","description":"Returns the wallet ledger of one customer of the reseller, newest first. Every movement is included, whatever booked it: reseller adjustments, top-ups, add-on purchases and refunds. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole result set; send neither and the body stays exactly as described above. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the customer account. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of transactions to return. Defaults to 50 and is clamped to the range 1 to 200. Ignored once page or perPage is present.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 200; larger values are reduced to 200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"The wallet ledger of that customer, newest first.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Transaction identifier."},"customerId":{"type":"integer","description":"The customer wallet the movement was booked on."},"amount":{"type":"string","description":"Signed amount in EUR, as a decimal string. Negative for a deduction."},"balanceAfter":{"type":"string","description":"Wallet balance the account holds after the movement, as a decimal string. Read back from the wallet after every movement, including the ones POST /resellers/customers/{id}/balance books."},"type":{"type":"string","enum":["deposit","purchase","refund","admin_adjustment","bonus"],"description":"What caused the movement."},"paymentMethod":{"type":["string","null"],"description":"How it was paid, where a payment was involved."},"addonPurchaseId":{"type":["integer","null"],"description":"Add-on purchase the movement belongs to."},"subscriptionId":{"type":["integer","null"],"description":"Subscription the movement belongs to."},"description":{"type":["string","null"],"description":"Free-text note."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string. Reseller-initiated movements carry the reseller id here."},"createdAt":{"type":"string","format":"date-time","description":"When it was booked."}},"required":["id","customerId","amount","balanceAfter","type","createdAt"]}}}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer","description":"Transaction identifier."},"customerId":{"type":"integer","description":"The customer wallet the movement was booked on."},"amount":{"type":"string","description":"Signed amount in EUR, as a decimal string. Negative for a deduction."},"balanceAfter":{"type":"string","description":"Wallet balance the account holds after the movement, as a decimal string. Read back from the wallet after every movement, including the ones POST /resellers/customers/{id}/balance books."},"type":{"type":"string","enum":["deposit","purchase","refund","admin_adjustment","bonus"],"description":"What caused the movement."},"paymentMethod":{"type":["string","null"],"description":"How it was paid, where a payment was involved."},"addonPurchaseId":{"type":["integer","null"],"description":"Add-on purchase the movement belongs to."},"subscriptionId":{"type":["integer","null"],"description":"Subscription the movement belongs to."},"description":{"type":["string","null"],"description":"Free-text note."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string. Reseller-initiated movements carry the reseller id here."},"createdAt":{"type":"string","format":"date-time","description":"When it was booked."}},"required":["id","customerId","amount","balanceAfter","type","createdAt"]}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/addons/packages":{"get":{"tags":["Reseller"],"summary":"List the add-on packages the reseller offers","description":"Lists the credit packages the reseller can sell to its customers. These are the plans of type `addon` owned by the reseller that are both active and visible, in the reseller own sort order, reduced to the handful of fields a sale needs. A package counts as a voice package when the underlying plan carries a voice-minute allowance and as a chat package otherwise, and `quantity` is that allowance. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"type","in":"query","required":false,"description":"Return only packages of this kind. Any other value matches nothing and yields an empty list; omit it for all packages.","schema":{"type":"string","enum":["voice_minutes","chat_conversations"]}}],"responses":{"200":{"description":"The packages available for sale.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","description":"One entry per sellable package.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Plan identifier of the package. This is the value to pass as `packageId` to POST /resellers/addons/sell."},"type":{"type":"string","enum":["voice_minutes","chat_conversations"],"description":"What the package grants. Derived from the plan: a plan with a voice-minute allowance counts as `voice_minutes`, everything else as `chat_conversations`."},"name":{"type":"string","description":"Package name."},"quantity":{"type":"integer","description":"How many units the package grants. 0 when the plan carries neither allowance."},"totalPrice":{"type":"number","description":"Price of the whole package in EUR."},"description":{"type":"string","description":"What the package includes. Empty string when the plan has no description."}},"required":["id","type","name","quantity","totalPrice","description"]}}},"required":["data"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/addons/purchases":{"get":{"tags":["Reseller"],"summary":"List add-on purchases across reseller customers","description":"Lists the add-on purchases booked for customers of this reseller, newest first, each with a short reference to the customer. A reseller with no customers gets an empty list. `customerId` narrows the list to one customer and is checked for ownership; a value that is not a positive integer is answered with 400 `invalid_id` rather than dropped, so a malformed filter can never return the unfiltered list. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"customerId","in":"query","required":false,"description":"Return only purchases of this customer. Must be one of the reseller customers. A non-numeric or zero value is rejected with 400; an empty value means no filter.","schema":{"type":"integer","minimum":1}},{"name":"limit","in":"query","required":false,"description":"Maximum number of purchases to return. Defaults to 100 and is clamped to the range 1 to 500.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"The matching purchases.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","description":"One entry per purchase.","items":{"type":"object","properties":{"addon":{"type":"object","properties":{"id":{"type":"integer","description":"Purchase identifier."},"customerId":{"type":"integer","description":"The customer who bought it."},"addonType":{"type":"string","enum":["voice_minutes","chat_conversations"],"description":"What was bought."},"quantity":{"type":"integer","description":"Units granted."},"unitPriceEur":{"type":"string","description":"Price per unit in EUR, as a decimal string."},"totalPriceEur":{"type":"string","description":"Price of the purchase in EUR, as a decimal string."},"status":{"type":"string","enum":["pending","processing","completed","failed","canceled"],"description":"State of the purchase. Sales made through this API are written as `completed`."},"paymentDate":{"type":["string","null"],"format":"date-time","description":"When it was paid."},"expiryDate":{"type":["string","null"],"format":"date-time","description":"When the granted credits lapse. Set to twelve months after the sale."},"usedAmount":{"type":["integer","null"],"description":"Units of the purchase already consumed."},"metadata":{"type":["string","null"],"description":"Further context, as a JSON string: the source plan, who sold it and the reseller id."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","customerId","addonType","quantity","status"]},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer who bought it. Null when the user row could not be joined."}},"required":["addon"]}}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/addons/sell":{"post":{"tags":["Reseller"],"summary":"Sell an add-on package to a customer","description":"Sells one of the reseller add-on packages to one of its customers: it books a completed purchase record, grants the credits to the customer straight away and sets them to lapse twelve months later. The customer must hold at least one active subscription, otherwise the call is refused with 400 `no_subscription`. With `paymentMethod` `balance` the package price is taken from the customer wallet and a `purchase` transaction is booked, and a wallet that cannot cover the price is refused with 400 `insufficient_balance`; with `manual` no money moves and collecting it is left to the reseller. The package must be an add-on plan owned by this reseller, otherwise 404. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. One quirk of the error shape here: when the body fails validation this route answers 400 with `error.message` set to the raw list of validation issues, an array rather than a string, and without `error.details`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","requestBody":{"required":true,"description":"The customer, the package (the plan id from GET /resellers/addons/packages) and how it is paid for.","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"number"},"packageId":{"type":"number"},"paymentMethod":{"type":"string","enum":["balance","manual"]}},"required":["customerId","packageId","paymentMethod"]}}}},"responses":{"201":{"description":"The add-on was sold and the credits were granted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"purchaseId":{"type":"integer","description":"Identifier of the purchase record."},"creditsGranted":{"type":"integer","description":"Units granted to the customer. 0 when the underlying plan carries no allowance."}},"required":["success","purchaseId","creditsGranted"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/addons/stats":{"get":{"tags":["Reseller"],"summary":"Get add-on sales statistics","description":"Returns lifetime add-on sales figures across all customers of this reseller: how many purchase records exist in any status, the revenue from the completed ones, and the same pair split by add-on type. There is no date window, the figures always cover everything. A reseller with no customers gets zeroes and an empty breakdown. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","responses":{"200":{"description":"The sales figures.","content":{"application/json":{"schema":{"type":"object","properties":{"totalAddons":{"type":"integer","description":"Purchase records in any status, including failed and cancelled ones."},"totalRevenue":{"type":"number","description":"Revenue in EUR from purchases in status `completed` only."},"byType":{"type":"array","description":"The same figures split by add-on type, counting completed purchases only. A type with no completed sale is absent rather than zero.","items":{"type":"object","properties":{"type":{"type":"string","enum":["voice_minutes","chat_conversations"],"description":"The add-on type."},"count":{"type":"integer","description":"Completed purchases of this type."},"revenue":{"type":"number","description":"Revenue in EUR from them."}},"required":["type","count","revenue"]}}},"required":["totalAddons","totalRevenue","byType"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/invoices":{"get":{"tags":["Reseller"],"summary":"List invoices raised by the reseller","description":"Lists the invoices this reseller has raised against its customers, newest invoice date first, each with a short reference to the customer. Line items are not included, fetch a single invoice for those. Two filters are available, and a `customerId` that is not a positive integer is rejected with 400 `invalid_id`. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole result set; send neither and the body stays exactly as described above. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"status","in":"query","required":false,"description":"Return only invoices in this state. Omit for all of them.","schema":{"type":"string","enum":["draft","sent","paid","failed","overdue","canceled"]}},{"name":"customerId","in":"query","required":false,"description":"Return only invoices for this customer. A non-numeric or zero value is rejected with 400; an empty value means no filter.","schema":{"type":"integer","minimum":1}},{"name":"limit","in":"query","required":false,"description":"Maximum number of invoices to return. Defaults to 100 and is clamped to the range 1 to 500. Ignored once page or perPage is present.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 500; larger values are reduced to 500.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"The matching invoices.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"invoice":{"type":"object","properties":{"id":{"type":"integer","description":"Invoice identifier."},"userId":{"type":"integer","description":"The customer being invoiced."},"resellerId":{"type":"integer","description":"The reseller that raised it."},"invoiceNumber":{"type":"string","description":"Invoice number, of the form `R<resellerId>-<YYYYMM>-<NNNN>`. The counter runs per reseller."},"description":{"type":["string","null"],"description":"What the invoice covers."},"subtotalEur":{"type":"string","description":"Net total in EUR, as a decimal string."},"taxEur":{"type":"string","description":"Tax in EUR, as a decimal string."},"totalEur":{"type":"string","description":"Gross total in EUR, as a decimal string."},"status":{"type":"string","enum":["draft","sent","paid","failed","overdue","canceled"],"description":"Payment state."},"issuedDate":{"type":"string","format":"date-time","description":"Invoice date."},"dueDate":{"type":["string","null"],"format":"date-time","description":"When payment is due."},"paidDate":{"type":["string","null"],"format":"date-time","description":"When it was paid."},"billingPeriodStart":{"type":["string","null"],"format":"date-time","description":"Start of the period billed."},"billingPeriodEnd":{"type":["string","null"],"format":"date-time","description":"End of the period billed."},"paymentMethod":{"type":["string","null"],"description":"How it was or will be paid."},"lineItems":{"description":"A denormalised copy of the line items, as stored JSON. Invoices raised through this API leave it null and keep their items in the separate items list instead.","type":["object","array","null"],"additionalProperties":true},"taxRate":{"type":"string","description":"The tax rate the invoice was issued at, in percent, as a decimal string."},"pdfUrl":{"type":["string","null"],"description":"Where the rendered document is served, once one has been rendered. Readable only with a browser session, not with an API key."},"pdfGeneratedAt":{"type":["string","null"],"format":"date-time","description":"When the document was last rendered."},"customerName":{"type":["string","null"],"description":"The recipient name as it stood when the invoice was raised."},"customerCompany":{"type":["string","null"],"description":"The recipient company at that moment."},"customerAddress":{"type":["string","null"],"description":"The recipient street address at that moment."},"customerPostalCode":{"type":["string","null"],"description":"The recipient postal code at that moment."},"customerCity":{"type":["string","null"],"description":"The recipient city at that moment."},"customerCountry":{"type":["string","null"],"description":"The recipient country at that moment."},"customerVatId":{"type":["string","null"],"description":"The recipient VAT ID at that moment."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When it last changed."}},"required":["id","userId","resellerId","invoiceNumber","subtotalEur","totalEur","issuedDate"]},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer being invoiced. Null when the user row could not be joined."}},"required":["invoice"]}}}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"invoice":{"type":"object","properties":{"id":{"type":"integer","description":"Invoice identifier."},"userId":{"type":"integer","description":"The customer being invoiced."},"resellerId":{"type":"integer","description":"The reseller that raised it."},"invoiceNumber":{"type":"string","description":"Invoice number, of the form `R<resellerId>-<YYYYMM>-<NNNN>`. The counter runs per reseller."},"description":{"type":["string","null"],"description":"What the invoice covers."},"subtotalEur":{"type":"string","description":"Net total in EUR, as a decimal string."},"taxEur":{"type":"string","description":"Tax in EUR, as a decimal string."},"totalEur":{"type":"string","description":"Gross total in EUR, as a decimal string."},"status":{"type":"string","enum":["draft","sent","paid","failed","overdue","canceled"],"description":"Payment state."},"issuedDate":{"type":"string","format":"date-time","description":"Invoice date."},"dueDate":{"type":["string","null"],"format":"date-time","description":"When payment is due."},"paidDate":{"type":["string","null"],"format":"date-time","description":"When it was paid."},"billingPeriodStart":{"type":["string","null"],"format":"date-time","description":"Start of the period billed."},"billingPeriodEnd":{"type":["string","null"],"format":"date-time","description":"End of the period billed."},"paymentMethod":{"type":["string","null"],"description":"How it was or will be paid."},"lineItems":{"description":"A denormalised copy of the line items, as stored JSON. Invoices raised through this API leave it null and keep their items in the separate items list instead.","type":["object","array","null"],"additionalProperties":true},"taxRate":{"type":"string","description":"The tax rate the invoice was issued at, in percent, as a decimal string."},"pdfUrl":{"type":["string","null"],"description":"Where the rendered document is served, once one has been rendered. Readable only with a browser session, not with an API key."},"pdfGeneratedAt":{"type":["string","null"],"format":"date-time","description":"When the document was last rendered."},"customerName":{"type":["string","null"],"description":"The recipient name as it stood when the invoice was raised."},"customerCompany":{"type":["string","null"],"description":"The recipient company at that moment."},"customerAddress":{"type":["string","null"],"description":"The recipient street address at that moment."},"customerPostalCode":{"type":["string","null"],"description":"The recipient postal code at that moment."},"customerCity":{"type":["string","null"],"description":"The recipient city at that moment."},"customerCountry":{"type":["string","null"],"description":"The recipient country at that moment."},"customerVatId":{"type":["string","null"],"description":"The recipient VAT ID at that moment."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When it last changed."}},"required":["id","userId","resellerId","invoiceNumber","subtotalEur","totalEur","issuedDate"]},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer being invoiced. Null when the user row could not be joined."}},"required":["invoice"]}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"post":{"tags":["Reseller"],"summary":"Raise an invoice for a customer","description":"Creates an invoice against one of the reseller customers from a list of line items. The handler works out the net total as quantity times unit price per line, applies `taxRate` as a percentage on top, and stores the resulting net, tax and gross amounts, so do not send totals yourself. The invoice number is generated per reseller as `R<resellerId>-<YYYYMM>-<NNNN>`, the counter continuing from the last invoice of this reseller. The invoice is created as a `draft`: nothing is sent to the customer and no payment is collected. At least one line item is required. A customer that does not exist, or that belongs to another reseller, is reported as 404 `not_found` rather than 403, so one reseller cannot probe for the customers of another. One quirk of the error shape here: when the body fails validation this route answers 400 with `error.message` set to the raw list of validation issues, an array rather than a string, and without `error.details`. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","requestBody":{"required":true,"description":"The customer, the invoice and due dates as date strings, the line items, an optional tax rate in percent (0 by default) and an optional note. The note is accepted by the schema but is not stored on the invoice.","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"number"},"invoiceDate":{"type":"string"},"dueDate":{"type":"string"},"items":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number"},"itemType":{"type":"string","enum":["subscription","chat_sessions","voice_minutes","addon","other"]}},"required":["description","quantity","unitPrice","itemType"]}},"taxRate":{"type":"number","default":0},"notes":{"type":"string"}},"required":["customerId","invoiceDate","items"]}}}},"responses":{"201":{"description":"The invoice was created as a draft.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."},"invoiceId":{"type":"integer","description":"Identifier of the new invoice."},"invoiceNumber":{"type":"string","description":"The generated invoice number."}},"required":["success","invoiceId","invoiceNumber"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/invoices/{id}":{"get":{"tags":["Reseller"],"summary":"Get one invoice with its line items","description":"Returns a single invoice raised by this reseller, together with the customer it is addressed to and its line items. An invoice of another reseller answers 404 `not_found`. The recipient details are a snapshot taken when the invoice was raised, so a later change to the customer record leaves the document as it went out. `pdfUrl` is null until a document has been rendered; render one with GET /resellers/invoices/{id}/pdf. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the invoice. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The invoice, its customer and its lines. Note the top-level shape: `invoice`, `customer` and `items` are siblings, and the response is not wrapped in `data`.","content":{"application/json":{"schema":{"type":"object","properties":{"invoice":{"type":"object","properties":{"id":{"type":"integer","description":"Invoice identifier."},"userId":{"type":"integer","description":"The customer being invoiced."},"resellerId":{"type":"integer","description":"The reseller that raised it."},"invoiceNumber":{"type":"string","description":"Invoice number, of the form `R<resellerId>-<YYYYMM>-<NNNN>`. The counter runs per reseller."},"description":{"type":["string","null"],"description":"What the invoice covers."},"subtotalEur":{"type":"string","description":"Net total in EUR, as a decimal string."},"taxEur":{"type":"string","description":"Tax in EUR, as a decimal string."},"totalEur":{"type":"string","description":"Gross total in EUR, as a decimal string."},"status":{"type":"string","enum":["draft","sent","paid","failed","overdue","canceled"],"description":"Payment state."},"issuedDate":{"type":"string","format":"date-time","description":"Invoice date."},"dueDate":{"type":["string","null"],"format":"date-time","description":"When payment is due."},"paidDate":{"type":["string","null"],"format":"date-time","description":"When it was paid."},"billingPeriodStart":{"type":["string","null"],"format":"date-time","description":"Start of the period billed."},"billingPeriodEnd":{"type":["string","null"],"format":"date-time","description":"End of the period billed."},"paymentMethod":{"type":["string","null"],"description":"How it was or will be paid."},"lineItems":{"description":"A denormalised copy of the line items, as stored JSON. Invoices raised through this API leave it null and keep their items in the separate items list instead.","type":["object","array","null"],"additionalProperties":true},"taxRate":{"type":"string","description":"The tax rate the invoice was issued at, in percent, as a decimal string."},"pdfUrl":{"type":["string","null"],"description":"Where the rendered document is served, once one has been rendered. Readable only with a browser session, not with an API key."},"pdfGeneratedAt":{"type":["string","null"],"format":"date-time","description":"When the document was last rendered."},"customerName":{"type":["string","null"],"description":"The recipient name as it stood when the invoice was raised."},"customerCompany":{"type":["string","null"],"description":"The recipient company at that moment."},"customerAddress":{"type":["string","null"],"description":"The recipient street address at that moment."},"customerPostalCode":{"type":["string","null"],"description":"The recipient postal code at that moment."},"customerCity":{"type":["string","null"],"description":"The recipient city at that moment."},"customerCountry":{"type":["string","null"],"description":"The recipient country at that moment."},"customerVatId":{"type":["string","null"],"description":"The recipient VAT ID at that moment."},"createdAt":{"type":"string","format":"date-time","description":"When the record was written."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When it last changed."}},"required":["id","userId","resellerId","invoiceNumber","subtotalEur","totalEur","issuedDate"]},"customer":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer being invoiced. Null when the user row could not be joined."},"items":{"type":"array","description":"The line items of the invoice, in insertion order. Empty when it has none.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Line-item identifier."},"invoiceId":{"type":"integer","description":"The invoice the line belongs to."},"description":{"type":"string","description":"What the line charges for."},"quantity":{"type":"string","description":"Quantity, as a decimal string."},"unitPrice":{"type":"string","description":"Price per unit in EUR, as a decimal string."},"amount":{"type":"string","description":"Quantity times unit price, in EUR, as a decimal string."},"itemType":{"type":"string","enum":["subscription","chat_sessions","voice_minutes","addon","other"],"description":"What kind of charge the line is."},"createdAt":{"type":"string","format":"date-time","description":"When the line was written."}},"required":["id","invoiceId","description","quantity","unitPrice","amount","itemType"]}}},"required":["invoice","items"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/invoices/{id}/pdf":{"get":{"tags":["Reseller"],"summary":"Render an invoice as PDF","description":"Renders an invoice this reseller raised and answers in whichever form the `Accept` header asks for. Send `Accept: application/pdf` and the body is the document itself, which is what an API key should use: the `pdfUrl` of the JSON form points at a route only a signed-in browser session can read. The document carries the reseller as its sender and the recipient details as they stood when the invoice was raised. It is drawn on every call rather than served from a cache, so a correction to the company details is on the next copy. An invoice of another reseller answers 404 `not_found`. While the company details required on an invoice are incomplete nothing is rendered at all: the call answers 409 `company_details_incomplete` and lists the missing fields in `error.missing`, which are filled in with PATCH /resellers/settings/company. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the invoice. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}},{"name":"Accept","in":"header","required":false,"description":"Set to `application/pdf` to receive the document itself. Anything else returns the JSON link.","schema":{"type":"string","enum":["application/json","application/pdf"],"default":"application/json"}}],"responses":{"200":{"description":"The document was rendered: the document itself, or a link to it.","content":{"application/json":{"schema":{"type":"object","properties":{"pdfUrl":{"type":"string","format":"uri","description":"Where the rendered invoice is served. Readable only with a browser session, not with an API key."}},"required":["pdfUrl"]}},"application/pdf":{"schema":{"type":"string","format":"binary","description":"The rendered invoice."}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/invoices/{id}/mark-sent":{"post":{"tags":["Reseller"],"summary":"Mark an invoice as sent","description":"Moves an invoice of this reseller from `draft` to `pending`, recording that it has gone out to the customer. It only sets the status: no mail is sent and no document is produced, delivery itself is up to the reseller. The status is written whatever the current one is, so calling this on a paid invoice would move it back to pending. An invoice of another reseller answers 404. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the invoice. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The invoice is now pending.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/invoices/{id}/mark-paid":{"post":{"tags":["Reseller"],"summary":"Mark an invoice as paid","description":"Marks an invoice of this reseller as `paid` and stamps the payment date with the current time. It records a payment the reseller collected elsewhere: no money is moved by this call and the customer wallet is not touched. The status is written whatever the current one is, and a draft can be marked paid without being marked sent first. An invoice of another reseller answers 404. Reseller keys only, as everywhere in this group; other keys get 403 `forbidden`. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the invoice. A value that is not an integer is answered with 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The invoice is now paid.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true."}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/phone-numbers/available":{"get":{"tags":["Reseller"],"summary":"List unassigned phone numbers","description":"Returns the reseller inventory that is not yet handed out: every number booked under the reseller whose owner is still empty. Results are sorted by the number itself in ascending order and are not paginated, so the whole inventory comes back in one response. Only public columns are selected: wholesale cost, carrier identifiers and the SIP trunk usernames and passwords are never part of the payload. The key used must belong to a reseller account.","responses":{"200":{"description":"The unassigned numbers.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PhoneNumber"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/phone-numbers/assigned":{"get":{"tags":["Reseller"],"summary":"List phone numbers handed out to customers","description":"Returns the counterpart of the available list: every number booked under the reseller that currently has an owner, paired with a short reference to the customer holding it. Sorted by the number in ascending order, not paginated. Each entry nests the number under `phoneNumber` and the holder under `owner`. As with the available list, the number object is built from an explicit public column list, so carrier credentials and wholesale cost never leave the platform. Requires a reseller key.","responses":{"200":{"description":"The assigned numbers with their holders.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"phoneNumber":{"$ref":"#/components/schemas/PhoneNumber"},"owner":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer the number is assigned to. Null when the user row could not be joined."}},"required":["phoneNumber"]}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/phone-numbers/{id}/assign":{"post":{"tags":["Reseller"],"summary":"Assign a phone number to a customer","description":"Hands one number out of the reseller inventory to one of the reseller customers. The number must be booked under the calling reseller and must still be unassigned; an already assigned number answers 404, the same status used when the number does not exist at all. The customer must belong to the calling reseller, otherwise the answer is also 404, so that one reseller cannot probe for another reseller customers. On success the owner is set and the purchase timestamp is stamped with the current time. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the phone number record to assign. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}}],"requestBody":{"required":true,"description":"The customer that shall receive the number, given by its numeric user id.","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"number"}},"required":["customerId"]}}}},"responses":{"200":{"description":"The number was assigned.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/phone-numbers/{id}/release":{"post":{"tags":["Reseller"],"summary":"Take a phone number back from a customer","description":"Returns a number to the reseller inventory. The owner and the purchase timestamp are cleared, any pending cancellation is cleared as well, and a revocation timestamp is stamped. The number also stops answering on the customer voice agent and is booked back onto the reseller account, so the next assignment starts from a clean line. The number only has to be booked under the calling reseller: releasing a number that is already unassigned succeeds and simply rewrites the same empty state. A number belonging to another reseller, or none at all, answers 404. This endpoint takes no request body. The API key must belong to a reseller account.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the phone number record to release. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The number was released back into the inventory.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/phone-numbers/{id}":{"delete":{"tags":["Reseller"],"summary":"Remove a phone number from the inventory","description":"Deletes an imported number for good: the record here and its registration with the telephony provider, including the stored SIP credentials. Use it for a trunk that was imported with wrong details or whose contract has ended. The number must be booked under the calling reseller and must currently be unassigned; a number a customer still holds answers 409 `number_assigned` and has to be released first, so that a working line cannot be dropped by accident. A number belonging to another reseller, or none at all, answers 404. Deleting the record does not cancel the number with the carrier it was bought from. This endpoint takes no request body.","parameters":[{"name":"id","in":"path","required":true,"description":"Identifier of the phone number record to delete. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The number was deleted.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/analytics/usage":{"get":{"tags":["Reseller"],"summary":"Get usage totals for a date window","description":"Aggregates everything the reseller customers consumed in a date window into a single set of totals, split into a chat share and a voice share, and reported both as revenue (what the reseller charged) and as base cost (what the platform charged the reseller). Without query parameters the window runs from the first day of the current month to now. Both bounds are inclusive of whatever the date parser makes of them. The response is the totals object itself, not wrapped in a `data` envelope. Only a reseller key reaches this route.","parameters":[{"name":"startDate","in":"query","required":false,"description":"Start of the window, as an ISO 8601 date or date-time. Defaults to the first day of the current month, keeping the current time of day. A value the date parser cannot read answers 400 `invalid_date`.","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","required":false,"description":"End of the window, as an ISO 8601 date or date-time. Defaults to the moment the request is handled. A value the date parser cannot read answers 400 `invalid_date`.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Usage totals for the requested window.","content":{"application/json":{"schema":{"type":"object","properties":{"totalChatSessions":{"type":"number","description":"Chat sessions billed in the window."},"totalVoiceMinutes":{"type":"number","description":"Voice minutes billed in the window."},"totalRevenue":{"type":"number","description":"What the reseller charged its customers in the window, in EUR."},"totalBaseCost":{"type":"number","description":"What the platform charged the reseller for the same consumption, in EUR."},"totalMargin":{"type":"number","description":"Revenue minus base cost, in EUR."},"chatSessionRevenue":{"type":"number","description":"The chat share of `totalRevenue`, in EUR."},"voiceMinuteRevenue":{"type":"number","description":"The voice share of `totalRevenue`, in EUR."},"chatSessionCost":{"type":"number","description":"The chat share of `totalBaseCost`, in EUR."},"voiceMinuteCost":{"type":"number","description":"The voice share of `totalBaseCost`, in EUR."}},"required":["totalChatSessions","totalVoiceMinutes","totalRevenue","totalBaseCost","totalMargin"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/analytics/revenue":{"get":{"tags":["Reseller"],"summary":"Get the revenue and margin breakdown for a date window","description":"Reads the same aggregation as the usage endpoint but reports it from the commercial side: total revenue, the platform share of it, the margin left to the reseller, and that margin as a percentage. The percentage is reported as 0 when there was no revenue in the window, so an empty period never produces a division by zero. Window defaults and date validation match the usage endpoint: first day of the current month to now, and 400 `invalid_date` for a value that cannot be parsed. A key from a non-reseller account is rejected with 403.","parameters":[{"name":"startDate","in":"query","required":false,"description":"Start of the window, as an ISO 8601 date or date-time. Defaults to the first day of the current month, keeping the current time of day. A value the date parser cannot read answers 400 `invalid_date`.","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","required":false,"description":"End of the window, as an ISO 8601 date or date-time. Defaults to the moment the request is handled. A value the date parser cannot read answers 400 `invalid_date`.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"The revenue breakdown for the requested window.","content":{"application/json":{"schema":{"type":"object","properties":{"totalRevenue":{"type":"number","description":"What the reseller charged its customers in the window, in EUR."},"platformRevenue":{"type":"number","description":"The part of that revenue the platform charged the reseller, in EUR."},"resellerMargin":{"type":"number","description":"Revenue minus platform cost, in EUR."},"marginPercent":{"type":"number","description":"The margin as a percentage of total revenue. 0 when there was no revenue."}},"required":["totalRevenue","platformRevenue","resellerMargin","marginPercent"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/settings":{"get":{"tags":["Reseller"],"summary":"Get payment status and invoicing profile","description":"Reports whether the reseller has payment credentials on file and returns the company data printed on invoices. No secret is ever returned: the stored card processor secret key and the stored wallet credentials are reduced to the booleans `stripeConfigured` and `paypalConfigured`. The publishable key is returned in full, because it is meant to be public. Company fields that were never filled in come back as empty strings rather than null. Answers 404 when the reseller record behind the key has disappeared. Reseller keys only.","responses":{"200":{"description":"Payment status and invoicing profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerSettings"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/settings/payment-keys":{"patch":{"tags":["Reseller"],"summary":"Store payment credentials","description":"Writes the card processor and wallet credentials used to bill the reseller own customers. Every field is optional and only the fields you send are written, so this is a partial update. Empty strings are treated as absent and leave the stored value untouched, which means credentials cannot be cleared through this endpoint. The change is recorded in the reseller activity log as `payment_keys_updated`. Values written here are never readable again through the API. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string. Requires a reseller-owned API key.","requestBody":{"required":true,"description":"The credentials to store. Send only the ones you want to change.","content":{"application/json":{"schema":{"type":"object","properties":{"stripeSecretKey":{"type":"string"},"stripePublishableKey":{"type":"string"},"paypalClientId":{"type":"string"},"paypalClientSecret":{"type":"string"}}}}}},"responses":{"200":{"description":"The credentials were stored.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/settings/company":{"patch":{"tags":["Reseller"],"summary":"Update the invoicing company data","description":"Updates the company details that appear on invoices the reseller issues to its customers. All fields are optional and the object you send is written as given, so sending a field with an empty string does clear it, unlike the trading name, which must be at least one character when present. The email field must be a valid address. The change is recorded in the activity log as `company_data_updated`. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string. The calling key has to belong to a reseller.","requestBody":{"required":true,"description":"The invoicing company fields to change.","content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string"},"companyAddress":{"type":"string"},"companyVatId":{"type":"string"},"companyPhone":{"type":"string"},"companyEmail":{"type":"string"},"companyPostalCode":{"type":"string"},"companyCity":{"type":"string"},"companyCountry":{"type":"string"},"companyTaxNumber":{"type":"string"},"companyBankName":{"type":"string"},"companyIban":{"type":"string"},"companyBic":{"type":"string"},"companyPaymentTermsDays":{"type":"number"},"companyInvoiceFooter":{"type":"string"}}}}}},"responses":{"200":{"description":"The company data was updated.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/company":{"get":{"tags":["Reseller"],"summary":"Get the company and branding profile","description":"Returns the branding side of the reseller profile: trading name, legal name, address, VAT id, the logo used on invoices, the accent colour of the branded portal and the portal logo. Fields that were never set come back as null here, unlike the settings endpoint, which reports empty strings. Answers 404 if the reseller record cannot be loaded. Access is limited to reseller accounts.","responses":{"200":{"description":"The company and branding profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerCompany"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"patch":{"tags":["Reseller"],"summary":"Update the company and branding profile","description":"Partially updates the branding profile. Every field is optional and only the keys you send are written. The portal logo accepts null explicitly, which removes it, while the remaining fields take strings only. Note that the trading name, address and VAT id overlap with the invoicing profile written by PATCH /resellers/settings/company: both endpoints write the same stored record. The change is recorded in the activity log as `company_profile_updated`. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string. Reseller keys only.","requestBody":{"required":true,"description":"The profile fields to change. Omitted fields keep their stored value.","content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string"},"companyLegalName":{"type":"string"},"companyAddress":{"type":"string"},"companyVatId":{"type":"string"},"companyLogo":{"type":"string"},"brandPrimaryColor":{"type":"string"},"brandLogo":{"type":["string","null"]},"companyPostalCode":{"type":"string"},"companyCity":{"type":"string"},"companyCountry":{"type":"string"},"companyTaxNumber":{"type":"string"},"companyBankName":{"type":"string"},"companyIban":{"type":"string"},"companyBic":{"type":"string"},"companyPaymentTermsDays":{"type":"number"},"companyInvoiceFooter":{"type":"string"}}}}}},"responses":{"200":{"description":"The branding profile was updated.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/balance-status":{"get":{"tags":["Reseller"],"summary":"Get the balance with a warning level","description":"Returns the reseller prepaid EUR balance together with a health classification meant for dashboard banners. The level is `healthy` at 5 EUR or more, `warning` from 1 EUR up to 5 EUR, and `critical` below 1 EUR, and `shouldShowBanner` is true whenever the balance is under 5 EUR. A preformatted string with a euro sign is included so a caller does not have to format it. Answers 404 when the reseller record behind the key no longer exists. Use GET /resellers/balance for the plain figure. A non-reseller key is refused with 403.","responses":{"200":{"description":"The balance and its warning level.","content":{"application/json":{"schema":{"type":"object","properties":{"balance":{"type":"number","description":"Current prepaid balance in EUR, as a number."},"status":{"type":"string","enum":["healthy","warning","critical"],"description":"Health classification of the balance."},"shouldShowBanner":{"type":"boolean","description":"True when the balance is below 5 EUR."},"formattedBalance":{"type":"string","description":"The balance rendered with a euro sign and two decimals."}},"required":["balance","status","shouldShowBanner","formattedBalance"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/activity-log":{"get":{"tags":["Reseller"],"summary":"Read the reseller audit trail","description":"Returns the audit entries written whenever a reseller action changes something: customer creation, pricing changes, credential updates, suspensions and the like. Entries come back newest first. Without paging the listing is capped rather than paginated: pass `limit` to change the page size, which defaults to 50 and is clamped into the range 1 to 200, with values above 200 reduced to 200, and there is no offset, so older entries beyond the limit are out of reach. Paging is opt-in. Send `page` or `perPage` and the response becomes `{ data, pagination }` with a real `total` counted over the whole log, which does reach the older entries; send neither and the body stays exactly as described above. Reseller keys only.","parameters":[{"name":"limit","in":"query","required":false,"description":"How many entries to return, newest first. Clamped into the range 1 to 200; a value that is not a number falls back to 50. Ignored once page or perPage is present.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 200; larger values are reduced to 200.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"The most recent audit entries.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResellerActivity"}}}},{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResellerActivity"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/voice-agents":{"get":{"tags":["Reseller"],"summary":"List the voice agents of all customers","description":"A read-only overview of every voice agent owned by any customer of the calling reseller, newest first, each paired with a short reference to its owner. The listing is not paginated and returns an empty `data` array when the reseller has no customers yet. Each entry nests the stored agent record under `agent`: this is the whole database row with the internal provider columns and credential columns removed, so it carries more fields than the summarised agent shape used elsewhere in this API. The endpoint is read-only; use the agent endpoints to change anything. The API key must belong to a reseller.","responses":{"200":{"description":"Every voice agent across the reseller customer base.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"agent":{"type":"object","description":"The stored voice agent record. The handler selects the whole database row and then removes the internal provider columns and credential columns, so further stored columns beyond those listed here can appear.","properties":{"id":{"type":"integer","description":"Numeric identifier of the voice agent."},"userId":{"type":"integer","description":"The customer account that owns it."},"resellerId":{"type":["integer","null"],"description":"The reseller the record is booked under."},"name":{"type":"string","description":"Internal name of the voice agent."},"language":{"type":["string","null"],"description":"Primary BCP-47 language code."},"createdAt":{"type":"string","format":"date-time","description":"When it was created."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","name"]},"owner":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer the agent belongs to. Null when the user row could not be joined."}},"required":["agent"]}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/chatbots":{"get":{"tags":["Reseller"],"summary":"List the chatbots of all customers","description":"The chatbot counterpart of the voice agent overview: every chatbot owned by any customer of the calling reseller, newest first, each with its owner. Not paginated, and an empty `data` array when there are no customers. Each entry nests the stored chatbot record under `chatbot`, again as the whole database row minus the internal provider and credential columns. Read-only. Only a reseller-owned key is accepted, any other key is answered with 403.","responses":{"200":{"description":"Every chatbot across the reseller customer base.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"type":"object","properties":{"chatbot":{"type":"object","description":"The stored chatbot record. The handler selects the whole database row and then removes the internal provider columns and credential columns, so further stored columns beyond those listed here can appear.","properties":{"id":{"type":"integer","description":"Numeric identifier of the chatbot."},"userId":{"type":"integer","description":"The customer account that owns it."},"resellerId":{"type":["integer","null"],"description":"The reseller the record is booked under."},"name":{"type":"string","description":"Internal name of the chatbot."},"language":{"type":["string","null"],"description":"Primary BCP-47 language code."},"createdAt":{"type":"string","format":"date-time","description":"When it was created."},"updatedAt":{"type":"string","format":"date-time","description":"When it last changed."}},"required":["id","name"]},"owner":{"anyOf":[{"$ref":"#/components/schemas/OwnerRef"},{"type":"null"}],"description":"The customer the chatbot belongs to. Null when the user row could not be joined."}},"required":["chatbot"]}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/pricing":{"get":{"tags":["Reseller"],"summary":"Get platform prices, reseller prices and margins","description":"Returns what the platform charges the reseller per chat session and per voice minute, what the reseller charges its own customers, and the difference between the two. The platform figures are read live, so they can change without any action by the reseller; if they cannot be read, 0.40 EUR per chat session and 0.30 EUR per voice minute are used. A reseller that has never set its own prices gets the platform prices echoed back with both margins reported as 0. Requires a reseller account key.","responses":{"200":{"description":"The current price and margin picture.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerPricing"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"patch":{"tags":["Reseller"],"summary":"Set the customer-facing prices","description":"Sets what the reseller charges its own customers per chat session and per voice minute. Both prices are required and must be positive; the pair is then checked against the current platform prices, and a price that does not clear that check is refused with 400 `validation_error` and a single string listing the reasons. A body that fails the schema itself is also refused with 400 `validation_error`, but there `error.message` is the raw array of validation issues rather than a string, so a client should be ready for both shapes. Prices are stored for the reseller, creating the pricing record on first use, and the change is written to the activity log as `pricing_updated`. Reseller keys only.","requestBody":{"required":true,"description":"The two customer-facing prices, in EUR. Both must be greater than zero.","content":{"application/json":{"schema":{"type":"object","properties":{"resellerChatSessionPrice":{"type":"number"},"resellerVoiceMinutePrice":{"type":"number"}},"required":["resellerChatSessionPrice","resellerVoiceMinutePrice"]}}}},"responses":{"200":{"description":"The prices were stored.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/pricing/suggestions":{"get":{"tags":["Reseller"],"summary":"Get suggested price tiers","description":"Returns four ready-made pricing tiers computed from the current platform prices: break-even, plus 25 percent, plus 50 percent and plus 100 percent. Each tier carries the suggested chat and voice price along with the resulting margin in EUR and in percent. This is a calculation only: nothing is stored, and applying a tier means sending its two prices to PATCH /resellers/pricing. The response is a bare JSON array, not wrapped in a `data` envelope. This route reads no reseller-specific data, but the reseller gate still applies, so an ordinary customer key is refused.","responses":{"200":{"description":"The suggested tiers, cheapest first.","content":{"application/json":{"schema":{"type":"array","description":"Four tiers, from break-even to the widest margin.","items":{"type":"object","properties":{"tier":{"type":"string","description":"Name of the tier, for example `Minimal (Break-even)` or `Standard (25% Margin)`."},"chatSessionPrice":{"type":"number","description":"Suggested customer price per chat session, in EUR."},"voiceMinutePrice":{"type":"number","description":"Suggested customer price per voice minute, in EUR."},"chatMargin":{"type":"number","description":"Suggested price minus the platform chat price, in EUR."},"voiceMargin":{"type":"number","description":"Suggested price minus the platform voice price, in EUR."},"chatMarginPercent":{"type":"number","description":"The chat margin as a percentage of the platform price."},"voiceMarginPercent":{"type":"number","description":"The voice margin as a percentage of the platform price."}},"required":["tier","chatSessionPrice","voiceMinutePrice"]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/credits/balance":{"get":{"tags":["Reseller"],"summary":"Get the prepaid credit balance","description":"Returns the reseller stock of prepaid voice minutes and chat messages, together with the lifetime totals purchased and consumed, the low-balance alert threshold and the timestamps of the last purchase and the last consumption. A reseller that has never held credits gets a synthetic all-zero record with a threshold of 100 and no record identifier instead of a 404. Requires a reseller-linked API key.","responses":{"200":{"description":"The current credit stock.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResellerCredits"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/credits/transactions":{"get":{"tags":["Reseller"],"summary":"List credit ledger movements","description":"Returns the movements on the prepaid credit ledger, newest first. Every purchase and every deduction is recorded, each with the balances that resulted from it, so the ledger can be replayed. Like the activity log this listing is capped and not paginated: `limit` defaults to 50 and is clamped into the range 1 to 200, and there is no way to page further back. Only a reseller key is admitted.","parameters":[{"name":"limit","in":"query","required":false,"description":"How many movements to return, newest first. Clamped into the range 1 to 200; a value that is not a number falls back to 50.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"The most recent credit movements.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/ResellerCreditTransaction"},{"type":"object","properties":{"voiceMinutesBalance":{"type":"integer","description":"Voice minute balance after the movement."},"chatMessagesBalance":{"type":"integer","description":"Chat message balance after the movement."},"paymentId":{"type":["string","null"],"description":"Payment reference of a purchase, where one exists."}}}]}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/billing/keys":{"get":{"tags":["Reseller"],"summary":"Check which payment credentials are configured","description":"A deliberately minimal status check: two booleans saying whether a card processor secret key and whether wallet credentials are stored. No key material, not even a masked or truncated form of it, is ever returned. Use GET /resellers/settings for the same two flags alongside the invoicing profile and the publishable key. Answers 404 when the reseller record cannot be loaded. Reseller keys only.","responses":{"200":{"description":"Which payment credentials are on file.","content":{"application/json":{"schema":{"type":"object","properties":{"stripeConfigured":{"type":"boolean","description":"Whether a card processor secret key is stored."},"paypalConfigured":{"type":"boolean","description":"Whether wallet credentials are stored."}},"required":["stripeConfigured","paypalConfigured"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/settings/logo":{"get":{"tags":["Reseller"],"summary":"Get the brand logo","description":"Returns the logo used in the branded customer portal and the trading name shown next to it. Both come back as null when they were never set. This is the read side of the same stored value that GET /resellers/company reports as `brandLogo`. Answers 404 if the reseller record is missing. The key must belong to a reseller account.","responses":{"200":{"description":"The brand logo and display name.","content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":["string","null"],"description":"Location of the portal logo, or null when none is set."},"displayName":{"type":["string","null"],"description":"Trading name shown beside the logo, or null."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"},"patch":{"tags":["Reseller"],"summary":"Set or remove the brand logo","description":"Writes the portal logo. The single field is required but nullable: send a location to set the logo, send null to remove it. Nothing is uploaded here, the value is stored as given, so the file has to be hosted somewhere already. The activity log records the change as `logo_updated`. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string. Requires a reseller key.","requestBody":{"required":true,"description":"Where the logo lives, or null to remove it.","content":{"application/json":{"schema":{"type":"object","properties":{"logoUrl":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"The logo was stored.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/credits/purchase":{"post":{"tags":["Reseller"],"summary":"Buy prepaid credits","description":"Adds prepaid voice minutes and chat sessions to the reseller stock and books the purchase on the credit ledger. Both amounts are optional but at least one of them has to be present and non-zero, otherwise the answer is 400 `validation_error`. Both kinds of credit are charged at 0.30 EUR per unit, and the resulting total is returned and stored on the ledger entry. The credit record is created automatically on first purchase; if that fails, the answer is 404 with a note to contact the platform administrator. A successful purchase also clears the low-balance alert flag. The response status is 201. Note that this books the credits immediately and does not run a payment: settlement happens outside this call. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string.","requestBody":{"required":true,"description":"How many voice minutes and chat sessions to add. Each must be a non-negative whole number, and at least one must be given.","content":{"application/json":{"schema":{"type":"object","properties":{"voiceMinutes":{"type":"number"},"chatSessions":{"type":"number"}}}}}},"responses":{"201":{"description":"The credits were booked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true on this status."},"newBalance":{"type":"object","properties":{"voiceMinutes":{"type":"integer","description":"Voice minutes available after the purchase."},"chatSessions":{"type":"integer","description":"Chat sessions available after the purchase."}},"required":["voiceMinutes","chatSessions"]},"totalPrice":{"type":"number","description":"What the purchase cost in EUR, at 0.30 EUR per unit."}},"required":["success","newBalance","totalPrice"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/credits/deduct":{"post":{"tags":["Reseller"],"summary":"Deduct prepaid credits","description":"Books consumption against the prepaid stock, for resellers that meter usage in their own system and mirror it here. Both amounts default to 0 and at least one of them must be non-zero. A deduction larger than the available stock is refused with 400 `insufficient_balance` and nothing is written, and voice and chat are checked separately in that order. When no credit record exists at all the answer is 404. The movement is recorded on the ledger, optionally attributed to one of the reseller customers, and the last-usage timestamp is updated. Note that the response reports the chat figure under `chatMessages`, while the purchase endpoint calls the same counter `chatSessions`. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string.","requestBody":{"required":true,"description":"What to deduct. `customerId` attributes the deduction to one of your customers and `description` overrides the generated ledger note.","content":{"application/json":{"schema":{"type":"object","properties":{"voiceMinutes":{"type":"number","default":0},"chatMessages":{"type":"number","default":0},"customerId":{"type":"number"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"The deduction was booked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true on this status."},"newBalance":{"type":"object","properties":{"voiceMinutes":{"type":"integer","description":"Voice minutes left after the deduction."},"chatMessages":{"type":"integer","description":"Chat messages left after the deduction."}},"required":["voiceMinutes","chatMessages"]}},"required":["success","newBalance"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/phone-numbers/import":{"post":{"tags":["Reseller"],"summary":"Import a number the reseller already owns","description":"Brings a number the reseller holds with its own carrier onto the platform, instead of buying one from the number marketplace. Two provider styles are accepted: a SIP trunk, where the inbound and outbound trunk configuration including registrar credentials is passed in the `sipTrunk` object, and a hosted carrier account, where the account identifier and token are passed in the object named after the second `provider` value. The number is registered with the voice engine first and only then stored locally, booked under the calling reseller with the calling user as owner, so a failure at the engine leaves no local record behind. The response status is 201 and carries both the external and the local identifier. When the platform has no master voice engine credential configured the answer is 412 `precondition_failed`. A body that fails validation answers 400 with `error.code` set to `validation_error` and `error.message` carrying the raw list of validation issues as an array of objects, not as a human-readable string. Requires a reseller-owned API key.","requestBody":{"required":true,"description":"The number, its label, the direction flags (both default to true) and exactly one provider configuration matching the `provider` field. A provider block that does not match the selected provider is ignored.","content":{"application/json":{"schema":{"type":"object","properties":{"phoneNumber":{"type":"string"},"label":{"type":"string"},"supportsInbound":{"type":"boolean","default":true},"supportsOutbound":{"type":"boolean","default":true},"provider":{"type":"string","enum":["sip_trunk","twilio"]},"sipTrunk":{"type":"object","properties":{"inbound":{"type":"object","properties":{"allowedAddresses":{"type":"array","items":{"type":"string"}},"allowedNumbers":{"type":"array","items":{"type":"string"}},"mediaEncryption":{"type":"string","enum":["disabled","allowed","required"]},"username":{"type":"string"},"password":{"type":"string"},"remoteDomains":{"type":"array","items":{"type":"string"}}}},"outbound":{"type":"object","properties":{"address":{"type":"string"},"transport":{"type":"string","enum":["auto","udp","tcp","tls"]},"mediaEncryption":{"type":"string","enum":["disabled","allowed","required"]},"username":{"type":"string"},"password":{"type":"string"},"headers":{"type":"string"}},"required":["address"]}}},"twilio":{"type":"object","properties":{"sid":{"type":"string"},"token":{"type":"string"},"apiKeySid":{"type":"string"},"apiKeySecret":{"type":"string"}},"required":["sid","token"]}},"required":["phoneNumber","label","provider"]}}}},"responses":{"201":{"description":"The number was imported.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true on this status."},"phoneNumberId":{"type":"string","description":"Identifier the voice engine assigned to the imported number."},"localId":{"type":"integer","description":"Identifier of the stored phone number record, as used by the other reseller phone number endpoints."}},"required":["success","phoneNumberId","localId"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"412":{"description":"The platform has no voice engine credential configured, so numbers cannot be imported. Contact the platform administrator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/suspend":{"patch":{"tags":["Reseller"],"summary":"Suspend a customer account","description":"Blocks a customer from logging in and from using the API by setting its account state to suspended. The customer must belong to the calling reseller; a customer that does not, or does not exist, answers 404 rather than 403, so the endpoint cannot be used to discover accounts of other resellers. Suspending an already suspended account succeeds and changes nothing. The action is written to the activity log as `customer_suspended`. This endpoint takes no request body. Only reseller keys are accepted.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user id of the customer to suspend. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The account was suspended.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"},"customerId":{"type":"integer","description":"The customer that was acted on."},"status":{"type":"string","enum":["suspended"],"description":"The account state after the call."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/unsuspend":{"patch":{"tags":["Reseller"],"summary":"Reactivate a suspended customer account","description":"The counterpart of the suspend call: sets the customer account state back to active so login and API access work again. Ownership is checked the same way, so a customer of another reseller answers 404. The call is unconditional, meaning an account that was never suspended is simply set to active. The activity log records it as `customer_unsuspended`. No request body is expected. The API key has to belong to a reseller account.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user id of the customer to reactivate. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}}],"x-no-request-body":"This endpoint takes no request body.","responses":{"200":{"description":"The account was reactivated.","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"},"customerId":{"type":"integer","description":"The customer that was acted on."},"status":{"type":"string","enum":["active"],"description":"The account state after the call."}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/resellers/customers/{id}/usage":{"get":{"tags":["Reseller"],"summary":"Get one customer usage broken down by type","description":"Aggregates one customer consumption over a date window, grouped by usage type, with quantity, revenue, platform cost and margin per type. Types with no consumption in the window do not appear, so the array can be empty. The window is given by `from` and `to` and defaults to the first day of the current month at midnight up to now. Unlike the reseller-wide analytics endpoints, these two values are not validated: an unparseable date is passed to the query as-is and surfaces as 500 `internal_error` rather than as a 400. The customer must belong to the calling reseller, otherwise the answer is 404. The window that was actually applied is echoed back under `period`. Reseller keys only.","parameters":[{"name":"id","in":"path","required":true,"description":"Numeric user id of the customer. A non-numeric value answers 400 `invalid_id`.","schema":{"type":"integer"}},{"name":"from","in":"query","required":false,"description":"Start of the window, as `YYYY-MM-DD`. Defaults to the first day of the current month at midnight. Not validated: an unreadable value produces a 500.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"End of the window, as `YYYY-MM-DD`. Defaults to the moment the request is handled. Not validated: an unreadable value produces a 500.","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"The customer consumption in the window.","content":{"application/json":{"schema":{"type":"object","properties":{"customerId":{"type":"integer","description":"The customer the figures belong to."},"period":{"type":"object","properties":{"from":{"type":"string","format":"date-time","description":"Start of the window that was applied."},"to":{"type":"string","format":"date-time","description":"End of the window that was applied."}},"required":["from","to"]},"usage":{"type":"array","description":"One row per usage type that saw consumption in the window.","items":{"type":"object","properties":{"usageType":{"type":"string","description":"What was consumed, for example `voice_minute` or `chat_session`."},"totalQuantity":{"type":"number","description":"Units consumed."},"totalRevenue":{"type":"number","description":"What the customer was charged, in EUR."},"totalCost":{"type":"number","description":"What the platform charged the reseller for it, in EUR."},"totalMargin":{"type":"number","description":"Revenue minus cost, in EUR."}},"required":["usageType","totalQuantity"]}}},"required":["customerId","period","usage"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/resellers/balance":{"get":{"tags":["Reseller"],"summary":"Get the prepaid EUR balance","description":"Returns the reseller own prepaid EUR balance as a plain figure, with the currency and a preformatted string. This is the same balance that GET /resellers/balance-status reports, without the warning level, and it is kept deliberately small so external billing systems can poll it cheaply. Answers 404 when the reseller record behind the key cannot be found. Not reachable with a non-reseller API key.","responses":{"200":{"description":"The current prepaid balance.","content":{"application/json":{"schema":{"type":"object","properties":{"balance":{"type":"number","description":"Current prepaid balance, as a number."},"currency":{"type":"string","enum":["EUR"],"description":"Always `EUR`."},"formatted":{"type":"string","description":"The balance rendered with a euro sign and two decimals."}},"required":["balance","currency","formatted"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/batch-calling/campaigns":{"post":{"tags":["BatchCalling"],"summary":"Create a batch calling campaign","description":"Creates an outbound campaign: a named list of up to 10000 numbers that one voice agent will call. The campaign is created in state `draft` and nothing is dialled until you submit it, so this call places no calls and incurs no call charges. The agent must belong to you, and so must `callerPhoneNumberId` if you set one; either being unknown or owned by someone else gives 404. Per-recipient `metadata` is handed to the agent as conversation context, which is how you personalise each call. This operation is guarded by the `write:agents` scope rather than a scope of its own.","requestBody":{"required":true,"description":"The campaign name, the identifier of the voice agent that will place the calls (`agent_42`, as returned by GET /agents), and the recipient list. Optionally `scheduledTime`, an ISO 8601 timestamp for when calls should begin, and `callerPhoneNumberId`, the database id of the number to call from; without it the system default number is used.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"agentId":{"type":"string","description":"Identifier of the voice agent to call with, as returned by GET /agents (agent_42)"},"recipients":{"type":"array","items":{"type":"object","properties":{"phoneNumber":{"type":"string","description":"E.164 format phone number, e.g. +491234567890"},"name":{"type":"string","description":"Recipient display name"},"metadata":{"type":"string","description":"Custom key-value data passed to the agent as context"}},"required":["phoneNumber"]}},"scheduledTime":{"type":"string","description":"ISO 8601 timestamp. If omitted, calls start immediately upon submission."},"callerPhoneNumberId":{"type":"number","description":"Database ID of the phone number to call from. If omitted, system default is used."}},"required":["name","agentId","recipients"]}}}},"x-required-scope":"write:agents","responses":{"201":{"description":"The draft campaign was created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the new campaign."},"name":{"type":"string","description":"The name you gave it."},"status":{"type":"string","enum":["draft"],"description":"Always `draft` on creation."},"totalRecipients":{"type":"integer","description":"How many numbers were stored."},"createdAt":{"type":"string","format":"date-time","description":"When it was created."}},"required":["id","name","status","totalRecipients","createdAt"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}],"parameters":[{"$ref":"#/components/parameters/ActAsCustomer"}]},"get":{"tags":["BatchCalling"],"summary":"List batch calling campaigns","description":"Returns the account's outbound campaigns, newest first. Query parameters are validated strictly: an unrecognised `status`, or a `perPage` above 100, is rejected with 400 rather than quietly clamped, which is different from the paging behaviour elsewhere in this API. The `pagination` block echoes the applied page and page size and carries `total`, a real count of the campaigns matching the same `status` filter as the page. Reading campaigns uses the `read:agents` scope rather than a scope of its own.","parameters":[{"name":"page","in":"query","required":false,"description":"Page to return, starting at 1. Defaults to 1. A value below 1 is rejected with 400, not clamped.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page, from 1 to 100. Defaults to 20. A value outside that range is rejected with 400, not clamped.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"status","in":"query","required":false,"description":"Return only campaigns in this state. Any other value is rejected with 400.","schema":{"type":"string","enum":["draft","pending","scheduled","running","completed","cancelled","failed"]}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"One page of campaigns, newest first, with the full count in `pagination.total`.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CampaignSummary"}},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"The page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied after clamping."},"total":{"type":"integer","description":"Total number of matching records, across all pages."}},"required":["page","perPage","total"]}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]}},"/batch-calling/campaigns/{campaignId}":{"get":{"tags":["BatchCalling"],"summary":"Read a batch calling campaign","description":"Returns one campaign with its current counters: how many numbers it holds and how many calls have succeeded, failed or are still waiting. `batchId` stays null until the campaign is submitted. The counters are the ones stored on the campaign record and are updated as call results arrive, so poll this endpoint to follow a running campaign. It is guarded by the `read:agents` scope rather than a scope of its own.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Identifier of the campaign. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"The campaign and its counters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]},"delete":{"tags":["BatchCalling"],"summary":"Delete a batch calling campaign","description":"Deletes a campaign and its recipient rows permanently. Only a campaign that was never submitted, or one that has since been cancelled or failed, can be deleted: a submitted campaign in any other state is refused with 409 and has to be cancelled first. Deleting removes the recipient history along with the campaign, so export anything you need from the recipients endpoint before calling this. The response is a bare confirmation message with no `success` field, and the operation uses the `write:agents` scope rather than a scope of its own.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Identifier of the campaign.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"write:agents","responses":{"200":{"description":"The campaign and its recipients were deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Human-readable confirmation. There is no `success` field on this response."}},"required":["message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/batch-calling/campaigns/{campaignId}/submit":{"post":{"tags":["BatchCalling"],"summary":"Submit a campaign and start calling","description":"Hands a draft campaign to the calling infrastructure and starts it. This is the point where calls are actually placed and charged, so treat it as the irreversible step: only a campaign in state `draft` can be submitted, and submitting one in any other state is refused with 409. A campaign whose recipient list turned out to be empty is refused with 400. If the campaign carries a scheduled time it moves to `scheduled` and dialling begins then, otherwise it moves to `pending` and begins immediately; the state may come straight back as `running` or `completed` if dispatch reports that far already. Outbound calling being unconfigured on the platform gives 503, and a dispatch that returned no batch identifier gives 502. This operation uses the `write:agents` scope rather than a scope of its own.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Identifier of the draft campaign to submit.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body. Everything needed was set when the campaign was created.","x-required-scope":"write:agents","responses":{"200":{"description":"The campaign was dispatched.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the campaign."},"batchId":{"type":"string","description":"Identifier of the dispatched batch. Keep it for support enquiries."},"status":{"type":"string","enum":["pending","scheduled","running","completed"],"description":"The state the campaign moved to."},"message":{"type":"string","description":"Human-readable confirmation."}},"required":["id","batchId","status","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"The campaign was dispatched but the calling infrastructure answered without a batch identifier, so the campaign was left unchanged. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Outbound calling is not configured on this platform, so nothing could be dispatched. Contact support rather than retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/batch-calling/campaigns/{campaignId}/cancel":{"post":{"tags":["BatchCalling"],"summary":"Cancel a running or scheduled campaign","description":"Stops a submitted campaign: pending calls are abandoned and the campaign moves to `cancelled`. Calls already placed are not undone and remain chargeable, so cancelling limits further spend rather than reversing it. A campaign that was never submitted is refused with 400 and should be deleted instead, and one that is already `completed`, `cancelled` or `failed` is refused with 409. Cancellation is recorded locally even when the upstream stop could not be attempted. This operation uses the `write:agents` scope rather than a scope of its own.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Identifier of the campaign to cancel.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-no-request-body":"This endpoint takes no request body.","x-required-scope":"write:agents","responses":{"200":{"description":"The campaign was cancelled.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"Identifier of the campaign."},"status":{"type":"string","enum":["cancelled"],"description":"Always `cancelled`."},"message":{"type":"string","description":"Human-readable confirmation."}},"required":["id","status","message"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:agents"]}]}},"/batch-calling/campaigns/{campaignId}/recipients":{"get":{"tags":["BatchCalling"],"summary":"List a campaign's recipients and call results","description":"Returns the numbers in a campaign together with the outcome of each call: status, duration, cost and the identifier of the conversation it produced, where there is one. Rows come back in storage order, which is the order they were supplied in, and the campaign's own state is repeated in `campaignStatus` so a poller does not need a second request. Paging defaults to 50 per page here rather than 20, and `perPage` is clamped to 100. The `pagination` block carries `total`, a count of the recipients stored for this campaign. This operation uses the `read:agents` scope rather than a scope of its own.","parameters":[{"name":"campaignId","in":"path","required":true,"description":"Identifier of the campaign. A non-numeric value is rejected with 400.","schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"description":"Page to return. Starts at 1; values below 1 are clamped to 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"perPage","in":"query","required":false,"description":"Items per page. Clamped to the range 1 to 100; larger values are reduced to 100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"$ref":"#/components/parameters/ActAsCustomer"}],"x-required-scope":"read:agents","responses":{"200":{"description":"One page of recipients, with the campaign state alongside.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Recipient"},"description":"The recipients on this page, in the order they were supplied."},"pagination":{"type":"object","properties":{"page":{"type":"integer","description":"The page that was returned."},"perPage":{"type":"integer","description":"Page size that was applied after clamping."},"total":{"type":"integer","description":"Total number of matching records, across all pages."}},"required":["page","perPage","total"]},"campaignStatus":{"type":"string","enum":["draft","pending","scheduled","running","completed","cancelled","failed"],"description":"Current state of the campaign these recipients belong to."}},"required":["data","pagination","campaignStatus"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:agents"]}]}},"/provisioning/create":{"post":{"tags":["Provisioning"],"summary":"Provision a customer account","description":"Creates a customer account under the calling reseller and returns the credentials an external billing system needs to hand to the end customer. When `planId` is given, the plan must belong to the calling reseller, otherwise the call answers 404; a matching plan is written onto the user and a primary one-month auto-renewing subscription is opened, which is what unlocks access for the new account. A temporary password is always generated and stored hashed, but it is only returned in the response when `sendWelcomeEmail` is true, which is the default; with it set to false the field is absent and the password cannot be recovered afterwards. An email that already exists anywhere on the platform answers 409 `conflict`, and the endpoint is limited to 10 calls per minute per API key, answering 429 `rate_limit_exceeded` beyond that. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","requestBody":{"required":true,"description":"The customer profile to create. Only `email` is required. The address fields are stored on the billing profile, and `country` is resolved to a two-letter code, staying empty when the text cannot be resolved. `language` sets the interface language of the new account and `planId` must reference a plan owned by the calling reseller.","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"company":{"type":"string"},"phone":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"postcode":{"type":"string"},"country":{"type":"string"},"vatId":{"type":"string"},"planId":{"type":"number"},"sendWelcomeEmail":{"type":"boolean","default":true},"language":{"type":"string","enum":["de","en","fr"],"default":"de"}},"required":["email"]}}}},"responses":{"201":{"description":"The account was created.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"success","description":"Always `success` on a 201."},"userId":{"type":"integer","description":"Identifier of the new account. This is the value to pass back as `serviceId` on every later lifecycle call."},"portalUrl":{"type":"string","format":"uri","description":"Login URL for the customer. Points at the reseller portal when the reseller has a portal slug, otherwise at the platform login."},"tempPassword":{"type":"string","description":"Generated password in clear text, present only when `sendWelcomeEmail` was true. Deliver it to the customer and discard it; it cannot be read again."}},"required":["result","userId","portalUrl"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/provisioning/suspend":{"post":{"tags":["Provisioning"],"summary":"Suspend a customer account","description":"Locks a customer account: the account status becomes `suspended` and any active subscription is paused, so the billing middleware blocks the customer from using the product. Nothing is deleted, agents and data stay in place, and the change is reversible with POST /provisioning/unsuspend. The customer must belong to the calling reseller, otherwise the call answers 404, which is also the answer for an unknown id. Suspending an already suspended account succeeds and changes nothing. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","requestBody":{"required":true,"description":"Identifies the customer. `serviceId` is the numeric user id returned by POST /provisioning/create, passed as a string; a value that is not numeric answers 400.","content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"string","description":"Numeric user ID (returned from /provisioning/create)"}},"required":["serviceId"]}}}},"responses":{"200":{"description":"The account was suspended. `status` is `suspended`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisioningResult"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/provisioning/unsuspend":{"post":{"tags":["Provisioning"],"summary":"Reactivate a suspended account","description":"Reverses a suspension: the account status returns to `active` and any subscription left in the paused state is set back to active, restoring the customer access. Only paused subscriptions are touched, so a subscription that was cancelled separately stays cancelled and the customer may still be blocked by billing. The customer must belong to the calling reseller, otherwise the call answers 404. Unsuspending an active account succeeds and changes nothing. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","requestBody":{"required":true,"description":"Identifies the customer. `serviceId` is the numeric user id, passed as a string; a value that is not numeric answers 400.","content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"string","description":"Numeric user ID (returned from /provisioning/create)"}},"required":["serviceId"]}}}},"responses":{"200":{"description":"The account was reactivated. `status` is `active`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisioningResult"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/provisioning/terminate":{"post":{"tags":["Provisioning"],"summary":"Terminate a customer account","description":"Ends a customer relationship in a data-protection compliant way. This is destructive and cannot be undone: the email is replaced with a generated placeholder, the name and company are cleared, the password and all stored external service credentials are wiped, every voice agent and chatbot is set inactive, and all subscriptions are cancelled with a timestamp. The row itself is kept so the audit trail survives. The stored account status is `suspended`, which is what the response reports in `status`: the account-status field has no `terminated` value to write, so a terminated account is a suspended row whose personal data is gone. The separate `terminated` flag in the response is what distinguishes it from an ordinary suspension. The customer must belong to the calling reseller, otherwise the call answers 404. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","requestBody":{"required":true,"description":"Identifies the customer. `serviceId` is the numeric user id, passed as a string; a value that is not numeric answers 400.","content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"string","description":"Numeric user ID (returned from /provisioning/create)"}},"required":["serviceId"]}}}},"responses":{"200":{"description":"The account was terminated and its personal data anonymized. `status` is `suspended`, the value actually stored, and `terminated` is true.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvisioningResult"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/provisioning/change-package":{"post":{"tags":["Provisioning"],"summary":"Move a customer to another plan","description":"Upgrades or downgrades a customer to a different billing plan. Both the customer and the target plan must belong to the calling reseller, and either failing that check answers 404 with the same message. The plan is written onto the user record and onto the primary subscription; nothing else about the subscription is changed, so the renewal date, contract duration and status stay as they were, and no proration or invoice is produced by this call. Remaining allowances already granted to the customer are not recalculated here. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","requestBody":{"required":true,"description":"`serviceId` is the numeric user id passed as a string, and `planId` is the numeric identifier of a plan owned by the calling reseller, as listed by GET /provisioning/plans.","content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"string"},"planId":{"type":"number"}},"required":["serviceId","planId"]}}}},"responses":{"200":{"description":"The plan was changed.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"success","description":"Always `success` on a 200."},"userId":{"type":"integer","description":"The customer account that was moved."},"planId":{"type":"integer","description":"The plan the customer is now on."},"planName":{"type":"string","description":"Display name of that plan."}},"required":["result","userId","planId","planName"]}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["write:resellers"]}],"x-required-scope":"write:resellers"}},"/provisioning/onboarding-status":{"get":{"tags":["Provisioning"],"summary":"Check the reseller provisioning setup","description":"Confirms that a reseller API key is wired up correctly and reports what the integration needs to know before it starts provisioning: the reseller record, its status, whether a customer portal slug is configured, the portal login URL, and how many customers are currently linked to the reseller. `portalUrl` is null when no portal slug is set, in which case customers are sent to the platform login instead. The customer count is the total number of linked customers, without regard to their account status, despite the field name. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","responses":{"200":{"description":"The reseller setup behind the API key.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"success","description":"Always `success` on a 200."},"reseller":{"type":"object","properties":{"id":{"type":"integer","description":"Reseller identifier."},"companyName":{"type":["string","null"],"description":"Trading name of the reseller."},"status":{"type":["string","null"],"description":"State of the reseller account."},"portalConfigured":{"type":"boolean","description":"True when a customer portal slug is set."},"portalUrl":{"type":["string","null"],"description":"Customer portal login URL, or null when no portal slug is set.","format":"uri"}},"required":["id","portalConfigured"]},"stats":{"type":"object","properties":{"activeCustomers":{"type":"integer","description":"Number of customers linked to this reseller. Not filtered by account status."}},"required":["activeCustomers"]},"apiVersion":{"type":"string","description":"Version string reported by the provisioning interface. Fixed at `1.0.0` and independent of the public API version."}},"required":["result","reseller","stats","apiVersion"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/provisioning/plans":{"get":{"tags":["Provisioning"],"summary":"List the reseller billing plans","description":"Returns the active billing plans owned by the calling reseller, so an external billing system can map each of its products onto the right `planId` before calling POST /provisioning/create or POST /provisioning/change-package. Inactive plans are excluded and cannot be assigned. The list is ordered by the configured sort order and then by name, is not paginated, and takes no query parameters. The plans are returned under a `plans` key rather than the usual `data` envelope. The whole provisioning router is reserved for API keys issued to a reseller: a key without a reseller behind it is rejected with 403 `forbidden` before the handler runs. No additional scope is checked on this route.","responses":{"200":{"description":"The active plans of the calling reseller.","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"string","const":"success","description":"Always `success` on a 200."},"plans":{"type":"array","description":"The active plans, in display order.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Plan identifier to pass as `planId`."},"name":{"type":"string","description":"Display name."},"description":{"type":["string","null"],"description":"Free-text description."},"type":{"type":["string","null"],"description":"Kind of plan, for example a subscription or a credit package."},"priceEur":{"type":["string","number","null"],"description":"Price in EUR. Returned as stored, which is a decimal string."},"billingCycle":{"type":["string","null"],"description":"How often the plan is billed, for example `monthly`."},"voiceMinutesPerMonth":{"type":["integer","null"],"description":"Monthly voice-minute allowance. Null when unlimited or not set."},"chatConversationsPerMonth":{"type":["integer","null"],"description":"Monthly chat-conversation allowance. Null when unlimited or not set."},"maxAgents":{"type":["integer","null"],"description":"Maximum number of agents the plan permits. Null when unlimited or not set."}},"required":["id","name"]}}},"required":["result","plans"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/InsufficientScope"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}},"security":[{"ApiKeyAuth":["read:resellers"]}],"x-required-scope":"read:resellers"}},"/health":{"get":{"tags":["Utilities"],"summary":"Check API availability","description":"Reports that the API process is up and which version of the public interface it serves. This route is mounted before authentication, so it needs no API key and consumes no rate limit budget. The `version` field is the same value the OpenAPI document reports in `info.version`, which makes it the reliable way to tell which build you are talking to. It performs no database or downstream check: a 200 means the HTTP layer answered, not that every dependency is healthy.","responses":{"200":{"description":"The API is answering.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"ok","description":"Always `ok` when the request is answered."},"version":{"type":"string","description":"Version of the public API interface, for example `1.6.0`."},"timestamp":{"type":"string","format":"date-time","description":"Server time when the probe was answered, in ISO-8601."}},"required":["status","version","timestamp"]}}}},"500":{"$ref":"#/components/responses/InternalError"}},"security":[]}}}}