Połączenie
REST API
Kompletna dokumentacja API v1. Wszystko jest w JSON, a błędy mają jeden wspólny kształt. Ceny volai są liczbami całkowitymi w setnych częściach korony (sufiks Hal); połączone faktury używają jednostek podrzędnych podanej waluty.
Uwierzytelnianie
Każde żądanie zawiera klucz API w nagłówku Authorization: Bearer vk_... . Klucz otrzymają Państwo w portalu, w sekcji Dostęp i API (więcej szczegółów w Szybkim starcie).
curl https://volai.cz/v1/balance \
-H "Authorization: Bearer vk_YOUR_KEY"Całe to API ma też opis maszynowy pod GET /openapi.json (OpenAPI 3.1, klucz API nie jest wymagany) - można z niego wygenerować klienta, kolekcję Postman albo podłączyć go bezpośrednio do n8n.
Format błędu
Błąd ma zawsze ten sam kształt, niezależnie od tego, co zawiodło:
{
"error": {
"code": "insufficient_credit",
"cause": "account",
"message": "Insufficient credit. Account balance is 3.00 CZK. Top up at volai.cz/en/credit and try again.",
"action": "Top up the credit at https://volai.cz/en/credit (or ask the account owner to), then send the request again. Nothing was charged and volai itself is up.",
"requestId": "req_9f2b7a1c4e6d8f0a",
"docsUrl": "https://volai.cz/en/docs/api#errors"
}
}Te pięć może pojawić się niemal wszędzie - poniższa dokumentacja endpointów wymienia poza nimi tylko kody specyficzne dla sytuacji:
| HTTP | Kod | Przyczyna | Znaczenie |
|---|---|---|---|
| 401 | unauthorized | request | Brakuje nagłówka Authorization lub jest nieprawidłowy. |
| 402 | insufficient_credit | account | Na to działanie nie wystarcza salda. |
| 403 | account_suspended | account | Operator volai ręcznie zawiesił konto - prosimy skontaktować się z pomocą techniczną. Dokumenty podatkowe (GET /v1/billing/documents, /{id} i /{id}/pdf, narzędzia MCP list_billing_documents i get_billing_document) pozostają dostępne podczas zawieszenia. |
| 429 | rate_limited | busy | Przekroczono limit zapytań - w przypadku limitu klucza API należy odczekać tyle, ile podaje nagłówek Retry-After. Nie każde 429 ma ten nagłówek (na przykład dzienny limit SMS, wciąż trwający zakup albo publiczne GET /v1/changelog), dlatego należy kierować się komunikatem błędu. |
| 500 | internal_error | service | Błąd po naszej stronie - prosimy spróbować ponownie. |
400 (nieprawidłowe dane wejściowe) i 404 (nie znaleziono rekordu) mogą się pojawić niemal wszędzie, ale ich code jest specyficzny dla pola lub endpointu - dokładna lista zawsze znajduje się przy poszczególnym wywołaniu poniżej. Niektóre działania mogą też zwrócić 502 (błąd zewnętrznego dostawcy - sieci telefonicznej, ElevenLabs) albo 503 (tymczasowo niedostępne, prosimy spróbować za chwilę).
Czyja to sprawa: cause i action
Każdy błąd zawiera też cause (cztery wartości) i action (jedno zdanie po angielsku o tym, co zrobić, które można odczytać użytkownikowi dosłownie). cause to maszynowo czytelna odpowiedź na pytanie, czy błąd musi naprawić Państwa kod, Państwa konto, upływ czasu, czy my:
| Przyczyna | Kto to naprawia |
|---|---|
request | Treść, URL, nagłówki lub argumenty narzędzia w żądaniu - w tym brakujący lub nieprawidłowy klucz API. Należy poprawić dane wejściowe i wysłać ponownie; volai działa. |
account | Stan Państwa konta: saldo, limity, własność numeru lub agenta, weryfikacja e-mail, konfiguracja albo rekord, który nie istnieje na tym koncie. Treść była w porządku; należy zmienić stan konta (w portalu lub innym wywołaniem) albo wziąć właściwe id z listy i spróbować ponownie. |
busy | Tymczasowa blokada lub limit na dany moment (inna operacja na tym samym rekordzie wciąż trwa, limit zapytań). Należy odczekać kilka sekund i wysłać TO SAMO żądanie ponownie, niczego nie zmieniając. |
service | My lub nasz dostawca (sieć telefoniczna, platforma głosowa). Po Państwa stronie nie ma nic do naprawy; prosimy spróbować za chwilę, a jeśli problem się powtarza, napisać do pomocy technicznej, podając requestId. |
Dla agentów AI (Claude Code, Cursor, własne skrypty): według cause należy informować użytkownika, kto naprawia błąd. Przy request wskazać pole z message, przy account powiedzieć, co zmienić na koncie lub które id sprawdzić, przy busy odczekać i ponowić, a o problemie po stronie volai mówić tylko przy service. Nigdy nie zgłaszać request, account ani busy jako awarii volai.
Przyczynę określa kod, a nie status HTTP: limity konta (agent_limit, tool_limit, api_key_limit, relay_lease_limit) zwracają 400 i nadal są account; not_found na rekordzie to account (odpowiedź nie może ujawniać, czy cudze id istnieje), nieznana ścieżka pod /v1 to request. Pełne mapowanie kodów na przyczyny dla każdej operacji znajduje się w GET /openapi.json (opis każdej odpowiedzi z błędem).
Komunikat dla kodu validation wskazuje pole (ścieżka z kropkami w treści, na przykład dataFields.0.type), faktyczną wartość i akceptowane maksimum, na przykład Field "knowledge" is 210000 characters long, the maximum accepted is 200000. lub Field "systemPrompt" is required. Limity produktowe (60 / 6000 / 300 / 20000 znaków dla nazwy, instrukcji, celu i wiedzy agenta) przychodzą jako osobne kody invalid_name, invalid_system_prompt, invalid_goal, invalid_knowledge z dokładną liczbą. Nieznane pole w ścisłej treści (wersja robocza agenta) zwraca Unknown field "revision". Remove it; the documentation lists the accepted fields.
requestId i docsUrl
Każda odpowiedź v1 (udana lub z błędem) zawiera unikalny identyfikator żądania w nagłówku X-Request-Id (req_ i 16 znaków szesnastkowych) - błąd powtarza go też w treści jako requestId. Odtworzona odpowiedź idempotentna dostaje WŁASNY bieżący requestId - nagłówek i treść nigdy się nie rozchodzą, choć reszta odpowiedzi jest identyczna jak za pierwszym razem. Po przesłaniu tego identyfikatora pomoc techniczna znajdzie w logu dokładnie to jedno żądanie.
Treści błędów zawierają też docsUrl - link prowadzący prosto do tej sekcji.
W przypadku wersji roboczych agenta (zob. sekcja Wersje robocze agenta poniżej) błąd revision_conflict zawiera też currentRevision - bieżącą rewizję. Należy ją wczytać przez GET .../draft i przejrzeć różnice przed ponowieniem zapisu - nie należy ślepo wysyłać ponownie tej samej treści z tym samym expectedRevision.
{
"error": {
"code": "revision_conflict",
"cause": "request",
"message": "The draft has changed since you last read it. Fetch the latest revision and try again.",
"action": "Reload the record you are changing - GET /v1/agents/{id}/draft (or the get_agent_draft tool) for an agent draft, GET /v1/tasks/{id} (or the get_task tool) for a task - reapply your change on top of its current revision and send the request again with that value in expectedRevision (agent draft) or revision (task). This is not a volai outage.",
"currentRevision": 4,
"requestId": "req_9f2b7a1c4e6d8f0a",
"docsUrl": "https://volai.cz/en/docs/api#errors"
}
}Błąd subscription_confirmation_required (409) zawiera też confirmUrl - link do strony portalu, na której osoba zalogowana na konto potwierdza zmianę zwiększającą kwotę, jaką wolno obciążyć zapisaną kartę (zob. PATCH /v1/subscription). Żądanie niczego nie zmieniło; link jest ważny 24 godziny i można go użyć raz.
{
"error": {
"code": "subscription_confirmation_required",
"cause": "request",
"message": "This change was not applied, because it raises what may be charged to the saved card, and only a person signed in to the portal can confirm that. Nothing was changed. Open confirmUrl in a browser signed in to the account, review the change and confirm it; the link is valid for 24 hours and can be used once.",
"action": "Nothing was changed, on purpose: either the change raises what may be charged to the saved card, or the account is a consumer who must make the request to start the plan in person, so only that person can confirm it. Give the confirmUrl from this error to the account owner to open while signed in to the volai portal; a confirmation link is valid for 24 hours and works once, a link to the plan form stays valid. Do not send the same request again: it only produces a new link. This is not a volai outage.",
"confirmUrl": "https://volai.cz/kredit/pausal/potvrdit?t=eyJ1aWQiOiJ1X3NhbXBsZSJ9.4f1a9c0e",
"requestId": "req_9f2b7a1c4e6d8f0a",
"docsUrl": "https://volai.cz/en/docs/api#errors"
}
}Nagłówki
Po zweryfikowaniu klucza KAŻDA odpowiedź (udana lub z błędem) zawiera RateLimit-Limit, RateLimit-Remaining i RateLimit-Reset (sekundy do zresetowania bieżącego okna) - 429 z limitu klucza API dodaje też Retry-After z tą samą liczbą sekund. 429 rate_limited z limitu połączeń wychodzących (POST /v1/calls) niesie własny czas oczekiwania w Retry-After - jak długo potrwa zwolnienie tego limitu, jeśli jest znany.
401 zawiera nagłówek WWW-Authenticate: Bearer realm="volai", error="invalid_token" (RFC 6750), dzięki czemu biblioteka klienta HTTP może rozpoznać przyczynę niepowodzenia bez analizowania treści.
Nieznana ścieżka i błędna metoda
Nieznana ścieżka pod /v1 (w tym samo /v1) zwraca JSON 404 o tym samym kształcie błędu co wszędzie - {"error":{"code":"not_found", ...}} - dla każdej metody (GET, POST, PUT, PATCH, DELETE, OPTIONS i HEAD). Wyjątek: błędna METODA na ISTNIEJĄCEJ ścieżce (na przykład DELETE /v1/balance) pozostaje zwykłym 405 z Next.js bez treści JSON - jedyne miejsce w całym v1, gdzie błąd nie ma udokumentowanego kształtu.
Idempotentność
POST /v1/messages, POST /v1/calls, POST /v1/rings, POST /v1/voice-messages, POST /v1/relay, POST /v1/numbers, POST /v1/numbers/orders, POST /v1/sms-numbers, POST /v1/voice-sms-numbers, POST /v1/agents, POST /v1/tools, POST /v1/agents/{id}/draft/publish, POST /v1/agents/{id}/test-call, POST /v1/integrations, POST /v1/tasks, POST /v1/tasks/{id}/start, POST /v1/tasks/{id}/process, POST /v1/credit/topup, POST /v1/credit/auto-topup/setup, POST /v1/subscription/checkout, POST /v1/subscription/pay i POST /v1/subscription/card przyjmują nagłówek Idempotency-Key. Klucz jest globalny dla danego klucza API, niezależnie od metody HTTP, ścieżki i treści. Każdemu odrębnemu działaniu logicznemu, we wszystkich endpointach i treściach żądań, należy nadać nową wartość; tę samą wartość wolno użyć ponownie tylko przy ponawianiu dokładnie tego samego żądania logicznego, zwykle po przekroczeniu czasu, gdy nie wiadomo, czy pierwsza próba się powiodła. W ciągu 24 godzin otrzymają Państwo wtedy dokładnie tę samą odpowiedź co za pierwszym razem, w tym odpowiedź z błędem, jeśli pierwsza próba się nie powiodła, a nic nie zostanie wysłane ani wywołane dwukrotnie. Można użyć identyfikatora zamówienia z własnej aplikacji plus nazwy konkretnego działania. Równoległe żądanie z tym samym kluczem, które wciąż trwa (odpowiedź nie jest jeszcze zapisana), zwraca 409 idempotency_in_progress - prosimy spróbować za chwilę.
Dla POST /v1/numbers/orders klucz jest trwale powiązany z zamówieniem, zanim zostanie wywołany operator; ponowne użycie go z innym adresem zwraca 409 idempotency_conflict. Jeśli po zakupie operator da niejednoznaczną odpowiedź, zamówienie może wymagać przeglądu statusu.
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: objednavka-4471" \
-d '{"to": "+420777123456", "body": "Dziękujemy za zamówienie."}'Limity zapytań
60 żądań na minutę na klucz API (MCP korzysta z tego samego limitu - używa tego samego klucza). Przekroczenie zwraca 429 i nagłówek Retry-After z liczbą sekund do następnej próby.
POST /v1/messages ma ponadto własny dodatkowy limit: najwyżej jeden SMS na 2 sekundy i 100 SMS na konto dziennie. Wysyłka z numeru komórkowego (fromNumber) ma dalszy dzienny limit na konto: 20 wiadomości w pierwszych 30 dniach po zakupie numeru, potem 50. GET /v1/sms-numbers pokazuje, ile zostało i kiedy limit się resetuje (dailySendRemaining, dailySendResetsAt).
Połączenia wychodzące (POST /v1/calls, relay i połączenia testowe) mają wspólny własny limit: 10 połączeń na minutę i 1000 na dobę na konto, przy czym jednocześnie mogą trwać najwyżej 2 połączenia wbudowanego agenta.
Ile z limitu 60/min Państwu zostało, widać jeszcze przed osiągnięciem 429 - w nagłówkach RateLimit-Limit, RateLimit-Remaining i RateLimit-Reset, które zawiera każda odpowiedź (zob. sekcja Format błędu > Nagłówki wyżej).
Stronicowanie
GET /v1/messages i GET /v1/calls przyjmują limit (maksymalna liczba zwracanych rekordów - domyślnie 50, ograniczona do 200 nawet przy większym żądaniu) oraz before (znacznik czasu w milisekundach - zwraca tylko starsze rekordy).
hasMore i nextBefore
Obie odpowiedzi zawierają też hasMore (wartość logiczna - istnieje starsza strona) i nextBefore (znacznik czasu ostatniego zwróconego rekordu albo null, gdy hasMore ma wartość false). Na następną stronę należy wysłać nextBefore z poprzedniej odpowiedzi jako nowe before - bez ręcznego liczenia offsetu.
before jest WYŁĄCZNE (zwraca tylko rekordy ściśle starsze, nigdy równie stare) - ostatni rekord poprzedniej strony nigdy nie powtarza się na następnej. W rzadkim przypadku, gdy dwa rekordy mają dokładnie ten sam znacznik czasu w milisekundach na granicy stron, drugi może wypaść. Należy deduplikować po id - nie wolno zakładać, że strony nigdy niczego nie pomijają.
GET /v1/calls?direction=in lub ?direction=out przeszukuje historię partiami z twardym limitem liczby partii - hasMore: true może więc oznaczać albo "na pewno istnieje kolejna strona", albo "przeszukano partie do limitu i nie dotarliśmy jeszcze do końca historii". false zawsze oznacza, że historia w tym kierunku naprawdę się skończyła.
Wersjonowanie
Aktualna wersja publicznego interfejsu to 1.57.0 (GET /openapi.json, pole info.version - zob. akapit o maszynowym opisie API we wstępie powyżej). Zmianą wersji rządzą trzy zasady:
- W obrębie
v1dodawane są wyłącznie pola OPCJONALNE i nowe endpointy - istniejące pola nigdy nie znikają, nie zmieniają typu ani znaczenia. Integracja, która odczytuje tylko znane sobie pola, nigdy nie zostanie zepsuta przez aktualizację. Jedynym wyjątkiem była wersja 1.5.1, która przestała zwracaćansweredBydla połączeń PRZYCHODZĄCYCH - pole nigdy nie miało tam sensownej wartości (zob. Dziennik zmian poniżej). - Pole wymagane albo zmiana znaczenia istniejącego pola trafia zawsze wyłącznie w nowej wersji głównej (
v2) - nigdy jako cicha zmiana wv1. - W przypadku wersji roboczych agenta (zob. Wersje robocze agenta powyżej) zawsze należy odsyłać dokładnie ten obiekt, który odczytano z
GET .../draft- treśćPUT/POSTjest.strict(), więc nieznany klucz (powiedzmy z nowszej wersji, o której integracja jeszcze nie wie) zwraca 400validationzamiast zostać po cichu odrzucony.
Dziennik zmian
- 5 Nov 2026 (1.57.0): The agent
languageaccepts 20 values:cs,sk,en,de,pl,nl,pt,sl,ro,da,et,lv,bg,fi,el,fr,it,hu,lt,sv. A number in another country can be routed to an ElevenLabs agent; the errorel_outbound_trunk_not_configured(HTTP 503, causeservice) means our outbound trunk is missing. REST and MCP messages stay in English. - 4 Nov 2026 (1.56.0):
GET /v1/voice-sms-numbers/countriesand the MCP toollist_voice_sms_countriesreturnmonthlyFeeHalfor every country (hellers per 30 days excluding VAT).POST /v1/voice-sms-numberswithcountryorders the number at that country's fee from the offer; no fee confirmation is added. The MCP toolorder_voice_sms_numberrequiresconfirmedMonthlyFeeHalequal to the fee of the chosen country; any other value is avalidationerror and nothing is ordered, while the Prague number still confirms 4900. A number record carries themonthlyFeeHalof its country and the same amount is charged by the first fee and every renewal. A country without a set fee does not appear in the offer. - 4 Nov 2026 (1.55.0):
POST /v1/voice-sms-numbersaccepts an optionalcountry(ISO 3166-1 alpha-2, uppercase); without it the Prague number is ordered as before. The countries the account may order are returned byGET /v1/voice-sms-numbers/countriesand the MCP toollist_voice_sms_countries; a country outside that list is rejected withcountry_not_offeredand nothing is charged. Number records carrycountryandsms, and a number outside Czechia rejects sending SMS. Outbound calls go only to Czechia, and to the country of the number only where thecountry_and_czzone is enabled; the gate rejects any other destination. At most 2 numbers per country. - 4 Nov 2026 (1.54.0):
regionsinGET /v1/numbers/available, theregionparameter ofPOST /v1/numbersand thesearch_available_numbersandbuy_numbertools now knowjihocesky,karlovarsky,kralovehradecky,liberecky,moravskoslezsky,olomoucky,pardubicky,plzensky,stredocesky,ustecky,vysocina,zlinsky. Regional stock is small, sopool_emptyis more common there: usejoin_number_waitlistororder_number_from_region. - 4 Nov 2026 (1.53.0): The offer catalogue now knows all 14 regions. A regional offer (
jihocesky,karlovarsky,kralovehradecky,liberecky,moravskoslezsky,olomoucky,pardubicky,plzensky,stredocesky,ustecky,vysocina,zlinsky) appears inregionsofGET /v1/numbers/availableand in the MCP descriptions only once it is on sale; until then it is rejected as an unknownregion. TheregionLabeltext changes: a number from a region carries the English region name, such asMoravian-Silesian Region. - 4 Nov 2026 (1.52.1): A header value with three asterisks in a row (a leftover of the mask, such as
Bear***abc) is rejected byPUT /v1/webhookandset_webhookwithinvalid_webhook_headers; previously only the whole mask was. The descriptions ofget_webhook,send_test_webhookand the MCP server instructions mentionheaders, and the docs now say the test event carries custom headers. Value limits (1 to 1000 printable ASCII characters) added to OpenAPI and the webhooks page. - 3 Nov 2026 (1.52.0):
PUT /v1/webhookand MCPset_webhookaccept optionalheaders(at most 5 headers, values up to 1000 characters, printable ASCII only).GET/PUT /v1/webhookandget_webhook/set_webhookreturn a newheadersfield with masked values (Bear***,{}when none are set). Omittingheaderskeeps the stored ones,{}removes them, and a value equal to the mask keeps the stored one. Changing the URL to a different domain drops stored headers, and a mask is rejected there.host,content-type,content-length,volai-signatureand hop-by-hop headers cannot be set. Headers go with every event includingwebhook.testand are not sent when a delivery is redirected to a different domain. New errorinvalid_webhook_headers(400). - 3 Nov 2026 (1.51.0): REST
POST /v1/voice-messageswith the body{to, text, repeat?}(text1 to 500 characters,repeat1 to 3, default 1; an unknown field includingringingTimeoutSecsis rejected withvalidation) returns{id, status, from}and accepts anIdempotency-Keyheader likePOST /v1/calls. The MCP toolsend_voice_messagecalls the same service. The result is inGET /v1/calls/{id}askind: "voice_message"with avoiceMessageblock (text,repeat,repeatPlayed,completed) and in thecall.completedorcall.no_answerwebhook; the call summary e-mail is not sent for a voice message. New errorsvoice_messages_disabled(503, the feature is switched off),voice_message_unavailable(503, the voice engine cannot accept messages) andemail_unverified(403). - 1 Nov 2026 (1.50.0): REST
GET,PUTandDELETE /v1/account/spend-cap(response{spendCap: {enabled, capHal, warnShare, inboundAboveCap, month, monthSpendHal, remainingHal, reached, updatedAt}};PUTtakescapHalin whole hellers excluding VAT from 5,000 to 100,000,000,warnSharefrom 0.5 to 0.99 with a default of 0.8, andinboundAboveCap, which is not overwritten on a change when the field is omitted) and MCPget_spend_cap,set_spend_capandclear_spend_cap, all overlib/service/spend-cap.tslike the Monthly spending cap section under Credit. Once the cap is reached,POST /v1/calls,/v1/messages,/v1/rings,/v1/relay, the test call and task dialling are refused with the new errorspend_cap_reached(HTTP 402, nothing is charged); an inbound agent call withoutinboundAboveCap: trueends after one sentence and the call carriesendReason: spend_cap. The month's spend (Europe/Prague) is counted on every account, also without a cap, and includes the part covered by the monthly plan credit. Number fees, the Done for you round and the monthly plan are not counted; the cap does not stop calls already running or CDRs booked after midnight, forwarding without an agent, or outbound calls from a number in Own SIP PBX mode withinboundAboveCap: true. Warning and reached e-mails go at most once a month and are released again when the cap changes. RESTPOST /v1/agents/{id}/draft/knowledge-importwith a body of{url}or{text, filename}(a.txtor.mdfile up to 1 MB; the portal takes a file up to 800 kB), optionallymode(appendby default, orreplace),preview(only returns the result, writes nothing) andexpectedRevision. The MCP toolimport_agent_knowledge(id,urlortext+filename, the same optional fields) calls the same service,lib/service/knowledge-import.ts. The text is written intoknowledgeof the DRAFT and the source into the newknowledgeSourcesfield (at most 20 sources:urlorfilename,importedAt,chars), which the agent shape now also returns (GET /v1/agents/{id}, list and detail). What does not fit into 20,000 characters is summarized by a model (method: summarized) or cut (truncated). Errors:invalid_import_url,private_address,invalid_knowledge,import_too_large,revision_conflict,rate_limited(10 imports per hour per account). A fetched page is third-party text: the preview and the misuse check when the draft is saved guard against it, and REST saves it right away withoutpreview. RESTPOST /v1/account/cancellation-requestwith a body of{reason, note?}(reasonis one oftoo_expensive,callers_hang_up,missing_feature,not_needed,other;noteat most 500 characters) and the MCP toolrequest_account_cancellationcalllib/service/account-cancellation.ts, the same service as the form in Settings. Response{cancellationRequest: {reason, note, requestedAt, alreadyRequested}}; a repeated request returns the original record withalreadyRequested: true(HTTP 200) and sends no further e-mail. There is at most one request per account, it changes the account in no way and deletes nothing; the volai owner gets an alert with the reason and the customer a confirmation in the account language. Errors:validation,user_not_found. When you click thumbs down in the call e-mail, the rating is saved right away and on the page that opens you can pick a reason and add a note in a single form. Before, there were only five separate buttons and no note. When you write to podpora@volai.cz, you get an automatic confirmation that the message arrived, saying that we usually reply within one business day, with a link to the status page. At volai.cz/en/trust (in Czech volai.cz/duvera) you find in one place where the data of calls, SMS and your account lies, who touches it, how long we keep it, how we protect it and what we cannot do yet. The website gains a Call from your browser button that lets you try the volai demo agent with a call right from your browser, without signing up. The call is a demo, it is recorded and the agent speaks Czech. We are switching it on gradually, so it may appear only after a while.public/llms.txt,public/SKILL.md,/docs/apiand/docs/mcp(cs and en) match the code:create_calendar_eventcreates an appointment only withconfirm: trueand the calendar deletes nothing; the number for calls and SMS is in pilot mode (voiceSmsNumbersMode()isadmin), other accounts getvoice_sms_numbers_unavailable(until 3 Nov 2026; since then the mode isonand any account orders the number itself). The trial call'sremainingLinestates that an unanswered call also counts towards the limit of 3 calls per 24 hours. - 1 Nov 2026 (1.49.1):
CAMPAIGN_QUOTA_EXPIRES_ATinlib/service/call-quotas.tsis2026-11-15T23:00:00+01:00(22:00 UTC; previously2026-11-14T23:00:00+01:00) andPETCENTER_REMINDER_DISPATCH_ENDstays2026-11-21T19:55:00+01:00(18:55 UTC).petcenterCampaignQuotaActive()and therunPetcenterOutboundTaskTicktick therefore dial the main campaign until 15 November 22:00 UTC andcoupon_remindertasks still until 21 November 18:55 UTC. The stored quota of the campaign agent (call-quota:agent) has its ownvalidUntil, which is moved separately viaPUT /api/admin/call-quotasfrom2026-11-14T22:00:00Zto2026-11-15T22:00:00Z. The other conditions of the exception (theVOLAI_PETCENTER_CALL_QUOTA_ENABLEDswitch, account, agent, outbound task, eight relay names) are unchanged. - 30 Oct 2026 (1.49.0):
POST /v1/tools,PATCH /v1/tools/{id}and MCPcreate_toolandupdate_toolaccept an optionalrequireConfirmationfield (boolean, defaultfalse; any other type is400 validation). Responses that carry a tool (GET /v1/tools,GET /v1/tools/{id},POST,PATCH, MCPlist_tools) always return it as a boolean;POST /v1/tools/testdoes not have it. APATCHwithout the field keeps the stored value (neither an older client nor a stale portal page switches the gate off),falseswitches it off. The engine receivesrequire_confirmation: trueonly for a tool with the gate on and only for an agent on the engine incsorsk;falseis never sent explicitly. An agent inen,deorpland an ElevenLabs agent ignore the field, and saving the tool is not refused because of language, since a tool belongs to the account and is shared by several agents. Editing a tool does not rewrite agents on the engine: the flag reaches them at the agent's next save (PATCH /v1/agents/{id}) or publish (POST /v1/agents/{id}/draft/publish). With a running campaign, re-publishing an unchanged tested draft keeps the tested mark and the task carries on, now with a gate the test did not cover; a directPATCH /v1/agents/{id}clears the mark and the task stops withagent_version_changed. APATCHthat changes onlyrequireConfirmationdoes not call ElevenLabs and does not re-verify the tool address (no DNS lookup), so the gate can be switched even while the tool's domain is unreachable. The gate reads only the caller's last utterance and does not know the agent's question: it catches an unclear consent, not every “yes” given to a different question. Portal: the tool editor (/en/agent/tools/newand/en/agent/tools/[id]) stores the choice in the same field with the switch Require the caller's clear consent; a hidden form flag distinguishes an unchecked switch from a form that did not know the switch, so a stale page cannot silently switch the gate off, and after saving a changed gate the form says that for a running agent it takes effect only after that agent is published. The method, the parameter type and source are submitted from form state and the description is a controlled field, because React 19 resets the form after every form action and the second save after an error used to send the first option of a select and the old description.GET /v1/subscriptionand MCPget_subscription:availablesays whether a NEW plan can be started right now, and it istrueonly when the offer is on for the account and the account has no running plan (active,past_due); otherwise it isfalse. It used to betruefor a subscriber too, althoughPOST /v1/subscription/checkoutreturned409 subscription_exists. It istrueagain once the plan has ended (canceled). A client that usedavailableto tell whether the account has a plan should readsubscriptioninstead.recommendationdoes not depend onavailableand a subscriber still receives it.PATCH /v1/subscriptionand MCPupdate_subscription: a tier the account is already switching to (nextTier) counts as done (200, not400 invalid_tier), so the same body can be sent again after an outage in the middle and finishes what is left. Exceptions: the current tier with no scheduled change staysinvalid_tier; going back to the current tier (it cancels the scheduled change) does not count as done on a repeat and ends withinvalid_tier; on a plan set to end atieralone still returnsinvalid_state(withcancelAtPeriodEnd: truein the body it is a repeat and goes through).invalid_stateis now also listed among the errors ofPATCH /v1/subscriptionin OpenAPI and in the documentation.GET /v1/balanceand MCPget_balance: the noticessubscription_exhausted_topup,subscription_cap_reachedandsubscription_runwaycarry a new optional fieldcancelAtPeriodEnd: true(present only when true: the plan does not renew,renewsAtis the end of the plan and no new monthly credit arrives). The format ofrenewsAt(epoch ms) does not change and is documented as the same moment assubscription.periodEnd(ISO 8601). The e-mails about a failed renewal and about the end of a plan have their own wording when the account has no saved card (the card is missing, nothing was charged). An outbound agent call on the engine that stayedringingorinitiatedwithout aconversationIdand was closed asfailedwith price 0 by the watchdog (or by the taskreconcile) after two hours now behaves in a task likerelay_not_routed: the attempt hasitems[].attempts[].countsAsAttempt: falseanderrorCode: "no_provider_report", uses up neithermaxAttemptsnor thelorela_enterpriseschedule, the budget reservation is released, nothing is charged, and the contact getsnextAttemptAtno sooner than 60 seconds later.relay_not_routedandno_provider_reportattempts are added together in one series: after the third uncounted attempt in a row the contact goes toreviewwith thereviewReasonof the last one, that iscall:no_provider_report; earlier only when the next attempt does not fit beforeexpiresAtor thelorela_enterprisepolicy allows no further attempt. A call that a late post-call completed with aconversationIdwas seen by the engine, so it counts as before. Items that an earlier call of this kind had already sent to review (call:failed, orcall:unknownwith the policy) are not reopened automatically.errorCodeandreviewReasonare free strings, only a new value was added; an integration that reads reports from tasks should expect one more uncounted attempt and one more review reason. A pause (POST .../pause, the batch pause for a campaign) between the dial intent being written and the provider request used to leavereviewReason: "dispatch_in_flight", the active claim, the reservation and anin_progressitem with no call and no attempt;startthen returnedtask_needs_attention,resolvereturneditem_not_resolvable,DELETEreturnedtask_not_deletableandreconcilechanged nothing. After such a pause the task processing now releases the intent, the claim and the reservation itself and returnstask_paused(no call was placed): the contact ispendingagain (orfailedwith a free attempt) and the task ispausedwithout areviewReason, so the firststartsucceeds. The cleanup is retried up to three times on a concurrent write (revision_conflict), as is the cleanup at the last gate before the provider, whose result used to be discarded.reconcile(the cron,POST /v1/tasks/{id}/reconcile, MCPreconcile_task) additionally releases an intent that has no call record and no attempt result and whose claim and five-minute margin after it have both expired, that is at the earliest 15 minutes after the dial began (the claim lives for 10 minutes): it returns the reservation and puts the contact back to waiting, or completes it after a verified callback. A task that the expired claim had meanwhile stopped (dispatch_claim_expired) endspausedwithout a reason and has to be started again (start); a task the claim did not stop keeps running. An uncertain dial and a call that may have been created are left as they are. Tasks that got stuck earlier are repaired by a singlereconcileand end aspausedwithout a reason (the pause was intentional), they do not restart on their own. Response shapes do not change.POST /v1/dncand MCPadd_to_dncaccept eithere164(one number, the response{ "added": true, "e164": ... }is unchanged) or the new fielde164s(1 to 500 numbers, written with a singleSADDcommand). Both fields together, neither, an empty array and more than 500 entries are400 validation; in OpenAPI both fields are optional and the rule "exactly one of them" is enforced by validation only. An entry ofe164smust be a string of at most 256 characters, any other entry fails the whole request with400. The response toe164scarriesaddedCount,alreadyOnListCount,rejectedCountandrejected(entries{ index, input, reason: "invalid_number" }, whereinputis the entry as sent, cut to 32 characters).addedCount + alreadyOnListCount + rejectedCountalways equals the number of entries sent; a number repeated in the request or written in another format counts as already on the list. An entry that is not a valid number (including longer text, for example two numbers in one cell) does not fail the request, the valid entries are always added, and an input of only invalid entries is still200, so checkrejectedCountafter every response. The request is idempotent (repeating it returnsaddedCount: 0). In OpenAPI the response ofPOST /v1/dncis ananyOfof both shapes and the booleanaddedstays only in the single-number response. Numbers are still removed one at a time (DELETE /v1/dnc/{e164}). The destination check at dial time reads oneSISMEMBERinstead of the whole set, so a list with thousands of numbers no longer transfers tens of kB on every dial; the listingsGET /v1/dncandlist_dncare unchanged. RESTPOST /v1/tasks/{id}/items/{itemId}/resolveand MCPresolve_task_itemacceptreasonwith the valuecustomer_purchasedor, new,unsubscribedon the audited skip (action: "skip"withoperationIdandevidenceRef).evidenceRefis an opaque reference to the purchase or unsubscribe record, never a phone number or e-mail. Conditions are unchanged: adrafttask (contactpending) or apausedtask (contactpendingorfailed), no active claim, dial intent or reservation, and the skip does not resolve contacts inreview. Attempts, costs and the do-not-call list stay as they are. The reason is part of the fingerprint (resolution.fingerprint), so repeating the same operation writes nothing and the sameoperationIdwith a differentreasonorevidenceRefreturns409 revision_conflict.items[].resolution.reasonin every response that carries a task (GET /v1/tasks,GET /v1/tasks/{id},POST .../resolve, MCPlist_tasks,get_task,resolve_task_item) is now the enumcustomer_purchased|unsubscribed(a constant in OpenAPI before): a client that validates responses against the old specification must allowunsubscribed. An unknownreasonin the request returns400 validation. Descriptions in OpenAPI, MCP and the API, MCP and Tasks documentation are updated. - 26 Oct 2026 (1.48.0): REST:
GET /v1/voice-sms-numbers,POST /v1/voice-sms-numbers(no body,Idempotency-Keysupported, returns 202 with the number in theawaiting_registrationstate),GET /v1/voice-sms-numbers/{e164},PATCH /v1/voice-sms-numbers/{e164}(routing:agenton the in-house engine, ornone),POST /v1/voice-sms-numbers/{e164}/cancel(cancelling before the registration is approved is free) andDELETE /v1/voice-sms-numbers/{e164}(releasing an active number is irreversible and the period already paid is not refunded; when the cancellation with the carrier fails, the number stays in thereleasingstate and the response is 502release_failed). MCP:list_voice_sms_numbers,order_voice_sms_number(confirmedMonthlyFeeHalmust be 39000, otherwise nothing is ordered),set_voice_sms_number_routingandcancel_voice_sms_number; releasing is deliberately not available over MCP. Number states:awaiting_registration,active,rejected,releasing. SMS from the number are sent withfromNumberinPOST /v1/messagesandsend_sms(+420 recipients only, nodeliveryStatus), received SMS are still read withGET /v1/messages?direction=inand themessage.receivedwebhook; calls from the number are placed by an agent (POST /v1/callswithagentId, to +420 only, one outbound call at a time); a bridge between two numbers withfromand a ring from it are not available (unsupported_for_number_type). New error codes:voice_sms_numbers_unavailable,voice_sms_number_limit(at most 2 numbers per account, ordering needs a first top-up),voice_sms_order_limit,voice_sms_number_out_of_stock,unsupported_for_number_type(forwardandsipare not supported for this number) anddestination_not_allowed(the number can call +420 numbers only). The fee of 390 CZK per 30 days is charged asnumber_fee; thenumber_feenotice inGET /v1/balanceand MCPget_balancecounts these numbers too, and a debt older than 7 days releases the number just like other numbers. - 26 Oct 2026 (1.47.0):
GET /v1/accountand MCPget_accountreturn a newtestCallsfield next toaccount, withmax(3),remaining(test calls left in the current window) andresetAt(when the window ends, epoch ms; the window runs 24 hours from the first attempt;nullwhen there was no attempt in the last 24 hours).POST /v1/agents/{id}/test-calland MCPtest_callcarry the same field right after dialing. Every dialed attempt counts, including an unanswered one, which is not charged; an attempt that never dialed (agent, credit or capacity error) does not count. The field is additive, existing response shapes do not change.PATCH /v1/accountdoes not return it. - 23 Oct 2026 (1.46.1):
POST /v1/tasks/{id}/reconcile, MCPreconcile_taskand the task cron settle a task after recovery by the work that is left:pausedwhile any contact is waiting for another attempt (pending,failed,in_progress), even if other contacts are inreview;needs_attentiononly when nothing is left to dial but a contact inreview(the task'sreviewReasonis the reason of the first such item);completedwhen nothing is left.reviewReasonis set only forneeds_attentionand empty otherwise; after a pause in the middle of a dial and after an expired claimactiveItemId,activeClaimTokenandactiveClaimUntilare released as well. It applies to four recoveries: after a pause in the middle of a dial (dispatch_in_flight), after an expired claim (dispatch_claim_expired), after a call with a previously unknown price is settled (billing_unknown) and after an uncertain dial is resolved (dial:*). Before, recovery put review before work, so pausing a running task sent a task with hundreds of waiting contacts toneeds_attention, whichstartandPATCH(task_not_editable) refused. An uncertain dial for which no call can be found keeps the task inneeds_attention(the contact holds an unresolved dial intent andstartwould refuse it) untilPOST .../items/{itemId}/resolvedecides that contact; the same applies after a call with an unknown price is settled (billing_unknown) while another contact holds an unresolved dial intent. A running task and task-level reasons (for examplevariables_limit) are not changed byreconcile. With theretryPolicy: "lorela_enterprise"policy the state of a contact after the last allowed attempt (maxAttempts) changes too: ano_answer,missed,busy, completed voicemail without a human or an answered call in which the person said nothing closes it asskippedwithreviewReason: "retry_policy_exhausted", likeretry_age_expiredwhen the horizon runs out (before:review); the link to the last call (providerCallId) stays. An uncertain result of the last attempt stays inreviewwith the same reason, and the policy still rejects a manualresolvewithretry. A verified human callback after this closing turns the contact intocompleted(afterretry_age_expiredandtask_expiredit staysskippedwithcallbackContactCallId). Tasks without the policy do not change.POST .../items/{itemId}/resolvedoes not change: the last resolved item in review turns a task with work left intopaused. Tasks that got stuck inneeds_attentionbefore the rollout do not repair themselves (their reason is derived from an item):resolvewithskipover the items in review frees them once. - 23 Oct 2026 (1.46.0):
POST /v1/tasks,PATCH /v1/tasks/{id}, MCPcreate_taskandupdate_taskaccept optionalretryMaxAgeDays: a whole number 1 to 30, the retry horizon of theretryPolicy: "lorela_enterprise"policy in days from the recipient's first counted attempt. The public task (GET /v1/tasks,GET /v1/tasks/{id}, the create and PATCH responses, MCPget_taskandlist_tasks) returns it only when it was set; an absent field means the default 5 days as before, so tasks without the field behave unchanged. The field needs the policy (in the same body at creation, in the body or already stored on the task on PATCH), otherwisevalidationis returned with the fieldretryMaxAgeDays; so is a value outside 1 to 30 or a non-integer. Omitting it on PATCH preserves the value. UnlikeretryPolicy, it can be changed after attempts have started, in the states PATCH has always accepted (runningandcompletedstill not:task_not_editable). Raising the value on a paused task returns items that areskippedwithreviewReason: "retry_age_expired"topendingwith a newnextAttemptAtwhen they have a counted attempt, a free attempt belowmaxAttempts, no verified callback and their first counted attempt + the new horizon (andexpiresAt, when set) is in the future; lowering it reopens nothing and contacts outside the new horizon stay skipped. The attempt ladder (8 minutes after the first, the 08:00-09:00 and 17:00-19:00 windows, four attempts) does not change, only the horizon does. Portal: the task detail of a task with the policy shows a line about the retry schedule and horizon, the calling rules have a Retry horizon (days) field and offer four attempts for a task with the policy. OpenAPI, the MCP tool descriptions, SKILL.md and the Tasks guide are updated. - 23 Oct 2026 (1.45.1):
CAMPAIGN_QUOTA_EXPIRES_ATinlib/service/call-quotas.tsis2026-11-14T23:00:00+01:00(22:00 UTC; previously2026-10-31T23:00:00Z) andPETCENTER_REMINDER_DISPATCH_ENDis2026-11-21T19:55:00+01:00(18:55 UTC; previously 5 November).petcenterCampaignQuotaActive()and therunPetcenterOutboundTaskTicktick therefore dial the main campaign until 14 November 22:00 UTC andcoupon_remindertasks until 21 November 18:55 UTC. The stored quota of the campaign agent (call-quota:agent) has its ownvalidUntiland is moved separately viaPUT /api/admin/call-quotas. The other conditions of the exception (theVOLAI_PETCENTER_CALL_QUOTA_ENABLEDswitch, account, agent, outbound task, eight relay names) are unchanged. - 23 Oct 2026 (1.45.0): The number's relay names are set up in the background right after purchase (
POST /v1/numbers,buy_number, portal) and after an order is fulfilled (POST /v1/numbers/orders,order_number_from_region), whatever the number's routing. A lease (POST /v1/relay,create_relay_lease), an agent call or a transfer from a number that does not have them yet triggers the setup again. Thecreate_relay_leasedescription spells out a rule that already applied: a lease connects exactly one call, dialing the samesipNameagain is rejected with SIP 603, so create a new lease before every attempt and turn off automatic redialing in your platform. Response shapes are unchanged. - 22 Oct 2026 (1.43.0): New agent field
recordingNotice(boolean) inPOST/PATCH /v1/agents, MCPcreate_agent/update_agentand agent drafts (PUT /v1/agents/{id}/draft,save_agent_draft). The response always carries it (falsewhen unset). The sentence is appended only withrecordingNotice: trueandrecordCallson; in a draft an absent key means unchanged. The greeting length estimate (first_message_too_long) counts the sentence only when it is on. This changes whatrecordCallsimplies: recording alone no longer adds the sentence. It was removed on 22 Oct 2026 from existing agents that had it added automatically; a sentence written intofirstMessageitself stays as it is. - 22 Oct 2026 (1.42.3): An attempt's reservation (
reservedBudgetHal, the item'sestimatedCostHal) is the call leg plus the agent leg, each rounded up to a heller exactly as billing does (previously a single rounding of the combined rate, which came out one heller lower for some caps). A call with a duration cap (outbound task) is billed formin(durationSecs, maxDurationSecs); the storeddurationSecsstays as measured. A transferred leg of such a call gets at most the remainder up to the attempt's highest price. An agent withrecordCalls: falseon volai's engine (provider: "engine") has no recording:hasRecordingisfalseandGET /v1/calls/{id}/recordingreturnsno_recording. Saving such an agent requires an engine that knows the flag; otherwise the engine write fails instead of recording silently. - 21 Oct 2026 (1.42.2):
checkCallRateLimit(POST /v1/calls, MCPmake_call, outbound tasks) counts the attempts per 24 hours of a call covered by an agent's stored quota (call-quota:agent:{agentId}) or by the PetCenter campaign exception with a daily limit above 1,000 in its own counterratelimit:callsday-agent:{userId}:{agentId}and compares them with the quota's daily limit. Calls with the default limits, calls without an agent (the website test call, bridge, relay) and calls with a quota that does not raise the daily limit (1,000 or less) still share the account counterratelimit:callsday:{userId}; a call of the same agent outside the quota's scope (outboundTaskOnly,taskPurposes, aftervalidUntil) follows the campaign exception when it applies, and otherwise uses the default limit of 1,000 and the account counter. The concurrency slotsagentcallslot:{userId}(relay_lease_limit) and the per-minute counterratelimit:calls:{userId}stay shared per account. The coderate_limited, HTTP status 429, the message texts and the default limits of 2 concurrent, 10 per minute and 1,000 per 24 hours do not change; only an admin stores a quota.resolveAgentCallLimitsWithSourceinlib/service/call-quotas.tsreturns the source of the limits (stored,campaign,default) next to them from a single quota read, andusesAgentDailyCounterpicks the counter from them. - 18 Oct 2026 (1.42.1): An inbound call that the voice engine turned away because the cap on concurrent calls was used up now has
status: "missed",endReason: "capacity",durationSecs: 0andpriceHal: 0in REST (GET /v1/calls,GET /v1/calls/{id}) and MCP (list_calls,get_call); no credit is charged. Thecall.missedwebhook is sent once per call, and an SMS to the caller only for an agent withmissedCallSmsturned on (as for every missed inbound call); no summary e-mail is sent for it. When the carrier reported the unanswered call first,call.missedis sent at that moment (possibly withoutendReason) and the existing record is then completed withendReason: "capacity"; no second record and no second webhook are created. Until now such a call stayed instatus: "in_progress"forever, because the engine sends no post-call for it. The record is now created by the reconciliation of engine calls, within a few minutes of the rejection. Existing stuck records from the last seven days that are older than two hours are closed retroactively by the hourly check, only with evidence from the engine's call log and without a webhook; if the carrier created its own record for them, they are merged into it. The valuesmissedandcapacityalready existed in the enumerations, and the response shapes do not change. In theoutcomefilter (GET /v1/calls,list_calls) such a call counts asno_answer; for an outbound call nothing changes aboutcapacity(relay pool full). - 18 Oct 2026 (1.42.0):
GET /v1/subscriptionand MCPget_subscription:recommendationhas a new fieldsteady, whether the spend is not just a one-off peak: usage over the last 14 Prague days scaled to 30 days, plus the monthly number fees of the 30-day window counted once, reaches at least the price of the recommended tier. The portal offers the plan personally and the MCP sentence suggests it only whenworthItandsteadyare both true;worthItand the other fields do not change. The recommendation comes from one function for the portal, REST and MCP; for REST and MCP it is cached next to the credit runway (10 minutes, cleared by every credit movement). Nothing else in the API changes. - 18 Oct 2026 (1.41.0):
GET /v1/subscriptionand MCPget_subscription: new fieldpreviousPeriod, a summary of the paid period before the current one (subscription.periodStart) with the same figures as the renewal e-mail:tier,periodStartandperiodEnd(ISO 8601),creditHal,usedHal,usedPercent(rounded down),overHal(spending beyond the monthly credit; with top-ups without debt that the next credit paid off, with the cap including debt;nullwhen the credit history cannot prove it),expiredHal,exhaustionModeandexhaustedAt(ISO 8601,nullwhen the credit did not run out or the history cannot show when). It is saved at every renewal, by card (also when thepayment_intent.succeededwebhook completes it) or by a manual payment, and kept for 45 days; it is returned only while the plan is active, whateveravailablesays, and isnullwithout a plan, with statuspast_dueorcanceled, in the first period and when the usage cannot be proven. MCPget_subscriptionalso adds a sentence about the last period with Prague dates.GET /v1/balanceand MCPget_balance: on an active plan thenotice{kind:"subscription_runway",renewsAt,runsOutAt}now computesrunsOutAtfrom the plan's cautious pace, the same as the outlook on the plan card in the portal: the highest of the 28-day average (with a complete credit history), the average usage over the last 7 Prague days without number fees and the average since the current period started (after its first week), instead of the 28-day average. The shape and conditions of the notice do not change (nothing while auto top-up is on) andrunway.dailyAverageHalandrunway.runwayDaysstay the 28-day average. The renewal e-mail for a renewal that the webhook completes now also leaves out the advice when a tier change is scheduled. Nothing else in the API changes. - 18 Oct 2026 (1.40.1):
POST /v1/outbound/dispatch-scopes/petcenter/registrationsdecoded the result of its Lua script withJSON.parseeven when the default Upstash client (redis()with automatic deserialization; onlyredisRaw()has none) already returns it as an object. Every request therefore ended with the exceptionSyntaxError: "[object Object]" is not valid JSONand a 500 response. The exception happened after the script had run and before the result was mapped, so a newly stored registration (or a replay of the same operation) showed up as a 500 to the client, and a Lua refusal never reached it as409 revision_conflict(operation_conflict,manifest_cas_conflict,task_changed_during_registrationwitherror.details),404 not_foundor400 validation.registerPetcenterDispatchScopenow decodes the result with the helperdecodeLuaJsonObject:JSON.parseonly over a real string, and an object passes unchanged. A script reply that is not an object (valid JSON of another shape, or text that is not JSON) throws and the route answers 500, because the script may already have written the change and the outcome is uncertain; the client then asks for the operation result withGET /v1/outbound/dispatch-scopes/petcenter/registrations?operationId=.... An empty result still ends inregistration_failed. The batch pause decodes its results with the same helper and its behavior and error message do not change. The contract does not change: request and response shapes, idempotency byoperationId(replayed: trueon a repeat),expectedManifestSha, the status codes and theproviderActionStartedflag. The only client-visible difference is that the former 500 responses are now 200, 409, 404 or 400 according to the script result; 500 remains only for an unexpected shape of its reply. - 18 Oct 2026 (1.40.0):
GET /v1/subscriptionand MCPget_subscription: new fieldrecommendation, ornull(no plan offer for the account, or less than 30 Prague days of credit history). It holdsspendHal(spend over the last 30 Prague days on calls, the agent, numbers and SMS, minus SMS refunds),days(30),cheapestPath(per_secondorplan),tierandtierTotalHal(the cheapest tier with top-ups and its cost at that spend),savingsHal,worthIt(a saving of at least 50 CZK and 5%, the same threshold as the portal) andoptions(every way to pay for the same spend:{path, tier, totalHal}for paying by the second and for each tier). For a subscriber MCPget_subscriptionalso adds a sentence with advice under the same rule as the portal and the e-mail. Nothing else in the API changes. - 18 Oct 2026 (1.39.1):
PUT /api/admin/call-quotasloads the agent (getAgent) and refuses the write when the agent does not exist or is deleted (404agent_not_found), or whenuserIdin the body is not the agent's owner (400validation, the message does not reveal the real owner). A refused write stores nothing incall-quota:agent:{agentId}, in the indexcall-quota:indexor inuser:{userId}:admin-log.userIdstays required in the body so the stored shape fromGETcan be sent back. Therelay_lease_limitmessage when placing an agent call (POST /v1/calls, MCPmake_call, outbound tasks) now readsThis agent can have at most 3 running calls at once, and that many are already running on your account. Try again in a moment.(Czech:Tento agent může mít najednou nejvýš 3 běžící hovory a tolik jich teď na tvém účtu už běží. Zkus to za chvíli znovu.; the Czech noun and adjective agree with the count: 1 readsběžící hovor, 2 to 4běžící hovory, 5 or moreběžících hovorů). The number is the threshold of the calling agent fromresolveAgentCallLimits(a stored quota, otherwise the default 2, after the pool clamp); the running calls come from the account-wide counteragentcallslot:{userId}, so a slot may be held by another agent of the same account. Before, the message said the maximum was per account although the agent decides the threshold, and the Czech text used the same plural form for every count. The coderelay_lease_limit, the HTTP status 400, the counter, the threshold and the way outbound tasks wait on this code do not change. The history table heading on the customer detail page in the admin is nowAdmin action history(Historie admin akcí), the rows do not change. - 18 Oct 2026 (1.39.0): New admin endpoint
GET/PUT/DELETE /api/admin/call-quotas(admin session, not an API key; writes check the Origin) stores an agent's quota incall-quota:agent:{agentId}:concurrent1 to 30,perMinute1 to 60,daily1 to 20,000, a mandatorynote,outboundTaskOnlyand the optionaltaskPurposes(standard,coupon_reminder) andvalidUntil(milliseconds since the epoch). Every change is written touser:{userId}:admin-logascall_quota_setorcall_quota_deleted.resolveAgentCallLimitsinlib/service/call-quotas.tsapplies it ahead of the PetCenter exception and the default{ concurrent: 2, daily: 1000, perMinute: 10 }, only for the same account, an outbound task (whenoutboundTaskOnlyis set), an allowed purpose and a time beforevalidUntil. Concurrency is clamped tomin(stored, max(2, number of distinct names in ODORIK_RELAY_NAMES - 2)), the per-minute and daily limits are not. Without a stored quota behaviour is unchanged. Placing a call on a relay name waits for its turn (one shared pointer in Redis, spacingRELAY_PLACEMENT_GAP_MS3 s, at mostRELAY_PLACEMENT_MAX_WAIT_MS10 s; on a Redis outage the call is placed at once andrelay placement spacing unavailableis logged): agent calls (POST /v1/calls, MCP, tasks) and trial calls on the engine and on ElevenLabs, the transfer bridge claim for at most 4 s; not the BYO relay lease (POST /v1/relay) and not callback calls without a relay name.POST /v1/callsand a trial call can therefore take up to 10 s longer during a burst of calls;maxDurationis 100 s forPOST /v1/callsand 70 s forPOST /v1/agents/{id}/test-call. The outbound task tick starts no further task afterTICK_DISPATCH_BUDGET_MS(90 s), the rest waits for the next run. An engine rejection because of the concurrent-call cap carriesRetry-After: 30, which volai already forwards (retryAfterSecs, theRetry-Afterheader,items[].nextAttemptAt): once the engine with this cap is deployed, contacts wait 30 s instead of 5 minutes; an engine without the header still means 300 s. - 18 Oct 2026 (1.38.3): No change in API behaviour. The plan offer is on for every account (
PAUSAL_MODE=on), soGET /v1/subscriptionreturnsavailable: true;pausal_unavailable(404) remains for when the offer is switched off. The documentation (/en/docs/credit, SKILL.md, llms.txt) no longer says the plan is limited to some accounts and describes renewals as hourly. - 18 Oct 2026 (1.38.2):
agentCallLimitsinlib/service/call-quotas.tskeeps returning{ concurrent: 6, daily: 6000, perMinute: 15 }afterCAMPAIGN_QUOTA_EXPIRES_AT(2026-10-31T23:00:00Z) when the quota context carries the new optionaltaskPurpose: "coupon_reminder"andpetcenterReminderQuotaActive()holds, that is, theVOLAI_PETCENTER_CALL_QUOTA_ENABLEDswitch is on and the time is beforePETCENTER_REMINDER_DISPATCH_END(2026-11-05T19:55:00+01:00, 18:55 UTC; the constant moved intocall-quotas.tsand the dispatch tick reads it from there).makeAgentCallpasses the purpose to the quota only after it has verified the link to an approved reminder. A call without a purpose gets the default{ concurrent: 2, daily: 1000, perMinute: 10 }after 31 October. The other conditions (the exact account, agent and internal outbound-task context match and at least eight distinctODORIK_RELAY_NAMES) apply to reminders as well; the main campaign still dials nothing after 31 October,petcenterCampaignQuotaActiveand the tick logic are unchanged. API behaviour for other accounts is unchanged. - 18 Oct 2026 (1.38.1):
GET /v1/balanceand MCPget_balance: an eighthnoticeshape{kind:"subscription_runway",renewsAt,runsOutAt}(at the recent pace the balance runs out before the renewal; returned to subscribers instead of{kind:"runway"}, nothing while auto top-up is on).POST /v1/subscription/checkoutand MCPcreate_subscription_link:consumerConsentis ignored for an account with a company ID, VAT ID or company name (Stripe metadata get0); a consumer without a company ID still gets aconfirmUrl. Nothing else in the API changes. - 17 Oct 2026 (1.38.0): New REST endpoints:
GET /v1/subscription(response{subscription, tiers, topupBalanceHal, available};subscriptionisnullwithout a plan,tierslists{id, priceHal, creditHal}in hellers excluding VAT,availablesays whether a plan can be started on the account),POST /v1/subscription/checkout(body{tier, exhaustionMode, consumerConsent?}wheretieris"490","990"or"1990"andexhaustionModeistopuporcapwith no default; returns{checkoutUrl}, nothing is charged until a person pays in Checkout; an account without a company ID must sendconsumerConsent),PATCH /v1/subscription(body{tier?, cancelAtPeriodEnd?, exhaustionMode?}),POST /v1/subscription/pay(a Checkout link to pay a failed renewal by hand, only in statuspast_due, otherwiseinvalid_state) andPOST /v1/subscription/card(a Checkout link in setup mode to replace the saved card, nothing is charged); the POST endpoints acceptIdempotency-Key.PATCHapplies a lower tier (from the next period),cancelAtPeriodEnd: trueandexhaustionMode: "cap"at once and returns{subscription}; a higher tier,cancelAtPeriodEnd: false(resuming a plan set to end) andexhaustionMode: "topup"change nothing and return 409subscription_confirmation_requiredwith aconfirmUrl(single use, valid for 24 hours) that a person must open and confirm in the portal. Thesubscriptionobject carriestier,status(active,past_dueorcanceled),periodStartandperiodEndas ISO 8601 strings (unliketsin the ledger, which is epoch milliseconds),monthlyCreditHal,monthlyRemainingHal,exhaustionMode,cancelAtPeriodEnd,nextTierandcard(brand,last4).GET /v1/balanceand the MCP toolget_balancenow also carrysubscription(nullwithout a plan) andtopupBalanceHal(top-up credit alone, negative when the account is in debt);balanceHalis still the total of both. The ledger (GET /v1/credit/ledger, MCPlist_ledger) has two newtypevalues,subscription_grantandsubscription_expire; a ledger entry for a charge covered by the monthly credit also carriesfromSubscriptionHal(the part the monthly credit paid), other entries do not have the field. Five new MCP tools:get_subscription,create_subscription_link,update_subscription,cancel_subscriptionandcreate_subscription_payment_link; replacing the card stays REST and portal only. New error codes: 402subscription_cap_reached(returned instead ofinsufficient_creditby calls, test calls, gate rings, SMS and purchases of numbers, SMS numbers and relay leases when the plan runs withexhaustionMode: "cap", the monthly credit is used up and the remaining credit does not cover the action), 409subscription_exists, 404subscription_missing, 409subscription_canceled, 409subscription_confirmation_required, 400consumer_consent_required, 404pausal_unavailable(the offer is not enabled for the account), 400invalid_tierand 409subscription_cap_active(auto top-up cannot be turned on while the plan runs with the cap); the confirmation page in the portal (not REST) can also end in the stateconfirm_invalid,confirm_expiredorconfirm_used. Once the offer is on for an account, the portal shows a plan card on the Credit page and pages to start or change the plan and to confirm aconfirmUrllink. Accounts without a plan see no change in existing fields, statuses or codes. - 16 Oct 2026 (1.37.5): The engine sends
rationaleandreasonasnullin every item ofanalysis.evaluation_criteria_resultswhen they have no value. The post-call webhook schema rejectednulland dropped the evaluation of all criteria, sogoalinGET /v1/calls,GET /v1/calls/{id}and in the MCP toolslist_callsandget_callwasnullfor calls on the engine, thegoalfilter counted them asnot_evaluatedand the after-call e-mail had no goal line. The schema now treatsnullas a missing field for every optional field wherenullmeans nothing, and a malformed evaluation item drops only itself. Wherenullcarries meaning (for examplerecording_url: null= a call without a recording), it stays. No field or endpoint changes. Calls from 16 September to 16 October 2026 do not have an evaluation yet. - 15 Oct 2026 (1.37.4): Three new agent fields
turnDigitTailFloorMs,turnLongSpeechFloorMs(integer,0= off, otherwise 400 to 2300 ms; the gap 1 to 399 returnsvalidation) andturnLongSpeechSecs(1 to 30, decimals allowed) onPOST /v1/agents,PATCH /v1/agents/{id}(a number sets it,nullremoves the stored value, an omitted key leaves it unchanged),GET /v1/agentsandGET /v1/agents/{id}(always present, a number ornullwhen not set, the engine uses its defaults 0 / 0 / 5), in the draft snapshot (PUT/GET /v1/agents/{id}/draft; an absent key means unchanged,nullremoves it) and in the MCP toolscreate_agent/update_agent/save_agent_draft. They are sent to the engine in theturnblock asdigit_tail_floor_ms,long_speech_floor_msandlong_speech_secson EVERY save of the agent (the engine does not carry the fields over from the previous version; a missing value goes as the engine default) and the read-back compares them (engine contract ch. 3.5af and 3.5ag). Stored for either provider, read only on the engine in smart_turn mode; ElevenLabs ignores them. Engine guidance: step 1 = 800 for Czech agents that take dictated numbers, step 2 = 1200, practical ceiling 1500; dates, postcodes and years get the floor too. The counters of applied floors stay in the engine (its dump andGET /v1/calls), they are not in volai webhooks. In the portal, the "End of reply" card in the Advanced tab. - 15 Oct 2026 (1.37.3): New agent field
sttLanguageHintsStrict: booleanonPOST /v1/agents,PATCH /v1/agents/{id}(trueturns it on,falseornullturns it off, an omitted key leaves it unchanged),GET /v1/agentsandGET /v1/agents/{id}(always a boolean,falsewhen not set), in the draft snapshot (PUT/GET /v1/agents/{id}/draft; an absent key means unchanged,nullturns it off) and in the MCP toolscreate_agent/update_agent/save_agent_draft. It is sent to the engine asstt.language_hints_stricton EVERY save of the agent (the engine does not carry the field over from the previous version) and the read-back compares it. Stored for either provider, read only by the engine (Soniox); best effort, only for engine agents with languagecsand outbound calls, never forsk. The call detailGET /v1/calls/{id}, MCPget_calland thecall.completedwebhook (andcall.failedwithendReason: "engine_error") carry a newengine: { protectedUtterances: [{ timeInCallSecs?, atSecs, message, words, reason }], protectedUtterancesCount }block - utterances of the person who answered that the engine dropped during the greeting window (metadata.engine.protected_utterancesfrom the engine post-call, contract ch. 3.5ae): not in the transcript, the agent did not react to them;reasonisfirst_message_protect,greeting_courtesyorgreeting_identification(open list), at most 10 items,messageup to 200 characters, the count covers them all. The block is present only on engine calls from the 15 Oct 2026 build on ([]and0when nothing was dropped); older calls, theGET /v1/callslist and agents on ElevenLabs do not have it. In the portal a switch on the Advanced tab and a "What the person said during the greeting" card in the call detail. - 15 Oct 2026 (1.37.2): cdr-sync: the second leg of an answered ring (same
from/to, differentactive_call_id, starting shortly after the first leg, first leganswered) is matched to the samekind: "ring"record without the per-attempt fee; seconds are billed only whenlength > 0. Thering.rangSecsdescription in OpenAPI and the docs: the carrier's figure is usually 1 to 3 s aboveringSecs. No field or endpoint changes. - 15 Oct 2026 (1.37.1): New optional agent field
inboundVariables: Record<string, string>onPOST /v1/agents,PATCH /v1/agents/{id},GET /v1/agentsandGET /v1/agents/{id}, in the draft snapshot (PUT /v1/agents/{id}/draft,GET .../draft) and in the MCP toolscreate_agent/update_agent/save_agent_draft: default values for{{variable}}placeholders on INBOUND calls. The initiation webhook merges them intodynamic_variablesonly for names the call does not supply itself (caller_number,called_number,attempt_id,volai_*,trial_*andsystem__*take precedence and validation rejects such names); outbound calls (POST /v1/calls, relay, tasks) do not use them. At most 10 keys matching[A-Za-z0-9_]{1,40}, values are trimmed strings up to 200 characters. OnPATCHan object replaces the stored values entirely,nullor{}removes them and an omitted key leaves them unchanged; the response omits the key when the agent has none. In a draft an absent key means unchanged. Theprompt_variables_unavailable_inboundandfirst_message_emptywarnings skip a variable that has a default. Works for either provider - the engine and ElevenLabs read the same webhook. - 15 Oct 2026 (1.37.0): New
POST /v1/ringswith body{to, from?, ringSecs?}(from= your active volai number, defaults to the first number of the account;ringSecs3 to 30, default 10) replies right after the carrier accepts the order with{id, status: "initiated", kind: "ring", from, to, requestedSecs, priceHal}. The result arrives by webhook (call.no_answerforrangandended_early,call.completedforanswered) and inGET /v1/calls/{id}(also with?waitSecs=) in the newring: {requestedSecs, outcome, rangSecs}block;outcomeisrang,ended_early,answeredorunknown.kindhas the new valuering; the body of every call webhook now carrieskindand, for rings, theringblock. MCP toolring_numbertakes the same fields. Limits: 30 rings per minute and 500 per day per account, 120 per hour per target; the manual do-not-call list applies, the automatic 30-day block is neither applied nor fed.Idempotency-Keyis supported. New error code:ring_needs_number(account without an active number). - 13 Oct 2026 (1.36.2):
PATCH /v1/numbers/{e164}and MCPset_number_routingreturnvalidationwhenrouting.modeissip,sipUripoints at<credentialId>.sip.vapi.aior<credentialId>.sip.eu.vapi.ai, andsipUsernamewithsipPasswordare sent or stored from before. A Vapi BYO trunk recognises our calls by the IP in its gateways (IP81.31.45.0, netmask24) and does not connect authenticated routing. Vapi's native SIP numbers (sip:<name>@sip.vapi.ai) are not affected. A number that already has such routing stored is fixed by sending both fields as empty strings. Theset_number_routingdescription and the /en/integrations/vapi, /en/docs/bring-your-own-agent and /en/docs/sip pages say so; for a failed call to a Vapi BYO trunk, the portal points at the login fields, the gateways and the region. - 13 Oct 2026 (1.36.1): An outbound agent call through the relay whose route never reached the carrier (the routing webhook did not answer within five seconds) still has
status: "no_answer", but nowendReason: "rejected"instead of"no_answer", and it does not count towards the automatic destination block (3 failures in 24 hours). In tasks such an attempt hascountsAsAttempt: falseanderrorCode: "relay_not_routed", uses up neithermaxAttemptsnor thelorela_enterpriseschedule, and the contact getsnextAttemptAtno sooner than 60 seconds later. After the third such attempt in a row the contact goes toreviewwithreviewReason: "call:relay_not_routed". - 12 Oct 2026 (1.36.0): The new internal
POST /v1/outbound/dispatch-scopes/petcenter/registrationsaccepts a durableoperationId, manifest SHA-256 and ordered task IDs. The first manifest is pinned to the approved 19-task list; later manifests can only append. GET returns registration state and operation readback. The new/api/cron/outbound-tasks/petcentercron uses a separately and fairly rotated queue; the general cron and atomic claim both enforce separation. Limits of 6 concurrent calls, 15 attempts per minute and 6,000 per day remain unchanged.GET /v1/tasks?ids=id1,id2reads one to five selected tasks owned by the authenticated account, reports subset completeness and rejects responses over 4 MiB.POST /v1/outbound/dispatch-scopes/petcenter/pausessafely pauses up to five registered tasks with revision checks; GET returns durable outcomes and an input hash for safe reconciliation. - 11 Oct 2026 (1.35.0):
POST/PATCH /v1/agentsand the MCPcreate_agent/update_agent/save_agent_drafttools take two new fields:firstMessageProtectSecs: number | null(0 to 20 seconds, decimals allowed, outbound calls only; while the first line plays, a short reaction from the person who answered does not interrupt it: a reply without an interrupting word (for example stop, no, wait) is dropped in the protected window, so it does not interrupt the line, is not in the transcript and the agent does not react to it, while an interrupting word always interrupts it; the interrupting words follow the agent's language, so an English agent is interrupted by stop, no, wait or hold;nullor0turns the protection off) andsilencePromptText: string | null(your own silence reminder sentence, up to 80 characters, spoken word for word instead of the default reminder for the agent's language ("Are you still there?" in English); an empty string ornullrestores the default text). UnlikesilencePromptSecs, WITHOUT a forced default: a missing field onPOSTis not stored,GET /v1/agentsreturns it asnullfor an older agent, and aPATCHwithout the key leaves the stored value alone, so nothing changes for existing agents. Both fields are accepted and stored for EITHER provider, but only heard on an agent running on volai's own engine. A value outside the bounds fails with the generic validation error (validation, 400), no new code. A transcript turn (transcript[]inGET /v1/calls/{id}, MCPget_calland thecall.completedwebhook) carries an optionalinterrupted: trueon an agent line the caller interrupted, withmessagestill the full planned text; it is absent on calls handled by ElevenLabs and on calls without an interruption. - 10 Oct 2026 (1.34.5):
GETandPOST /v1/sms-numbersand the MCP toolslist_sms_numbersandbuy_sms_numberreturndailySendLimitfor each SMS number (20 SMS per 24-hour window during the number's first 30 days, 50 after that),dailySendRemaining(0 for a number being released) anddailySendResetsAt(Unix ms when the window ends,nullwhen none is running). The window starts with the first SMS from any SMS number of the account, is shared by all of them and also counts attempts over the limit and SMS the carrier rejected. Themessage.sentwebhook body has a newsmsNumberfield: the E.164 SMS number for a message sent from an SMS number,nullfor the shared sender SMSinfo. Theinternal_errorandsend_failed_operatormessages for SMS now say "The error is on our side and we know about it",sms_number_out_of_stockno longer promises a restock and therate_limitedmessage for a used-up SMS number limit says how long until the window ends; error codes do not change. In the portal the /en/messages form shows the limit of the selected SMS number and the SMS messages card on the overview also counts received SMS. - 10 Oct 2026 (1.34.4):
agentCallLimitsinlib/service/call-quotas.tsreturns{ concurrent: 6, daily: 6000, perMinute: 15 }for the campaign scope; the condition of at least eight distinctODORIK_RELAY_NAMESstays and guarantees two free names for other callers. The account default (2, 1000, 10), the enablement conditions and the shared account counters are unchanged; API behaviour for other accounts is unchanged. - 10 Oct 2026 (1.34.3): The action for
send_unknownno longer says thatunknownis always final: for an SMS sent from an SMS number it can still change on its own tosentwith adeliveryStatus, or tofailedwith the credit refunded; for an SMS from the shared sender SMSinfo it stays final. The action forrate_limitedno longer promises aRetry-Afterheader in every case: the daily cap for sending from an SMS number (20 SMS a day per account during the first 30 days after buying the number, 50 after that) resets the next day, and a number purchase already running finishes within seconds.POST /v1/messageslistsblocked_destination(400, premium-rate numbers) among its errors. Messages received on an SMS number havesource: "inbound", the received-message list holds at most the 1,000 newest messages per account, and themessage.receivedwebhook is not sent above 300 received messages per hour on one number (the messages are still stored). Inmessage.received,fromis an E.164 number or a text sender name (at most 32 characters); a message that was not picked up at once is added by a periodic check within about 15 minutes and its event then arrives late. Neither the delivery report (deliveryStatus) nor a latefailedtriggers an event; read them fromGET /v1/messages/{id}. Releasing an SMS number is not available over MCP, only in the portal and withDELETE /v1/sms-numbers/{e164}. - 9 Oct 2026 (1.34.2):
agentCallLimitsinlib/service/call-quotas.tsreturns{ concurrent: 8, daily: 6000, perMinute: 15 }for the campaign scope; the account default (2, 1000, 10), the enablement conditions and the shared account counters are unchanged. Therate_limitedmessage for the daily limit now says 6000. - 9 Oct 2026 (1.34.1):
CAMPAIGN_QUOTA_EXPIRES_ATinlib/service/call-quotas.tsis2026-10-31T23:00:00Z; theVOLAI_PETCENTER_CALL_QUOTA_ENABLEDswitch, the exact account, agent and internal outbound-task context match and at least eight distinctODORIK_RELAY_NAMESstill apply. API behaviour for other accounts is unchanged. - 9 Oct 2026 (1.34.0): New endpoints
GET /v1/sms-numbers,POST /v1/sms-numbers(no body,Idempotency-Keysupported, returns 201) andDELETE /v1/sms-numbers/{e164}(irreversible, the paid period is not refunded), and MCP toolslist_sms_numbersandbuy_sms_number(confirmedMonthlyFeeHalmust equal the current fee in hellers, otherwise nothing is bought).GET /v1/messagesand MCPlist_messagestakedirection(outby default,in,all) and also return messages received on an SMS number; messages now carrydirection,smsNumberanddeliveryStatus(deliveredorundelivered, only for SMS sent from a number),statushas a new valuereceivedandfailReasona new valueopted_out.POST /v1/messagesand MCPsend_smstakefromNumber(an active SMS number of the account; Czech +420 recipients only; 1.90 CZK per segment). New webhook eventmessage.receivedwith{id, from, to, body, segments, receivedAt}(receivedAtis Unix ms); the text of a received SMS is untrusted third-party content and MCP marks it withsecurityNotice. Received SMS are kept for 90 days. New error codes:sms_numbers_unavailable,sms_number_limit,sms_number_out_of_stock,invalid_from_number,from_number_unsupported_destinationandrecipient_opted_out; the existing codesrecipient_rejected,sms_gateway_failed,recipient_is_virtual_number(an SMS from the shared sender to a volai SMS number) andrelease_failedalso apply to SMS numbers. The messagefromfield is still the internal labelvolaifor a message sent withoutfromNumber, the SMS number itself for one sent from it, and the sender for a received message. - 8 Oct 2026 (1.33.0): The
callback-routingpolicy accepts an optionalmissedFirstMessage(up to 500 characters): it replacesfirstMessagewhen the verified preceding outbound call had no conversation with the customer (not answered, voicemail, voice menu, nobody spoke, or the call is not closed yet). Initiation sends the enginevolai_callback_kind(missed/contacted) next tovolai_call_mode=campaign_callback. The engine post-call may carrycaller_optout_requested(outbound calls only): the call then exposesoptOutRequested: truein the list and detail, the called number is added to the account's do-not-call list (GET /v1/dnc) and thelorela_enterprisepolicy schedules no further attempt for that item. An engine call withanswered_by: unknownstaysunknown- the portal no longer guesses a voicemail from a short reply ("Yes." after the intro), sohasConversationof such a call follows the transcript; a voicemail detected by the engine (voicemailReasonpresent) gets another attempt under thelorela_enterprisepolicy despite the carrier greeting in the transcript. New reasonvoicemailReason: digits(a carrier greeting reading the called number digit by digit). Everything is additive; existing policies, calls and tasks are unchanged. - 8 Oct 2026 (1.32.1): The message
fromfield stays the internal labelvolai, and OpenAPI now describes it; thesourcefield description says the valueinboundnever occurs for messages. The MCP server instructions, SKILL.md and the llms files say the same. API behaviour is unchanged. - 8 Oct 2026 (1.32.0): Save an explicit allowlist of owned active engine agents for returning callers with a unique real outbound call from this number within
lookbackSecs(60-604800 seconds). Unknown, ambiguous or unverified callers keep the default agent. EmptyagentIdsdisables the policy. OptionalfirstMessageapplies only to verified callbacks.engineSyncRequired: truemeans the default agent engine configuration must separately activatewebhooks.inbound_route_urlwith readback; this request does not rewrite live engine configuration. Verified incoming calls exposecallbackOf.callIdand optional server-ownedtaskIdandtaskItemIdin call details. New REST GET/PUT/v1/numbers/{e164}/callback-routingand MCPget_callback_routing/set_callback_routing. Signed incoming tool context carries direction=in and empty dispatch/task/item fields. Server-ownedvolai_call_mode=campaign_callbackis set only for verified callbacks. Verified inbound human contact stops later scheduled retries of the same task item without creating an outbound attempt or charge. An already running call and its billing finish normally. The server-owned dispatch/task/item binding is retained for eight days to cover the callback window. - 8 Oct 2026 (1.31.0):
POST /v1/tasks,PATCH /v1/tasks/{id},create_taskandupdate_taskaccept optionalretryPolicy: "lorela_enterprise"andexpiresAt(positive safe-integer Unix milliseconds); both fields are returned in the public task. Without a policy,maxAttemptsremains 1-3 with a creation default of 1; with the policy it is 1-4 with a creation default of 4. PATCH without a limit preserves it. The policy can be enabled only before the first dispatch attempt. The second attempt is no earlier than eight minutes after the first real attempt starts, within 08:00-22:00 Europe/Prague; the third uses the next other 08:00-09:00 or 17:00-19:00 window; the fourth uses the opposite window on the following calendar day. A narrowercallingWindow, including weekdays, still applies. Onlyno_answer,missed,busyor completed voicemail without human contact can retry; uncertain results and engine failures need review. The policy rejects manualresolvewithaction: "retry"and stops calling five days after the first real attempt.expiresAtalso works without a policy and prevents any new dial at or after the deadline, including the first attempt; a call already in progress finishes normally. DNC and automatic blocking after three failures within 24 hours are not bypassed, so the fourth attempt is not guaranteed. - 5 Oct 2026 (1.30.2): A call for which the engine does not send
caller_loopin the post-call (the caller first talked to the agent and only afterwards just a repeated message was left on the line) hasendReason: "completed"and the regular price, like any other completed call. The fallback sync of a lost post-call (the engine'sGET /v1/calls) now readsmetrics.caller_loop_reported: the value1means the engine sentcaller_loopin the post-call, andcaller_loopis taken over as before; the value0means an ordinary call, which is closed and billed withendReason: "completed"regardless ofmetrics.caller_loop_released. For an older engine without this field, the existing rule withmetrics.caller_loop_releasedapplies (a positive value means an ordinary call). REST and MCP response shapes, theendReasonvalues and theoutcomefilter do not change. - 4 Oct 2026 (1.30.1):
endReason: "caller_loop"andpriceHal: 0are now given only to an inbound call with a positivedurationSecs; an outbound call, or a call with zero duration, for which the engine sendstermination_reason: "caller_loop"takes the regular path (endReason: "completed"and regular billing; a zero-duration call stays unclosed as before). The fallback sync of a lost post-call (the engine'sGET /v1/calls) takes overcaller_looponly whenmetrics.caller_loop_releasedis missing or0; with a positive value (a person spoke up during the wait) the call is closed and billed like any other completed call withendReason: "completed", andcall.completedcarries the price, the same as when the post-call arrives on its own. A call withendReason: "caller_loop"is never listed in the portal among calls needing attention (Overview and the Calls filter), and the weekly e-mail summary does not count it as a handled call or a caller and does not list it among calls that need a reaction. REST and MCP response shapes, theendReasonvalues and theoutcomefilter do not change. - 4 Oct 2026 (1.30.0): New
endReason: "caller_loop"value (RESTGET /v1/callsandGET /v1/calls/{id}, MCPlist_callsandget_call, webhookcall.completed): an inbound call whose caller was an automated system repeating the same message (a queue, a dialer), ended by the engine's guard. The source ismetadata.engine.termination_reason: "caller_loop"in the engine's post-call (for a lost post-call, the same field from the engine'sGET /v1/calls). The call hasstatus: "completed", a positivedurationSecsandpriceHal: 0, and no credit is charged; the price of 0 applies up to five minutes of call duration - a longer call is billed at the regular rates and keepsendReason: "caller_loop". Thecall.completedwebhook carriesendReason: "caller_loop". A call where a person spoke up during the wait and the engine carried on with them does not getcaller_loop. In theoutcomefilter (GET /v1/calls,list_calls) such a call counts asagent. - 4 Oct 2026 (1.29.6):
PATCH /v1/agents/{id},update_agentand draft publish: changinglanguagefrom a language in which new agents start on ElevenLabs (todayen,de,pl) to one in which they start on the engine (cs,sk) triggers, for an agent withprovider: "elevenlabs", the same post-save engine switch attempt as turning ontransferTo. On success the agent hasprovider: "engine"and the engine's default voice for the new language (milena,katarina); on failure it stays on ElevenLabs. A change betweencsandskmoves nothing.voicemail_requires_enginehas new wording: on creation it points touseForOutboundTasks: true, on an existing agent to support. - 4 Oct 2026 (1.29.5):
POST /v1/agentsandcreate_agentreturn a new codeelevenlabs_by_explicit_choice({code, message: {cs, en}}) inwarningswhen the request sentuseForOutboundTasks: false, the deployment default would have picked the engine for the agent's language, and the agent really stayed on ElevenLabs (atransferTohandoff may have switched it to the engine). The agent is created as before and the code blocks nothing.PATCH /v1/agents/{id}, draft publish and rollback never return it. - 3 Oct 2026 (1.29.4): Documentation only, the API behavior does not change.
maxAttempts(1-3) is the highest number of automatic dials of one contact, the first call included;1means no retries. A contact that you giveresolvewithaction: "retry"after its attempts are used up gets one more dial. Every attempt where the call could have happened counts (unanswered, declined by the person called, or with an uncertain result). An attempt rejected before dialing does not count and hasitems[].attempts[].countsAsAttempt: false. - 3 Oct 2026 (1.29.3): For attempts recorded from this version on,
items[].attempts[].countsAsAttemptisfalsefor every code where the call provably never happened, so they do not count towardmaxAttempts:agent_not_found,agent_no_number,insufficient_credit,invalid_number,unsupported_country,blocked_destination,on_dnc,destination_auto_blocked,cannot_call_own_number,cannot_call_volai_number,capacity_busy,call_rejected,validation,rate_limited,relay_lease_limit,invalid_workflow,engine_not_configuredanddestination_busy. Previously the permanent problems with the number (invalid_number,unsupported_country,blocked_destination,on_dnc,destination_auto_blocked,cannot_call_own_number,cannot_call_volai_number) were counted, so after lifting a block and another rejection a contact showed more attempts than allowed although nothing was dialed. Attempts written before this version are not rewritten retroactively (the exception is an attempt whose dial intent the accounting repair closes). Uncertain dial errors (for exampleengine_unavailable) still count, because the call may have happened. A permanent problem still sends the contact toreview(you decide), a repairable code (missing credit, an agent without a number and the like) pauses the task;rate_limitedandrelay_lease_limitleave the task running and only wait. Nothing is dialed in a loop because of this. A contact you return viaresolvewithretryafter its attempts are used up now stayspendingafter a repairable dial error without a call (for exampleinsufficient_credit) and is dialed again once the cause is fixed; before, it stayedfailedand the task could not be started (task_not_startable). - 3 Oct 2026 (1.29.2):
destination_busywhile dialing from a task (the call provably never happened): the contact keeps waiting (failedwhen it already had a counted attempt and has another free one, otherwisepending) withitems[].lastError: "destination_busy"anditems[].nextAttemptAtfrom the remaining time of the number lock (at least 30 s, at most 16 min, 90 s without a lock), the attempt hascountsAsAttempt: false, the reservation inreservedBudgetHalis released and the other contacts keep running; the task'slastErroris not set, only a stale waitingrate_limitedorrelay_lease_limitis cleared, because a busy result proves the limit passed. After the 6th busy result in a row (5 waits) the contact goes toreviewwithreviewReason: "dial:destination_busy". The seven permanent codes (on_dnc,destination_auto_blocked,cannot_call_own_number,cannot_call_volai_number,invalid_number,unsupported_country,blocked_destination) send the contact toreviewwithreviewReason: "dial:<code>", but the task staysrunningand moves toneeds_attentiononly when no other work is left (previously immediately). Over the API and MCP a block is lifted with two calls:POST /v1/dnc/{e164}/unblock(unblock_destination), thenresolvewithaction: "retry"(resolve_task_item); for your own number, a number of another volai account, an invalid number, a number outside Czechia and Slovakia and a premium-rate line onlyskiphelps. A contact inreviewcan now appear while the task is still running;resolvethen returns 409task_not_editable, so wait forneeds_attentionor pause the task.POST /v1/callsdoes not change. - 2 Oct 2026 (1.29.1):
items[].providerCallIdpoints only to the call of the latest attempt: a contact infailedthat is waiting for another attempt does not carry it; the call of every attempt stays initems[].attempts[].providerCallId. Previously the item kept the call of the first attempt, the next dialed call was not recorded on the task, and the contact was dialed again without counting the attempt and without adding the price tospentHal. The errorson_dnc,destination_auto_blocked,cannot_call_own_numberandcannot_call_volai_numberwhile dialing from a task are provably no call: the attempt getsoutcome: "failed", the reservation inreservedBudgetHalis released right away and the contact goes toreviewwithreviewReason: "dial:<code>"(previouslyoutcome: "unknown"and a held reservation).POST /v1/tasks/{id}/startno longer returns 409task_needs_attentionbecause of a reservation that no contact holds;POST /v1/tasks/{id}/reconcilesettles such a reservation and also closes an older open dispatch intent left after an attempt that was never dialed. - 2 Oct 2026 (1.29.0): New endpoint
POST /v1/numbers/orders/{id}/cancel(no body) and MCP toolcancel_number_order: apendingorder moves to the new statuscancelled(a newstatusvalue of the order inGET /v1/numbers/ordersandlist_number_orders);provisioningreturns 409in_progress,doneandfailed409invalid_transition, a foreign or unknown id 404not_found; cancelling again returns the order unchanged. A cancelled order no longer counts toward the first top-up gate (one number per account without a top-up).callingWindow.endof tasks is inclusive of the whole minute (current <= end): a00:00-23:59window covers the whole day andwindow_closedno longer occurs in theendminute itself. - 30 Sep 2026 (1.28.1):
PATCH /v1/numbers/{e164}(MCPset_number_routing) withmode: "forward"returns 402insufficient_creditwhen the account has never topped up and the number is not forwarded yet. A number that is already inforwardcan be pointed at a different target without a top-up.POST /v1/numbers(MCPbuy_number) andPOST /v1/numbers/orders(MCPorder_number_from_region) return 402insufficient_creditwhen asked for an additional number while an account without a top-up already holds an active number or has an open order (pending,provisioning). Same code and cause (account) as for SMS, different message text - it says what to do (make the first credit top-up); nothing is charged. After the first credit top-up (card, auto top-up) the restriction is gone. - 25 Sep 2026 (1.28.0): New
suspended: booleanfield on the agent object (REST and MCP,GET/POST/PATCH /v1/agents). New error codesagent_suspendedandaccount_suspended(403,cause: "account") -account_suspendedon every REST call with an API key and every MCP tool (after the rate limit) except reading tax documents (GET /v1/billing/documents,/{id}and/{id}/pdf, the MCP toolslist_billing_documentsandget_billing_document), which stay available;agent_suspendedonPOST /v1/calls,POST /v1/agents/{id}/test-callandPOST /v1/tasks/{id}/start. An inbound call blocked by a suspension of the agent or the account carriesendReason: "suspended"(a new value next tocredit_blocked, price0). Anaccount_suspended/agent_suspendedresponse is never stored under anIdempotency-Key, so the same key succeeds once restored. Custom variable keys (variablesonPOST /v1/callsandmake_call) can no longer start with thevolai_prefix - it is reserved for variables volai fills in itself (such asvolai_block_reason), and such a request returns 400validation; in tasks, such a key is only refused when the contact is dialed. Suspending and resuming is done only by the operator in the admin panel - there is no REST or MCP tool for it. The sentence a caller hears on a blocked line (debt or suspension) is now spoken in the agent's language (cs, sk, en, de, pl). - 25 Sep 2026 (1.27.3): No interface change: the same
warningscodeagent_misuse_suspectedin the same REST and MCP responses, only broader recognition. - 24 Sep 2026 (1.27.2): Outbound tasks:
relay_lease_limitfrom dialing (the quota of 2 concurrent agent calls per account) now behaves likerate_limited- the task staysrunning, setslastError: "relay_lease_limit", the attempt does not count (countsAsAttempt: false) and waiting contacts getitems[].nextAttemptAtone minute later. Previously the item went toreviewwithreviewReason: "dial:relay_lease_limit"and the task toneeds_attention. The engine sync (GET /v1/calls) reads the newattempt_idfield of an outbound call and uses it to match aninitiatedrecord after an uncertain dial (nocall_sid, also for an agent without its own number); once matched,call.completedis sent with the price and the relay line and quota are released. Requires an engine that listsattempt_id; an older listing behaves as before. - 24 Sep 2026 (1.27.1):
POST /v1/callswithagentIdon an agent on volai's own engine, a trial call withsystemPrompt, MCPmake_calland outbound tasks: when dialing on the engine ends with a timeout, a dropped connection, a 500/504 response or an unreadable 2xx, the response is still502 engine_unavailable, but the call staysinitiated(nopriceHal), the relay line is not released and the sametostays locked for 15 minutes (destination_busy); the concurrent call quota is released right away. If the call happened, the post-call or the engine sync fills in the outcome and price (the post-call now finds it by the attempt identifier even without acall_sid, including agents without a number of their own) andcall.completedis sent; if not, the watchdog closes it after 2 h asfailedwithpriceHal: 0. Thecall.failedwebhook is not sent for an uncertain dial. A certain failure (no connection to the engine at all or a timed-out connection attempt, 4xx, 502/503) behaves as before:failed,priceHal: 0,call.failed. - 24 Sep 2026 (1.27.0): New
warningscodeagent_misuse_suspected({code, message: {cs, en}}). Appears inPOST/PATCH /v1/agents,PUT /v1/agents/{id}/draft,POST /v1/agents/{id}/simulate,POST .../draft/publish,POST .../draft/rollback,POST /v1/calls(the trial call prompt orvariables),POST/PATCH /v1/tasks,POST/PATCH /v1/toolsand the matching MCP tools (create_agent,update_agent,save_agent_draft,simulate_agent_draft,publish_agent_draft,rollback_agent_draft,make_call,create_task,update_task,create_tool,update_tool). Thewarningsfield is optional, absent when there is nothing to flag, and /openapi.json describes it on all of these responses. - 24 Sep 2026 (1.26.3):
POST /v1/callswithfrom(bridge) and MCPmake_call: when the connection order to the phone operator ends with a network error, a timeout or a 429/5xx response, the call is returned as a success withstatus: "initiated"instead of502 callback_failedand staysinitiateduntil cdr-sync pairs it with the CDR (and bills it), or the watchdog closes it after 24 h asfailedwithpriceHal: 0.callback_failednow means only a provable rejection by the operator (an error response or 4xx). Thecall.failedwebhook is not sent for an uncertain order; the sametonumber stays locked for about 10 minutes (destination_busy). - 24 Sep 2026 (1.26.2): The per-account cap on calls within 24 hours (shared by
POST /v1/callswith an agent or a bridge, relay and trial calls, MCPmake_calland outbound tasks) is now 1000 instead of 200. Exceeding it still returns429 rate_limitedwith the daily-limit message;10/min, concurrency and the engine budget are unchanged. - 24 Sep 2026 (1.26.1): Outbound tasks: a call record in
failedwith noconversationIdand nopriceHal(the engine refused or did not confirm the dial) now settles at 0 inreconcile- previously the item keptreviewReason: "billing_unknown"and the task stayedneeds_attentionforever.resolvewithskipon abilling_unknownitem moves a task with remaining work topaused(the original call's reservation keeps counting inreservedBudgetHal, andstartaccepts it);retrystill waits for the price. A decision right after a dial error (reviewReason: "dial:...") no longer loses the link to the call or its reservation.rate_limitedwhile dialing (engine limit, 10 calls per minute, 200 per day) keeps the taskrunningwithlastError: "rate_limited"and sets waiting items'nextAttemptAtto the end of the limit (fromRetry-After, otherwise in 5 minutes, at most 24 hours) instead ofpaused; the attempt does not count.invalid_workflowandengine_not_configuredwhile dialing are provably no call (the task becomespaused, the attempt does not count). Every call by an agent on volai's own engine that the engine never dialed (outbound tasks as well asPOST /v1/calls, MCPmake_calland the portal) haspriceHal: 0anddurationSecs: 0inGET /v1/callsinstead of missing values; thecall.failedwebhook is unchanged. - 24 Sep 2026 (1.26.0):
POST /v1/callswithagentIdon an agent on volai's own engine (and MCPmake_call): a rejection by the engine's outbound-minute limit (hourly and daily bucket per account) or by full engine concurrency is now429 rate_limitedinstead of502 engine_unavailable; the minute limit adds aRetry-Afterheader (seconds until the end of the hourly window, or until midnight UTC for the daily one). Nothing was dialed or charged; the call stays in the history asfailedand thecall.failedwebhook carriesreason: "rate_limited". Outbound tasks treatrate_limited(including the 10 calls/min and 200/day limits) as provably no call: the item isfailedwithout counting an attempt, and the task ispausedinstead ofneeds_attention. ThePOST /v1/callserror table now also lists502 engine_unavailable- for an agent on volai's own engine it means only a real engine outage, not a limit. - 23 Sep 2026 (1.25.0): New optional
event_idfield in the webhook envelope ({event, data, ts, event_id}) forcall.completed/call.failed/call.no_answer/call.missed-message.sentand thewebhook.testtest event are unchanged. A failed delivery of those four events no longer stops after 3 attempts (3s apart): it stays queued and retries over the following minutes, up to 5 rounds; a repeat delivery carries a byte-identical body.GET /v1/webhook/deliveries(and MCPlist_webhook_deliveries) now also show rounds before the final one,attemptsis the sum of attempts across all rounds, and there is a new optionaleventIdfield. - 23 Sep 2026 (1.24.1): Two new
warningscodes:prompt_variables_unavailable_inbound(an agent withnumberE164whosesystemPrompt/firstMessageuses a{{variable}}outside the set an inbound call actually supplies) andfirst_message_empty(afirstMessagewith no letter or digit left in it outside the variables once every{{...}}block is removed). Both REST (POST/PATCH /v1/agents, publish and rollback) and MCP (create_agent,update_agent,publish_agent_draft,rollback_agent_draft) read the same source as the portal's agent editor (agentWarnings). - 20 Sep 2026 (1.24.0): New optional
busyCalendarIdsfield on the agent (POST/PATCH /v1/agents, MCPcreate_agent/update_agent, drafts) and on agent-less slot search and booking (POST /v1/integrations/{id}/availability,POST /v1/integrations/{id}/events, MCPfind_free_slots,create_calendar_event). The calendars are read in parallel; if any of them fails, the whole lookup fails, so no time is offered over an event we simply could not see. At most four, without duplicates and withoutcalendarId; otherwise 400calendar_invalid_rules, an unknown calendar 400calendar_invalid_calendar. - 20 Sep 2026 (1.23.4): The error tables of
POST /v1/agents,PATCH /v1/agents/{id}and draft publish/rollback add the six codesvalidateCalendarreturns: 404calendar_integration_not_foundand 400calendar_invalid_calendar,calendar_invalid_timezone,calendar_invalid_windows,calendar_no_windows,calendar_invalid_rules. This flows intoopenapi.json, the /docs/api reference,SKILL.mdandllms-full.txt. - 20 Sep 2026 (1.23.3):
maybeQueueMissedCallSmsnow also fires for an outbound call withansweredBy: "voicemail"(previously onlystatus: "no_answer"and inboundmissed). A voice menu (ivr) orunknowndoes not trigger it. All other conditions (CZ/SK number only, DNC, blocked destinations, one SMS per number per 24 h, quiet hours, last task attempt) are unchanged;CallRecord.missedSmsis written the same way. - 19 Sep 2026 (1.23.2): A timestamp with milliseconds was formatted with a time zone offset one minute short (
+01:59instead of+02:00) - this affected thenowfield in the agent tools and the start of a created event when a client sentstartwith fractional seconds. Alternatives returned with 409calendar_slot_taken(POST /v1/integrations/{id}/events, MCPcreate_calendar_event) are now computed from the requested time, the same asfind_free_slotswith a time. - 19 Sep 2026 (1.23.1): The agent
calendarfield (POST/PATCH /v1/agents, MCPcreate_agent/update_agentand drafts) rejects a calendar whoseaccessRoleisreaderorfreeBusyReaderwith 400calendar_invalid_calendar, because the agent writes appointments into the same calendar.list_calendarsreturns the role asaccessRolewhere the provider reports it; a calendar without a role is accepted as before. No other error codes change. - 19 Sep 2026 (1.23.0): New
CallRecord.ratingfield (value: "good" | "bad",reason?,note?,at,source: "portal" | "email" | "api" | "mcp") andCallRecord.missedSms;PATCH /v1/calls/{id}and MCPannotate_callacceptrating/ratingReason/ratingNote(patchCallAnnotationcallsrateCallwithsource: "api"/"mcp"). NewAgentRecord.missedCallSms { enabled, text? }field (variables{firma},{cislo},{agent}) onPOST/PATCH /v1/agents,GET /v1/agents/{id}and MCPcreate_agent/update_agent/get_agent/save_agent_draft.GET/PATCH /v1/accountand MCPget_account/update_accounthavecompanyName(max 80 chars, empty string clears it), and GET now returnsnotifications.weeklySummary. The test call in the agent editor shows the outcome based on the call goal (call.evaluation.goal) and the collected data. - 18 Sep 2026 (1.22.0): New endpoints
POST /v1/integrations/{id}/availability(free slots without a saved agent -windowsomitted means 24 hours every day) andPOST /v1/integrations/{id}/events(create an appointment, requiresconfirm: true, optionalidempotencyKeyfor safe retries; 409calendar_slot_takenon a collision), MCP toolsfind_free_slotsandcreate_calendar_eventwith the same shape.DELETE /v1/integrations/{id}now returnsaffectedAgents- agents whosecalendarfield was turned off by disconnecting this integration. Newcalendarfield onPOST/PATCH /v1/agents,GET /v1/agents/{id}and MCPcreate_agent/update_agent/get_agent:integrationId,calendarId,timezone,slotMinutes,minNoticeMinutes,horizonDays,bufferMinutes,windows(an array of{from,to}windows per weekday). Works only on volai's own engine (provider: "engine") - on an agent hosted on ElevenLabs the field is saved and waits for thecalendar_pending_engine_switchwarning until the agent switches. Eleven new error codes withcause/action:calendar_slot_taken,calendar_confirmation_required,calendar_invalid_range,calendar_integration_not_found,calendar_invalid_calendar,calendar_invalid_timezone,calendar_invalid_windows,calendar_no_windows,calendar_invalid_rules,calendar_too_many_events,calendar_unavailable. - 18 Sep 2026 (1.21.0): New ledger movement type
setup_revision(GET /v1/credit/ledger, MCP) for a done-for-you setup order's revision round beyond the package, amountSETUP_EXTRA_REVISION_HAL(490 CZK excl. VAT). Tax documents (GET /v1/billing/documents, .../{id}, list_billing_documents, get_billing_document) now always carry akindfield ("invoice" - default, or "credit_note"), plus optionalcorrectsNumberandcorrectionReasonfields; a done-for-you setup refund's credit note is numbered in its own OD series and carries positive amounts (the minus sign is added by the PDF).credit()(lib/service/balance.ts) gained an internalnotifyTelegramparameter - a done-for-you payment now sends just one Telegram alert ("Na klíč: zaplaceno"), not two. New public landing page at /receptionist-done-for-you (Czech canonical /recepcni-na-klic). - 18 Sep 2026 (1.20.0): New
laughterfield (auto/on/off) onPOST/PATCH /v1/agentsand MCPcreate_agent/update_agent:autofollows the engine's sensitive-topic detection (debt collection, healthcare, authorities, funeral services),onoverrides that detection,offalways suppresses it; an omitted key is stored and returned asauto, never absent.GET /v1/agents/{id}and the new MCP toolget_agent(previously missing parity for a single agent's detail) addlaughterEffective({active, reason, sensitiveCategory} | null) nested inside the returned agent object, computed fresh for the published configuration;nullmeans it could not be determined right now (the engine did not respond within 3 seconds, was down, or returned a silent 404 on an older build) - never a guess. MCPupdate_agentreturns the same object as a top-level field next toagentinstead, sincePATCH /v1/agents/{id}never returns it. The field is accepted and stored for an agent on ElevenLabs too, but has no audible effect there - only a Czech Cartesia voice on volai's own engine plays it. - 18 Sep 2026 (1.19.0): New REST API v1 and MCP area - source setup-orders, capability area setup (after credit). Seven endpoints (POST /v1/setup-orders, GET /v1/setup-orders, GET/POST /v1/setup-orders/{id}, .../messages, .../checkout, .../launch, .../cancel) and seven matching MCP tools (create_setup_order, list_setup_orders, get_setup_order, message_setup_order, checkout_setup_order, launch_setup_order, cancel_setup_order) over the same service layer, lib/service/setup-orders.ts. The quote is always computed by the catalog in code (lib/setup-catalog.ts); the model only picks items from the conversation. New error codes: setup_order_not_found, order_closed, invalid_transition, quote_pending_confirmation, checkout_open, turn_in_progress, advisor_no_quote, advisor_unavailable, setup_orders_disabled. Documentation at /docs/done-for-you (Czech /docs/na-klic).
- 17 Sep 2026 (1.18.0): The agent draft (
PUT/GET /v1/agents/{id}/draft,POST /v1/agents/{id}/draft/publish) and the MCPsave_agent_drafttool now treatnumberE164as three-state: an absent key leaves the agent's number untouched,nulldisconnects it, and a string attaches that number - parity with the semanticsPATCH /v1/agents/{id}already had. Reading a draft also reconciles it against the live agent:published.numberE164always mirrors the live number, anddraft.numberE164does too whenever the draft made no explicit choice, so the editor's pre-publish check and outbound tasks see the true state even after a number was attached or moved outside the editor; "no explicit choice" also covers a string that matches the agent's current LIVE number - that value behaves the same as an absent key, re-checked on every draft read against whatever number is live at that moment; if the number later moves to a different agent outside this draft, the same stored value becomes an explicit choice again and the next publish or rollback reattaches it here - not silently: publish and rollback responses (REST and both MCP tools) now carry a newnumber_changed_by_publishwarning withfrom/to(E.164, ornullfor no number) in that case, without blocking the operation either. One read-side compatibility note:draft.numberE164can now come back asnull(an explicit "no number" choice) where the key used to be simply absent - treat it the same as an absent key, meaning the agent has no number. The "tested" marker and the agent fingerprint pinned to a running outbound task are now computed WITHOUT the phone number too (behaviorFingerprint, notdraftFingerprint), so attaching or moving a number outside the editor no longer requires a fresh test and no longer stops a running campaign; a draft with a number saved BEFORE this change has its marker computed the old way, including the number, so it asks to be re-earned once - one test or publish after the update restores it, and the new rule applies from then on. - 15 Sep 2026 (1.17.0): A new goal display state,
GoalDisplayState(success/failure/unclear/no_conversation/not_evaluated), replaces theAgentGoalResulttriple in the portal's display, in the/hovoryfilters and in the overview. REST and MCP gainedansweredBy: "ivr",goal.reason("no_caller_speech"/"evaluation_error"), andhasConversation(boolean | null). Thegoalparameter (GET /v1/calls, MCPlist_calls) accepts five new values alongside the existingunknown, which stays backward compatible - it still covers records with no clear result (goal.resultmissing orunknown): alwaysunclearandnot_evaluated, andno_conversationonly when its result is alsounknown. - 13 Sep 2026 (1.16.0): A new
endReason: "engine_error"value (statusfailed) appears when the engine sendsmetadata.engine.termination_reasonasengine_stallorengine_errorfor a call that was actually handled - previously that field was discarded for such calls.priceHalis set to0only when the call has no price yet (otherwise it is left unchanged, with an ALERT log). Thecall.failedcustomer webhook gained four new optional fields:durationSecs,priceHal,transcript,summary- populated only for this technical branch, missing everywhere else as before.GET /v1/calls/{id}and MCPget_callreturn the sameendReasonvalue. Outbound tasks (POST /v1/outbound-tasks, MCP) treatengine_erroras a retryable attempt (the same path asno_answer), not a reason to stop the campaign, and do not record it toward the automatic 30-day destination block. - 13 Sep 2026 (1.15.0):
POST/PATCH /v1/agentsand the MCPcreate_agent/update_agent/save_agent_drafttools take a newsilencePromptSecs: number | nullfield (a whole number 3 to 25,nullturns it off), bounds matching the engine (engine/types.py, U1). A missing field onPOST/create_agentsaves the default of 10 - unlikevoicemail, the field is accepted and returned for EITHER provider (GET /v1/agentsreturns the stored value even forprovider: "elevenlabs"). A value outside the bounds fails with the generic validation error (validation, 400), no new code. Existing agents on the engine get the default of 10 via a one-off re-sync script (scripts/resync-silence-prompt.mts) that changes only this one field. - 13 Sep 2026 (1.14.7): OpenAPI adds descriptions to every operation and request field, including nested variants. Agent, tool, SMS and call constraints use product limits shared with the service layer instead of permissive parser ceilings. Automated checks validate all request and response examples against their schemas. API behavior, error codes and provider settings are unchanged.
- 13 Sep 2026 (1.14.6):
POST /v1/messagesand MCPsend_smsreturninsufficient_credit(HTTP 402, causeaccount) until the account has a positivetopup(card or auto top-up) oradmin(manual owner adjustment) ledger entry. The previous cap of 3 SMS for accounts without an active number of their own is gone, and an active number no longer grants an exception. The first top-up is kept as a permanent account flag, so a long ledger history never blocks a customer. - 13 Sep 2026 (1.14.5): New
workflow_node_tool_missingcode inwarningsonPOST/PATCH /v1/agents, on draft publish and rollback, and on MCPcreate_agent,update_agent,publish_agent_draftandrollback_agent_draft. It applies to both providers, because a deleted tool does not reach the phase on volai's own engine either.workflow_node_tool_not_syncednow only covers a tool that is on the account but not among the agent's own. The Reception template also recommends two sentences for the base prompt: do not guess operational details, and ask about one thing at a time. - 12 Sep 2026 (1.14.4): Setup distinguishes OAuth grant identifiers from API keys when restoring older saved choices. An unavailable status check is not treated as revoked access.
- 12 Sep 2026 (1.14.3): Added the catalog files katty-studio.mp3 and katty-telefon.mp3 using the same generator and content as the other voices. The agent voice, provider and phone service settings are unchanged.
- 12 Sep 2026 (1.14.2): Copy labels and next-step guidance match the selected connection method. Codex desktop with an API key points to config.toml. Closing the mobile menu preserves focus restoration.
- 12 Sep 2026 (1.14.1): Sign-in preserves the assistant setup destination. The workspace preference is stored per account separately from actual MCP verification. Manual guides cover Claude Code, Codex CLI and desktop, with API keys as an alternative. Help and the names Voice agents, Phone numbers and Data & integrations are consistent.
- 12 Sep 2026 (1.14.0): When
useForOutboundTasksis omitted, selection depends onVOLAI_ENGINE_DEFAULT_FOR_NEW, engine availability andVOLAI_ENGINE_DEFAULT_FOR_NEW_LANGUAGES(unset meanscs,sk). Language alone does not guarantee the engine. Explicittruerequests the engine;falsestarts on ElevenLabs. Applies to the portal,POST /v1/agentsand MCPcreate_agent. Read backproviderandvoiceIdafter creation; handoff may trigger a subsequent provider switch attempt. - 12 Sep 2026 (1.13.0): The workflow field is available in POST and PATCH /v1/agents, in the agent draft, and in the create_agent, update_agent and save_agent_draft MCP tools. A call that went through phases carries workflowPath in GET /v1/calls and GET /v1/calls/{id}. New workflow_*, workflow_not_synced_to_provider and workflow_node_tool_not_synced warnings flag phase configuration issues.
- 12 Sep 2026 (1.12.5):
voiceIdfor an ElevenLabs agent:katty(default, the template voice) or a raw ElevenLabs id;janais gone from the catalog and from ElevenLabs (the two agents from the afternoon of 12 Sep were switched to Katty and the voice was removed from the workspace).GET /v1/voices?provider=elevenlabsand MCPlist_voiceswithprovider: elevenlabsreturnkatty. The engine's ElevenLabs fallback for Milena, Tereza and Katarina is Katty as well. - 12 Sep 2026 (1.12.4): Polling no longer interrupts a slow status request. Successful MCP read evidence is found by active access even if writing the helper index failed. Verification still requires get_balance or list_agents through MCP.
- 12 Sep 2026 (1.12.3): Fixed the English calendar privacy link and kept an active goal filter visible even without previous evaluations. Both languages have regression coverage for links and unavailable integrations.
- 12 Sep 2026 (1.12.2): The MCP guide uses existing evidence of successful reads through an active API key or OAuth grant, with no simulated verification. The goal=unknown filter in GET /v1/calls and MCP list_calls also includes records without a goal evaluation. Technical completion does not mean a goal was met. REST and MCP still return individual records; the portal groups them into conversations.
- 12 Sep 2026 (1.12.1):
voiceIdfor an ElevenLabs agent:jana(default, the template voice) or a raw ElevenLabs id;anetpasses only on an agent that already has it, otherwise a 400validationwith an explanation (previously a 502eleven_labs_errorfrom ElevenLabsvoice_live_moderated_not_allowed).GET /v1/voices?provider=elevenlabsand MCPlist_voiceswithprovider: elevenlabsreturnjana. An ElevenLabs agent withoutvoiceIdgets Jana explicitly (the record carries her id). SIP registration: the carrier returned403 INVALID_USERfor lines bought through its API until the line's SIP protocol was toggled off and on once; fixed for all lines and done automatically when a new line is bought. - 12 Sep 2026 (1.12.0): OAuth authorization code + PKCE S256, DCR, resource-bound opaque tokens, refresh rotation/replay detection, OIDC discovery/JWKS/userinfo and revocation. Access tokens last 10 minutes, refresh tokens up to 30 days, grants up to 90 days. Password changes invalidate OAuth access. MCP annotations explicitly distinguish overwrites, paid actions, messages and external side effects.
- 12 Sep 2026 (1.11.0):
GET /v1/numbers/{e164}/sipand MCPget_sip_credentials:outboundTrunkAddress=sip.volai.cz(volai's own SIP proxy with digest auth, realmsip.volai.cz; measured: the platform refuses an INVITE larger than the UDP MTU, the same call passes over TCP),transport=tcp(schema is nowenum [tcp, udp], UDP works too),port5060 unchanged. The watchdog monitors the proxy healthz (SIP_PROXY_HEALTHZ_URL): ops e-mail when unreachable, when the auth data sync stalls or when the certificate has under 14 days left. The ElevenLabs post-call webhook has its HMAC secret in production and is enabled per agent (ELEVENLABS_POST_CALL_WEBHOOK_ID). - 12 Sep 2026 (1.10.0):
GET /v1/numbers/{e164}/sipand MCPget_sip_credentials:outboundTrunkAddressis driven by its own configuration (it may equalserver), field descriptions no longer mention a realm;inboundSignallingCidrsunchanged. The ElevenLabs post-call webhook matches inbound calls viametadata.phone_call.call_sid(it used to read the SIP Call-IDcall_id, which never matched thecall:sidkey written by the initiation webhook) throughfindExistingCallByKeysshared with theconversations-synccron (ordercall:conv->call:attempt->call:sid). Per-agent post-call webhook settings (platform_settings.workspace_overrides.webhooks) are migrated by the cron onceELEVENLABS_POST_CALL_WEBHOOK_IDis set; until then the cron keeps closing calls. The engine post-call acceptsphone_call.external_number: nullandcaller_presentation; theuri_caller prefix is stripped as in the initiation webhook. The call transfer target (transfer_to_number) is built from a separateVOLAI_SIP_TRANSFER_HOST, not from the public SIP domain. New internal source of auth data for the SIP proxy (GET /api/internal/sip-subscribers, outside v1, bearer + IP allowlist + rate limit). - 12 Sep 2026 (1.9.0):
POST/PATCH /v1/agentsand MCPcreate_agent/update_agentcan now return a secondwarningscode:first_message_too_long, independent offirst_message_no_ai_disclosure- and publishing an agent draft (POST /v1/agents/{id}/draft/publish, MCPpublish_agent_draft) and rolling it back to an earlier revision (POST /v1/agents/{id}/draft/rollback, MCProllback_agent_draft) now return the samewarningstoo, which they previously omitted entirely - both of them write the first line to the live agent. The estimate counts words, digits per character, and all-caps abbreviations up to four characters; a longer all-caps word counts as one word, so a company name written in capitals no longer inflates the estimate. The single threshold (an estimated duration over 7 seconds) is measured against what the caller actually hears - the greeting plus the call-recording notice, when enabled. An unchanged default greeting template never gets the warning, only a line the customer has edited. - 12 Sep 2026 (1.8.2): Documentation, integration guides and error actions point to AI assistant for access management. MCP makes clear that the voice engine is required to start an outbound task; a task can also be created with an ElevenLabs agent.
- 12 Sep 2026 (1.8.1): OpenAPI now describes Idempotency-Key for 14 supported operations, response headers and the optional revision JSON body when deleting a task. Documentation corrections without changing paid-action behavior:
useForOutboundTasksdistinguishes true/false/omission, tasks support 1000 recipients andbilling_unknownretains the original call and reservation until its price is known. Credit tax documents are available over REST and MCP; an API key can revoke only itself over REST; integration disconnect is REST/portal. Correction to 1.6.1:preview_task_csvis not an MCP tool; usePOST /v1/tasks/csv-previewor the portal. Install commands share the onboarding generator, example tabs have unique anchors and Python preserves JSON values. - 11 Sep 2026 (1.8.0): The new REST
POST /v1/dnc/{e164}/unblockand the corresponding MCP toolunblock_destinationlift an automatic destination block; they also work for blocks created before this deployment. Older blocks may be absent fromGET /v1/dncandlist_dncbecause they predate the listing index, but direct unblock by phone number still works. Indexed blocks are returned in ablocked: [{e164, blockedAt, expiresAt}]array (times are Unix milliseconds) alongside the unchangednumbers. After a block is lifted, the failed-attempt counter starts over - three new failed attempts to the same number block it again, and lifting it again is not limited.groupCallsByConversation(the shared primitive behind /hovory, the CSV export, the Previous/Next navigation, and the stats) now sorts byrepresentative.startedAtdescending; it used to sort bykey, i.e. alphabetically by conversation id.TestCallOutcomehas a newno_answervalue kept separate fromfailed, so the panel never claims a cause it doesn't know. A name written in ALL CAPS now gets an ALL CAPS vocative form (RADEK->RADKU) in both the e-mail and the Overview header. - 11 Sep 2026 (1.7.1):
agent_busy(HTTP 409,cause: "busy") was added to the shared error mapPUBLIC_ERROR_HTTP/PUBLIC_ERROR_CAUSE(lib/types.ts). It is returned byswitchAgentProvider(lib/service/engine-provider.ts) - the only portal admin action that switches an agent's provider; REST v1 and MCP have no such input (agents.switch_providerin the capability catalog,lib/capabilities.ts). The check uses a newGET /v1/calls/activeclient (lib/engine.tslistActiveCalls): the agent's call is matched PRIMARILY by theagent_idfield, which the engine carries on an active call since the version that added it was deployed; the number stays a fallback for an older engine without it. - 11 Sep 2026 (1.7.0):
POST/PATCH /v1/agentsand the MCPcreate_agent/update_agent/save_agent_drafttools take a newvoicemail: {enabled, action: "mark"|"hangup"|"message", message?}field - only for an agent withprovider: "engine", otherwisevoicemail_requires_engine;messageis required whenaction: "message", up to 400 characters (invalid_voicemail_message).GET /v1/agentsreturns the configured value. The engine's post-call now carriesanswered_by/voicemail_reason/voicemail_message_left/voicemail_detected_at_secs;answered_bytakes precedence over the previous transcript-based estimate (lib/answered-by.ts) only withhumanandvoicemail-unknownis treated as "the engine didn't decide" and the transcript-based estimate is used instead.GET/list_calls/get_callfor an outbound call handled by the engine also returnvoicemailReason(a fourth value,human_reply, for a short human reply),voicemailMessageLeft, andvoicemailDetectedAtSecs. - 11 Sep 2026 (1.6.5): For an outbound call ended before answer the engine deletes the room and delivers a post-call webhook: empty transcript,
call_duration_secs: 0and the new additive fieldmetadata.engine.termination_reason(no_answerordisconnected_before_answer). The call getsstatus: no_answer,endReason: no_answerand price 0 as before, only immediately. TheGET /v1/callsreconciliation pairs engine outbound calls bycall_sidand sends zero duration for calls that never connected.GET /openapi.json:answeredBynow carries adescriptionabout the outbound-only limitation. - 11 Sep 2026 (1.6.4):
POST /v1/tasksandPATCH /v1/tasks/{id}(andcreate_task/update_taskin MCP) accept up to 1000recipients,POST /v1/tasks/csv-previewup to 1000 rows. A new task record size check (2 MB of serialized JSON) returnsvalidationwith fieldrecipients. - 11 Sep 2026 (1.6.3): ElevenLabs initiation webhook: the payload's
agent_idis now checked against the routed agent'selevenAgentIdORid- a foreignagent_idgets only a minimal response withoutconversation_config_override; the same check guards closing the conversation in the post-call webhook and the conversations-sync cron. CDR-sync cron: an unrecognized outbound CDR record from your own line (excluding the demo number), older than 600s and without an ambiguously matching existing record, is now billed askind: "sip"at the outbound rate with no agent surcharge (it shows up inGET /v1/callswithkind: "sip"; thechargedSipcounter is only in the cron's own response, not in the public API); a younger or ambiguously matching record is still just held for manual review.CallKindextended with"sip". The CDR shape of a real SIP client call has no production evidence yet (no registered softphone) - hence both safeguards. - 11 Sep 2026 (1.6.2): New endpoint
POST /v1/tasks/{id}/items/{itemId}/resolvewith body{ action: "retry" | "skip", revision }and the MCP toolresolve_task_item; new error codeitem_not_resolvable(409, account),task_not_editablefor a running or completed task. The engine call sync matches the account's own outbound leg by relay line and start time, closes an unanswered outbound call asno_answerand releases the tenant concurrency quota after closing.relay_lease_limitwhile dialing a task is a transient state that does not consume an attempt. - 11 Sep 2026 (1.6.1):
POST /v1/tasks/csv-previewand the portal recognise the phone column regardless of case, surrounding spaces and a trailing colon and accept aliases (telefon,tel,mobile,phone number); the response always names the columnphone. A single-column input without a header whose first value is a phone is treated as data. Without a phone column the preview returns a singlephoneerror on the header instead ofPhone is requiredon every row. Theagent_not_readycontract is unchanged. Documentation correction, 12 Sep 2026: the original MCP-tool claim was incorrect; CSV preview is REST/portal-only. - 11 Sep 2026 (1.6.0):
GET /v1/numbers/{e164}/sipand the MCPget_sip_credentialstool now also returnoutboundTrunkAddress,port,transportandinboundSignallingCidrsalongsideserver/username/password; MCPcreate_relay_leaseand theget_sip_credentialsdescription now explicitly distinguishserver(registration) fromoutboundTrunkAddress(a platform's outbound trunk). The/en/docs/bring-your-own-agentguide, the ElevenLabs integration page and the blog post no longer putserverintooutbound_trunk_config.address- onlyoutboundTrunkAddress. - 11 Sep 2026 (1.5.1):
GET /v1/calls,GET /v1/calls/{id}, thecall.completedwebhook and MCPlist_calls/get_call:answeredByis present only whendirection: "out"(older records included).POST /v1/messagesandsend_sms: an account without an active number and without a positivetopup/refund/adminledger entry getsinsufficient_credit(HTTP 402, causeaccount) after three sent SMS. - 8 Sep 2026 (1.5.0): New REST endpoints:
GET/PATCH /v1/account,PUT /v1/account/billing,GET /v1/api-keys,DELETE /v1/api-keys/{id}(self-revoke);PATCH /v1/calls/{id}andGET /v1/callsextended withfrom,to,agentId,flagged,goal,outcomequery parameters;POST /v1/tools/test,POST /v1/tools/{id}/test,DELETE /v1/webhook, andGET /v1/tools/{id}now additionally returnsagents;POST /v1/agents/{id}/draft/rollback;GET /v1/numbers/{e164},GET/POST /v1/numbers/waitlist,DELETE /v1/numbers/waitlist/{offerId},GET /v1/numbers/{e164}/sip/status;POST /v1/integrations,DELETE /v1/integrations/{id},POST /v1/integrations/{id}/events/{eventId}/propose;GET /v1/changelog(no API key,x-volai-versionon every v1 response including errors, 429 and/openapi.json). 16 matching new MCP tools:get_account,update_account,update_billing_details,list_api_keys,annotate_call,test_tool,remove_webhook,list_webhook_deliveries,rollback_agent_draft,reconcile_agent_draft_operation,test_call,get_number,join_number_waitlist,leave_number_waitlist,get_sip_status,get_changelog- revoking someone else's API key, creating an API key, and disconnecting an integration or card stay REST-only or portal-only (spec §3). The new capability catalog holds acapabilityfield on every endpoint and tool; guard tests verify the error table and response example on/docs/apimatch the real error codes for every endpoint. Additive only - no existing status or code changed. The newcontent/changelog.jsonfile is the single source of truth:/en/changelog, the changelog section on/docs/api, the Versioning section in SKILL.md, RSS (/en/feed/<area>),GET /v1/changelogand the MCPget_changelogtool are all generated views over it. Additive only - no existing content disappeared. New REST endpoints:GET/POST /v1/tasks,GET/PATCH/DELETE /v1/tasks/{id},POST /v1/tasks/{id}/start,POST /v1/tasks/{id}/pause,POST /v1/tasks/{id}/reconcile,POST /v1/tasks/csv-preview. 7 matching new MCP tools:list_tasks,get_task,create_task,update_task,pause_task,reconcile_task,start_task(which requires repeating the real recipient count and budget, otherwise it returns an error with the real numbers) - deleting a task and previewing a CSV stay REST and portal only (spec §3). Portal: a Delete task button on the task detail page with type-to-confirm. Additive only - no existing status or code changed. New REST endpoints:GET /v1/credit/ledger,POST /v1/credit/topup,GET/PATCH /v1/credit/auto-topup,POST /v1/credit/auto-topup/setup,POST/DELETE /v1/credit/auto-topup/card,GET /v1/billing/documents,GET /v1/billing/documents/{id},GET /v1/billing/documents/{id}/pdf;GET /v1/balancenow additionally returnsrunwayandnotice. 6 matching new MCP tools:list_ledger,create_topup_link,get_auto_topup,disable_auto_topup,list_billing_documents,get_billing_document(get_balanceextended with the samerunway/notice) -get_billing_documentdoes not return the PDF, which stays REST-only viaGET /v1/billing/documents/{id}/pdf(binary output); turning auto top-up on and changing its amount or card stay behind a Stripe Checkout link only, MCP has neither (spec §2, §3). Additive only - no existing status or code changed.GET /v1/accountnow returns a newnotifications.productUpdatesfield (on by default); there is a matching toggle in portal Settings. The daily email digest (cron0 6 * * *UTC) only sends to accounts with a verified email andproductUpdates !== false, at most one email per account per day. TheList-Unsubscribeheader and cookie-free one-click unsubscribe now also cover the reactivation email whenUNSUBSCRIBE_SECRETis configured on the server - without it the email still sends, just without the unsubscribe link. Additive only - no existing status or code changed. - 8 Sep 2026 (1.4.3): Every input property of all 44 MCP tools carries its own
description(clients see it intools/list; thePATCH .../events/{eventId}andPUT /v1/agents/{id}/draftbodies expose it inopenapi.jsontoo). The invoice endpointsGET /v1/integrations/{id}/invoices[/{invoiceId}]list their error codes by real reachability in the docs and in OpenAPI -integration_storage_errorwas removed, it never came from the API - and their query validation returns the human-readable message like the rest of v1 (same forGET .../events). Additive only - no status or code changed. - 8 Sep 2026 (1.4.2): Fixed Apple event reads and confirmed updates: iCloud rejects UID searches, so event lists now return opaque CalDAV resource IDs. Pass externalId unchanged as eventId; reload any Apple event IDs saved from earlier versions. Invalid Apple IDs return 400 calendar_invalid_query. Google identifiers are unchanged.
- 8 Sep 2026 (1.4.1):
POST /v1/agentsandcreate_agentacceptuseForOutboundTasks(choosing volai's own voice engine at creation, the same choice as the portal checkbox "Use for outbound tasks"; omitted = the deployment default, today ElevenLabs). Additive only - no status or code changed. - 8 Sep 2026 (1.4.0): Apple Calendar shares the same calendar flow over REST and MCP as Google. Fakturoid and ABRA Flexi added
GET /v1/integrations/{id}/invoices[/{invoiceId}]plus thelist_invoicesandget_invoicetools (the interface only reads invoices).GET /v1/integrationsandlist_integrationsalso returnproviderswith setup readiness and a browser connection URL. Provider credentials still require connection in the portal. Additive only - no status or code changed. - 7 Sep 2026 (1.3.0): Google Calendar entered the public interface -
GET /v1/integrations,GET /v1/integrations/{id}/calendars,GET /v1/integrations/{id}/events,GET/PATCH /v1/integrations/{id}/events/{eventId}and six MCP tools (list_integrations,list_calendars,list_calendar_events,get_calendar_event,propose_calendar_update,confirm_calendar_update), which takes REST to 50 endpoints and MCP to 42 tools. Every error response also carriescause(request/account/busy/service- who fixes it) andaction(one sentence on what to do).validationmessages name the field, the actual value and the accepted maximum instead of the raw validator text; the input caps for an agent's name, instructions, goal and knowledge are ten times the product limits, so the limit is always reported by the product error with the exact number. MCP errors carry the same fields instructuredContent.error(includingrequestId) and a text prefixed by cause. Additive only, no status or code changed. - 6 Sep 2026 (1.2.0): The
/v1/agents/{id}/draft*and/simulateendpoints lost theokwrapper and plain-string errors - errors now share the same shape,{error:{code,message,requestId,docsUrl}}, as the rest of v1;revisionin request bodies was renamed toexpectedRevision; addedhasMore/nextBeforepagination fields,X-Request-IdandRateLimit-*headers,POST /v1/webhook/test,GET /v1/webhook/deliveries, andGET /openapi.json. The draft endpoints were already public from 5 Sep 2026, but without a stable contract - they are therefore excluded from the versioning guarantee (rule 1 of the Versioning section) up to and including 1.2.0.
Pełna, stale aktualizowana lista znajduje się na stronie Dziennik zmian (dostępna również jako kanał RSS).
Konwencje
Zasady obowiązujące we wszystkich endpointach - nie tylko w jednym z nich.
Identyfikatory
Każdy identyfikator ma prefiks wskazujący typ rekordu i poza tym jest nieprzezroczysty (nie należy go dalej analizować):
vk_ klucz API, ag_ agent, ad_ wersja robocza agenta, ado_ operacja na wersji roboczej, c_ połączenie, msg_ wiadomość SMS, tl_ narzędzie, whd_ dostarczenie webhooka, rl_ dzierżawa relay, int_ połączona integracja, inv_ dokument podatkowy (GET /v1/billing/documents), l_ wpis w księdze salda, task_ zadanie wychodzące, item_ odbiorca zadania, attempt_ próba połączenia na pozycji zadania, req_ identyfikator żądania (zob. Format błędu powyżej).
Znaczniki czasu
Znaczniki czasu są prawie wszędzie milisekundami uniksowymi (createdAt, startedAt, boughtAt, issuedAt, taxableAt) - WYJĄTKIEM są pola połączonego kalendarza ORAZ połączonych faktur: start, end, fetchedAt (kalendarz) i parametry timeMin/timeMax to ciągi RFC 3339 (2026-09-08T10:00:00.000Z), nigdy liczby; GET /v1/integrations/{id}/invoices i .../invoices/{externalId} również zwracają ciągi - fetchedAt ma ten sam kształt RFC 3339, a dueDate to zwykła data (2026-09-08) wprost z Fakturoid/ABRA Flexi albo null, gdy dostawca jej nie podał. Drugim wyjątkiem jest abonament miesięczny: periodStart i periodEnd (/v1/subscription, /v1/balance) to ciągi ISO 8601 w UTC, a nie milisekundy.
Stronicowanie - trzy schematy oraz listy bez żadnego
GET /v1/messages, GET /v1/calls i GET /v1/billing/documents przyjmują limit + before (znacznik czasu w ms, wyłączny) i odpowiadają hasMore/nextBefore - szczegóły w sekcji Stronicowanie powyżej. Wydarzenia połączonego kalendarza (GET /v1/integrations/{id}/events) używają zamiast tego nieprzezroczystego pageToken/nextPageToken - timeMin/timeMax/search muszą pozostać niezmienione między stronami, a sam pageToken tego nie gwarantuje. GET /v1/credit/ledger przyjmuje tylko limit (1-200, domyślnie 50) i w ogóle nie ma kursora - zawsze zaczyna od najnowszego wpisu. GET /v1/tasks bez ids zwraca najwyżej 100 ostatnich zadań; ids odczytuje do pięciu wskazanych zadań na żądanie, nie rościjąc sobie prawa do pełnego spisu zasobów najemcy. GET /v1/webhook/deliveries również nie ma kursora i zwraca najwyżej ostatnie 50 dostarczeń (udanych, nieudanych i testowych). Wyszukiwanie połączonych faktur też po cichu ma limit - pierwsze 40 wierszy z Fakturoid lub 50 z ABRA Flexi, bez kursora do dalszych; zamiast tego należy zawęzić wyszukiwanie. Wszystko inne, co zwraca listę (agenci, numery, numery komórkowe, narzędzia, klucze API, lista zakazu dzwonienia, głosy), zwraca cały zbiór w jednej odpowiedzi - nie ma czego stronicować.
Nieznane parametry i parametry o czeskich nazwach
Nieznany parametr zapytania jest prawie wszędzie po cichu ignorowany (żeby starszy klient nie psuł się przez jeden dodatkowy parametr) - wyjątkiem są endpointy połączonego kalendarza i połączonych faktur (/v1/integrations/{id}/...), które odrzucają nieznany parametr błędem 400 validation, ponieważ przekazują go dalej do dostawcy, a po cichu odrzucony filtr wyglądałby tam na brakujące dane, a nie na błąd; oraz, z niezwiązanego powodu, GET /v1/credit/ledger i GET /v1/billing/documents, których schematy zapytań są celowo ścisłe, aby literówka w limit/before kończyła się głośnym błędem, a nie cichym zignorowaniem.
Kilka pól celowo zachowuje czeską nazwę: przepływ adresu numeru (GET /v1/numbers/address-options, następnie POST /v1/numbers/orders - zwykłe POST /v1/numbers przyjmuje tylko e164/region i ignoruje adres) przekazuje psc, obec, cobce, ulice (tylko tam, gdzie adres ma ulicę) i cp - dokładne nazwy pól rejestru RÚIAN (własnej listy kodów adresowych operatora), z którego zamawiany jest numer, dzięki czemu wartość przekazywana tam i z powrotem nigdy nie zostanie po cichu błędnie przetłumaczona. GET /v1/calls/{id}/recording?stahnout=1 to jedyny parametr o czeskiej nazwie poza tym przepływem (pobranie jako załącznik zamiast wyświetlenia w przeglądarce). Cała reszta - identyfikatory, pola i parametry - jest po angielsku.
Limity zebrane w jednym miejscu
Każdy limit jest też wspomniany przy swoim endpoincie powyżej - to tylko zestawienie:
- 60 żądań/min na klucz API (REST i MCP dzielą go, zob. Limity zapytań powyżej)
- SMS: 1 na 2 s i 100/dzień na konto, dopiero po pierwszym doładowaniu salda
- SMS wysłane z numeru komórkowego (
fromNumber): dodatkowo 20 wiadomości dziennie na konto w pierwszych 30 dniach po zakupie numeru, potem 50; najwyżej dwa numery komórkowe na konto - Połączenia wychodzące (agent, most, próbne): 10/min i 1000/dzień na konto, jednocześnie najwyżej 2 połączenia wbudowanego agenta
- Dzwonki (
POST /v1/rings): 30/min i 500/dzień na konto oraz 120/godzinę na jeden numer, niezależnie od limitów połączeń - Wiadomości głosowe (
POST /v1/voice-messages): najwyżej 200 na 24 godziny na konto, oprócz zwykłych limitów połączeń - Połączenie próbne bez własnego numeru (
POST /v1/callszsystemPromptzamiastagentId/from): dodatkowo 3/24 h na konto ORAZ wspólny limit 30/godzinę dla WSZYSTKICH kont łącznie - wspólny limit może Państwa dotknąć bez Państwa winy. POST /v1/agents/{id}/test-call: 3/dzień na kontoPOST /v1/tools/testi.../{id}/test: 10/min na kontoPOST /v1/agents/{id}/simulate: 20/godzinę na kontoPOST /v1/webhook/test: 10/godzinę na kontoGET /v1/changelog: 30/min na adres IP (bez klucza API)- Zadania: uruchomione zadanie dzwoni w jednym takcie crona do najwyżej JEDNEGO odbiorcy. Kolejka ogólna pozostaje na
*/2 * * * *i obejmuje najwyżej 20 zadań ze wszystkich kont; zarejestrowane zadania PetCenter używają osobnego taktu* * * * *i kolejki, z tymi samymi limitami współbieżności i tempa połączeń. Obie kolejki obracają się sprawiedliwie, gdy czeka więcej niż 20 zadań.
Ponowienia webhooków
Nieudane dostarczenie dostaje do 3 prób w odstępie 3 sekund w ramach tej samej wysyłki. Dla zdarzeń połączeń (call.completed, call.failed, call.no_answer, call.missed) oraz dla message.received to nie koniec: zdarzenie pozostaje w kolejce, a dostarczenie jest ponawiane jeszcze kilka razy w ciągu następnych minut, łącznie do 5 rund - w rzadkim przypadku, gdy dotrze więcej niż raz, należy deduplikować po event_id z treści. message.sent i zdarzenie testowe webhook.test nie mają kolejki: po niepowodzeniu jest ono tylko zapisywane w logu i nie jest wysyłane ponownie. Postęp, w tym rundy przed ostatnią, można sprawdzić przez GET /v1/webhook/deliveries (lub list_webhook_deliveries) - attempts oznacza tam sumę prób ze WSZYSTKICH rund.
Wycofywanie
v1 dotąd tylko się rozszerzało (zob. Wersjonowanie powyżej) - jedynym wyjątkiem była wersja 1.5.1, która przestała zwracać answeredBy dla połączeń PRZYCHODZĄCYCH (pole nigdy nie miało tam sensownej wartości; dla połączeń wychodzących bez zmian). volai nie wycofało jeszcze w pełni żadnego pola. Gdy to nastąpi, pole zostanie oznaczone deprecated: true w GET /openapi.json, a odpowiedź będzie zawierać nagłówki Deprecation/Sunset z wyprzedzeniem co najmniej 90 dni, zaś odpowiedź MCP, która go dotyka, dostanie wpis w warnings (to samo pole, którego dziś używa się do ostrzeżenia o ujawnieniu AI Act) - samo usunięcie trafia zawsze wyłącznie w nowej wersji głównej (v2), nigdy po cichu w v1.
Połączenia a rozmowy - przed zsumowaniem liczników zob. akapit o tej samej nazwie w sekcji Połączenia poniżej.
Dane logowania (hasło, token, hasło do aplikacji) należy przekazywać wyłącznie wtedy, gdy użytkownik umieścił je już w miejscu, które można odczytać (zmienna środowiskowa lub plik, który wskazał) - nigdy nie należy o nie prosić w rozmowie ani ich powtarzać; dziś dotyczy to Kalendarza Apple i ABRA Flexi (zob. sekcje kalendarza i faktur poniżej).
Czego API nie potrafi
Całą stroną telefoniczną sterują Państwo przez API i MCP - numery, połączenia, SMS, agenci, webhooki, relay i lista zakazu dzwonienia. Profil, adres do faktury i dane firmy również obsługuje API (sekcja Konto powyżej). Kilka rzeczy z założenia pozostaje wyłącznie w portalu:
| Co | Gdzie to jest i dlaczego |
|---|---|
| Doładowanie salda | Portal /kredit (zarówno jednorazowe doładowania, jak i automatyczne). Płatność kartą jest krokiem człowieka - agent, który sam doładowuje saldo, by móc dalej dzwonić, to dokładnie to, czego nie chcemy. Gdy działanie trafi na insufficient_credit, należy poinformować użytkownika i odesłać go tutaj. |
| Tworzenie nowego klucza API | Portal /api-a-mcp. Wyświetlanie listy (GET /v1/api-keys) i unieważnianie WŁASNEGO klucza (DELETE /v1/api-keys/{id}) odbywa się przez API - nie dotyczy to tylko tworzenia nowego klucza: klucz tworzący kolejny klucz oznaczałby, że jeden wyciekły klucz daje trwały dostęp, którego nigdy nie da się unieważnić. |
| Zmiana hasła | Portal /nastaveni. Imię i nazwisko, język konta i adres do faktury obsługują PATCH /v1/account i PUT /v1/account/billing (sekcja Konto powyżej) - hasła nie, to czysto przeglądarkowe działanie z własnym potwierdzeniem. |
| Faktury podatkowe | Portal /kredit, sekcja Faktury - dla każdego doładowania salda czeka tam plik PDF do pobrania; ten sam PDF pobiera GET /v1/billing/documents/{id}/pdf. API nie może wystawić faktury: to dokument księgowy z własną numeracją ciągłą, który po wystawieniu nigdy się nie zmienia i nie wolno go tworzyć dwa razy. |
| SMS na numerach głosowych | Nigdzie - głosowe numery volai nie odbierają SMS. SMS odbiera dodatek numer komórkowy oraz produkt "numer do połączeń i SMS", zob. sekcje Numery komórkowe i Numer do połączeń i SMS poniżej. |
W drugą stronę brakuje celowo więcej niż jednej rzeczy: zwolnienie kupionego numeru (DELETE /v1/numbers/{e164}), numeru komórkowego (DELETE /v1/sms-numbers/{e164}) lub produktu "numer do połączeń i SMS" (DELETE /v1/voice-sms-numbers/{e164}) istnieje zarówno w REST API, jak i w portalu, ale nie ma go wśród narzędzi MCP - nieodwracalne działanie jest tam o jedno źle zrozumiane zdanie od zbyt dużego ryzyka. Celowo brakuje też: pobierania nagrania (treść binarna, GET /v1/calls/{id}/recording), szczegółów pojedynczej wiadomości lub narzędzia (listy list_messages/list_tools zawierają już te same pola, tylko nie dla jednego ID naraz) oraz osobnego narzędzia do połączenia testowego - make_call z agentId wykonuje to samo połączenie co POST /v1/agents/{id}/test-call, tylko bez jego surowszego dziennego limitu 3 połączeń.
Konto
Profil, adres alarmowy, dane do faktury i przegląd kluczy API. Tworzenie nowych kluczy API (POST /v1/api-keys) nie jest udostępnione - klucz, który tworzy klucze, zamieniłby utratę jednego z nich w trwały dostęp. Nowy klucz utworzą Państwo w portalu (sekcja Dostęp i API). Powiadomienia e-mail (callSummary, lowCredit, reactivation, productUpdates) są w tym interfejsie tylko DO ODCZYTU - zapis jest możliwy wyłącznie w portalu, ponieważ lowCredit to jedyny hamulec między wyczerpaniem salda a cichym wyłączeniem usługi.
GET/v1/account
Profil, adres alarmowy, dane do faktury i stan powiadomień e-mail (tylko do odczytu).
Żądanie
curl https://volai.cz/v1/account \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"account": {
"id": "u_8f2a1c9d3b47",
"email": "radek@example.com",
"name": "Radek Klein",
"locale": "cs",
"emailVerified": true,
"createdAt": 1755000000000,
"companyName": "Ukázková s.r.o.",
"address": { "street": "Na Zámku 636", "city": "Nehvizdy", "zip": "250 81" },
"billing": null,
"notifications": { "callSummary": true, "lowCredit": true, "reactivation": true, "productUpdates": true, "weeklySummary": true }
},
"testCalls": { "max": 3, "remaining": 2, "resetAt": 1755086400000 }
}Stany błędów
- 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
PATCH/v1/account
Aktualizuje nazwę i język konta. locale określa język e-maili i dokumentów podatkowych, NIE język tej odpowiedzi - REST i MCP zawsze odpowiadają po angielsku. Wymagane jest co najmniej jedno pole.
Żądanie
curl -X PATCH https://volai.cz/v1/account \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"locale":"en"}'Odpowiedź
{
"account": {
"id": "u_8f2a1c9d3b47",
"email": "radek@example.com",
"name": "Radek Klein",
"locale": "en",
"emailVerified": true,
"createdAt": 1755000000000,
"companyName": "Ukázková s.r.o.",
"address": { "street": "Na Zámku 636", "city": "Nehvizdy", "zip": "250 81" },
"billing": null,
"notifications": { "callSummary": true, "lowCredit": true, "reactivation": true, "productUpdates": true, "weeklySummary": true }
}
}Stany błędów
- 400
validationTreść żądania nie jest prawidłowym obiektem JSON, nie zawiera żadnego pola do aktualizacji,namejest pustym ciągiem lub ma więcej niż 200 znaków, albolocalenie jest anics, anien. - 400
invalid_nameNazwa po usunięciu spacji z początku i końca jest pusta. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
PUT/v1/account/billing
Zapisuje adres alarmowy i dane do faktury. address, jeśli jest obecny, zawiera ulicę, miasto i kod pocztowy naraz - żadnego z nich nie można wysłać osobno. company, ico i dic są ŁĄCZONE z tym, co już zapisano: brakujące pole pozostaje BEZ ZMIAN, wyczyścić je może tylko jawny pusty ciąg ("") (w przypadku dic wyczyszczenie usuwa też jego weryfikację w VIES). Zagraniczny numer VAT UE jest weryfikowany w rejestrze VIES; pole vies w odpowiedzi jest wypełnione tylko wtedy, gdy wynik zawiera zweryfikowany numer VAT (świeżo lub z zachowanej wcześniejszej weryfikacji). Krajowy numer VAT (CZ...) nie jest weryfikowany - nie wpływa na stawkę.
Żądanie
curl -X PUT https://volai.cz/v1/account/billing \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"company":"Ukázková s.r.o.","dic":"SK9999999999","address":{"street":"Na Zámku 636","city":"Nehvizdy","zip":"250 81"}}'Odpowiedź
{
"billing": {
"company": "Ukázková s.r.o.",
"ico": null,
"dic": "SK9999999999",
"dicCountry": "SK",
"dicVerified": true,
"dicName": "Ukázková s.r.o."
},
"address": { "street": "Na Zámku 636", "city": "Nehvizdy", "zip": "250 81" },
"vies": { "dic": "SK9999999999", "country": "Slovakia", "name": "Ukázková s.r.o." }
}Stany błędów
- 400
validationTreść żądania nie jest prawidłowym obiektem JSON, zawiera nieznane pole (schemat jest ścisły),company/ico/dicma więcej niż 120/20/20 znaków,addresszawiera nieznane pole,address.streetlubaddress.cityjest puste albo ma więcej niż 200 znaków, lubaddress.zipma mniej niż 3 albo więcej niż 10 znaków. - 400
invalid_addressUlica lub miasto po usunięciu spacji z początku i końca okazują się puste (na przykład pole złożone z samych spacji), albo kod pocztowy nie przechodzi normalizacji - nie wygląda jak kod pocztowy. - 400
invalid_icoSuma kontrolna numeru firmy (IČO) się nie zgadza. - 400
invalid_dicNumer VAT nie ma prawidłowego kształtu (krajowyCZplus 8-10 cyfr albo kod kraju zagranicznego plus numer z rejestru). - 400
foreign_vat_unsupportedKod kraju numeru VAT nie należy do państwa członkowskiego UE - odwrotne obciążenie obowiązuje tylko w UE. - 400
vat_id_not_foundEuropejski rejestr VAT (VIES) nie zna tego numeru VAT. Bez weryfikacji naliczany jest czeski VAT. - 503
vat_registry_unavailableRejestr VIES jest tymczasowo niedostępny - prosimy spróbować później; wcześniejsza weryfikacja (jeśli była) pozostaje w mocy. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
POST/v1/account/cancellation-request
Prosi o zamknięcie konta, tak samo jak formularz Zamknij konto w Ustawieniach. reason jest wymagane (too_expensive, callers_hang_up, missing_feature, not_needed lub other), note to opcjonalna notatka do 500 znaków. Prośba NIE zmienia konta i niczego nie usuwa: jest zapisywana, zespół volai dostaje alert z powodem, a klient potwierdzenie e-mailem w języku konta; konto jest następnie zawieszane ręcznie. Ponowne wysłanie jest bezpieczne: zwraca pierwotną prośbę z alreadyRequested: true i nie wysyła kolejnego e-maila.
Żądanie
curl -X POST https://volai.cz/v1/account/cancellation-request \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"reason":"callers_hang_up","note":"Volající zavěšují po první větě."}'Odpowiedź
{
"cancellationRequest": {
"reason": "callers_hang_up",
"note": "Volající zavěšují po první větě.",
"requestedAt": 1793520000000,
"alreadyRequested": false
}
}Stany błędów
- 400
validationTreść żądania nie jest prawidłowym obiektem JSON, zawiera nieznane pole (schemat jest ścisły), brakujereasonalbo nie jest jedną z pięciu wartości, lubnotema więcej niż 500 znaków. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
GET/v1/api-keys
Klucze API na koncie. id to tylko pierwszych 12 znaków odcisku klucza - pełny odcisk nigdy nie jest zwracany.
Żądanie
curl https://volai.cz/v1/api-keys \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"apiKeys": [
{
"id": "3f9a2c8e1b04",
"label": "Production integration",
"prefix": "vk_8h2j4kx",
"createdAt": 1750000000000,
"lastUsedAt": 1757280000000
}
]
}DELETE/v1/api-keys/{id}
Samounieważnienie: przez API klucz może unieważnić tylko SIEBIE, nigdy innego klucza na koncie - w przeciwnym razie skradziony klucz odciąłby wszystkie pozostałe integracje, sam pozostając aktywny. Unieważnianie innych kluczy jest możliwe tylko w portalu. Po unieważnieniu właściciel konta dostaje e-mail, niezależnie od tego, kto o to poprosił.
Żądanie
curl -X DELETE https://volai.cz/v1/api-keys/3f9a2c8e1b04 \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"revoked": true
}Stany błędów
- 404
not_foundNa koncie nie ma klucza o tymid. - 403
api_key_self_onlyidw ścieżce należy do innego klucza niż ten, który uwierzytelnia to żądanie - przez API można unieważnić tylko siebie.
Saldo
balanceHal i każda stawka w tym API są podane bez VAT - volai jest płatnikiem VAT, ale VAT jest naliczany dopiero przy doładowaniu salda kartą w portalu (zob. stronę /cenik), samo API go nie uwzględnia. Pełny opis z przykładami przepływów znajduje się pod /docs/kredit.
GET/v1/balance
Bieżące saldo konta oraz runway (szacunek, na ile dni wystarczy saldo przy obecnym tempie wydatków) i notice (to samo powiadomienie, które widać na stronie przeglądu w portalu, null, gdy go nie ma) - oba DODATKOWE, więc klient czytający tylko balanceHal/balanceCzk/currency nadal działa. Także topupBalanceHal (część z doładowań, ujemna, gdy konto jest zadłużone) i subscription (abonament miesięczny albo null): balanceHal pozostaje SUMĄ salda z abonamentu miesięcznego i salda z doładowań.
Żądanie
curl https://volai.cz/v1/balance \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"balanceHal": 8730,
"balanceCzk": 87.30,
"currency": "CZK",
"runway": { "dailyAverageHal": 2910, "runwayDays": 3 },
"notice": { "kind": "runway", "days": 3 },
"topupBalanceHal": 8730,
"subscription": null
}Stany błędów
- 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
GET/v1/credit/ledger
Historia ruchów na saldzie, od najnowszych, BEZ kursora (tylko limit, 1-200, domyślnie 50). description każdego wpisu to angielski opis SKŁADANY z type - zapisana czeska notatka nigdy nie wychodzi na zewnątrz.
Żądanie
curl "https://volai.cz/v1/credit/ledger?limit=3" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"ledger": [
{ "id": "l_9f2a1c8e", "ts": 1757280000000, "type": "call_out", "amountHal": -420, "grossHal": null, "description": "Outbound call" },
{ "id": "l_7c1e9a2f", "ts": 1757193600000, "type": "number_fee", "amountHal": -9900, "grossHal": null, "description": "Monthly number fee for +420601234567" },
{ "id": "l_3b7e1c9a", "ts": 1757107200000, "type": "topup", "amountHal": 50000, "grossHal": 60500, "description": "Card top-up" }
]
}Stany błędów
- 400
validationlimitjest poza zakresem 1-200 albo żądanie zawiera dodatkowy nieznany parametr. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
POST/v1/credit/topup
Tworzy link Stripe Checkout do jednorazowego doładowania o stałej kwocie w CZK. Nic nie jest pobierane, dopóki CZŁOWIEK nie dokończy płatności w przeglądarce. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/credit/topup \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: topup-2026-09-08-01" \
-d '{"amountCzk": 500}'Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 400
validationamountCzkbrakuje, nie jest dodatnią liczbą całkowitą albo treść zawiera nieznane pole. - 400
invalid_amountKwota nie należy do stałych oferowanych wartości. - 400
billing_address_requiredAdres do faktury konta brakuje lub jest niekompletny - bez niego nie można wystawić prawidłowego dokumentu podatkowego. Należy go uzupełnić przezPUT /v1/account/billing. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę.
GET/v1/credit/auto-topup
Stan automatycznego doładowania: włączone/wyłączone, próg, kwota, zapisana karta (tylko marka i cztery ostatnie cyfry), wydatki i limit w tym miesiącu kalendarzowym.
Żądanie
curl https://volai.cz/v1/credit/auto-topup \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"autoTopup": {
"enabled": true,
"thresholdHal": 10000,
"amountHal": 50000,
"card": { "brand": "visa", "last4": "4242" },
"spentThisMonthHal": 50000,
"monthlyCapHal": 1000000,
"disabledReason": null
}
}PATCH/v1/credit/auto-topup
WYŁĄCZNIE wyłączenie ({"enabled": false}) i/lub zmiana progu ({"thresholdHal": ...}) - włączenie lub zmiana kwoty odbywa się tylko przez POST .../setup (Stripe Checkout), nigdy bezpośrednim zapisem, więc zwykłe enabled: true nigdy nie może wywołać obciążenia zapisanej karty bez udziału człowieka.
Żądanie
curl -X PATCH https://volai.cz/v1/credit/auto-topup \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"thresholdHal": 20000}'Odpowiedź
{
"autoTopup": {
"enabled": true,
"thresholdHal": 20000,
"amountHal": 50000,
"card": { "brand": "visa", "last4": "4242" },
"spentThisMonthHal": 50000,
"monthlyCapHal": 1000000,
"disabledReason": null
}
}Stany błędów
- 400
validationTreść zawieraenabled: truelubamountHal(oba należą tylko doPOST .../setup),thresholdHaljest poza oferowanym zakresem albo treść nie zawiera anienabled, anithresholdHal. - 400
autotopup_not_configuredAutomatyczne doładowanie nigdy nie przeszło przezPOST .../setup- nie ma czego wyłączać, zmieniać ani w czym wymieniać karty. - 400
invalid_amountKwota nie należy do stałych oferowanych wartości.
POST/v1/credit/auto-topup/setup
Tworzy sesję Stripe Checkout, której ukończenie NATYCHMIAST pobiera amountHal jako pierwsze doładowanie i dopiero to zapisuje kartę dla przyszłych automatycznych doładowań - działa także przy karcie zapisanej wcześniej. Jedyny sposób na włączenie automatycznego doładowania lub zmianę jego kwoty; zmiana samego progu bez obciążenia to PATCH powyżej. Adres do faktury jest wymagany. Jest odrzucane z subscription_cap_active, gdy działa abonament miesięczny z limitem wydatków. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/credit/auto-topup/setup \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: autotopup-setup-2026-09-08-01" \
-d '{"thresholdHal": 10000, "amountHal": 50000}'Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 400
validationthresholdHallubamountHalbrakuje, nie jest nieujemną/dodatnią liczbą całkowitą albo treść zawiera nieznane pole. - 400
invalid_amountKwota nie należy do stałych oferowanych wartości. - 400
billing_address_requiredAdres do faktury konta brakuje lub jest niekompletny - bez niego nie można wystawić prawidłowego dokumentu podatkowego. Należy go uzupełnić przezPUT /v1/account/billing. - 409
subscription_cap_activeAutomatycznego doładowania nie można włączyć, dopóki abonament działa z limitem wydatków. Należy przełączyć abonament na doładowania z karty (PATCH /v1/subscriptionzwraca link potwierdzenia) albo zachować limit. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę.
POST/v1/credit/auto-topup/card
Tworzy sesję Stripe Checkout w trybie zapisu karty dla automatycznego doładowania - nic nie jest pobierane, zapisywana jest tylko nowa karta.
Żądanie
curl -X POST https://volai.cz/v1/credit/auto-topup/card \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 400
autotopup_not_configuredAutomatyczne doładowanie nigdy nie przeszło przezPOST .../setup- nie ma czego wyłączać, zmieniać ani w czym wymieniać karty. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę.
DELETE/v1/credit/auto-topup/card
Odłącza zapisaną kartę NATYCHMIAST - usuwana jest CAŁA konfiguracja automatycznego doładowania (wraz z progiem i kwotą), nie tylko karta. Ponowne włączenie działa wyłącznie przez POST .../setup, które od razu pobiera pierwsze doładowanie. Dopóki działający abonament miesięczny płaci tą samą kartą, wywołanie jest odrzucane z invalid_state: zamiast tego należy wymienić kartę przez POST /v1/subscription/card.
Żądanie
curl -X DELETE https://volai.cz/v1/credit/auto-topup/card \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"removed": true
}Stany błędów
- 400
invalid_stateKarta jest jednocześnie kartą działającego abonamentu miesięcznego, więc samo jej odłączenie spowodowałoby niepowodzenie następnego odnowienia. Należy ją wymienić przezPOST /v1/subscription/card; po zakończeniu abonamentu można ją odłączyć osobno.
GET/v1/account/spend-cap
Miesięczny limit wydatków: czy jest ustawiony, capHal, warnShare, inboundAboveCap oraz wydatki tego miesiąca kalendarzowego na połączenia, SMS i agenta głosowego (monthSpendHal, miesiąc YYYY-MM według czasu praskiego), pozostała kwota i czy limit został reached. Wydatki liczone są na każdym koncie, także bez limitu; opłaty za numery i abonament miesięczny nie są wliczane.
Żądanie
curl https://volai.cz/v1/account/spend-cap \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"spendCap": {
"enabled": true,
"capHal": 500000,
"warnShare": 0.8,
"inboundAboveCap": false,
"month": "2026-11",
"monthSpendHal": 183400,
"remainingHal": 316600,
"reached": false,
"updatedAt": 1793520000000
}
}Stany błędów
- 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
PUT/v1/account/spend-cap
Ustawia lub zmienia miesięczny limit wydatków: capHal w pełnych halerzach (od 50 do 1 000 000 CZK), opcjonalnie warnShare (0,5 do 0,99, domyślnie 0,8) dla e-maila z ostrzeżeniem oraz opcjonalnie inboundAboveCap (domyślnie false). Gdy wydatki miesiąca osiągną limit, nowe połączenia wychodzące, dzwonki, połączenia próbne i SMS kończą się błędem spend_cap_reached (HTTP 402), a połączenia przychodzące do Państwa agentów kończą się po jednym zdaniu o tymczasowej niedostępności linii (endReason: spend_cap, cena 0), chyba że inboundAboveCap ma wartość true. Połączenia już trwające nie są przerywane, więc wydatki mogą przekroczyć limit o ich koszt. Przekierowanie bez agenta nie jest zatrzymywane przez limit, a linie SIP konta ponad limitem opuszczają proxy SIP (połączenia przychodzące do Państwa własnej centrali też się zatrzymują), chyba że inboundAboveCap ma wartość true; przy inboundAboveCap: true linia pozostaje w proxy, więc limit nie zatrzymuje także połączeń wychodzących z Państwa centrali i są one nadal naliczane. Automatyczne doładowanie pozostaje bez zmian.
Żądanie
curl -X PUT https://volai.cz/v1/account/spend-cap \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"capHal": 500000, "warnShare": 0.8, "inboundAboveCap": false}'Odpowiedź
{
"spendCap": {
"enabled": true,
"capHal": 500000,
"warnShare": 0.8,
"inboundAboveCap": false,
"month": "2026-11",
"monthSpendHal": 183400,
"remainingHal": 316600,
"reached": false,
"updatedAt": 1793520000000
}
}Stany błędów
- 400
validationcapHalbrakuje, nie jest liczbą całkowitą halerzy z przedziału od 5 000 do 100 000 000,warnSharejest poza zakresem 0,5 do 0,99,inboundAboveCapnie jest wartością logiczną albo treść zawiera nieznane pole. - 502
ledger_failedNie udało się teraz zapisać limitu - nic nie zostało zmienione. Prosimy spróbować za chwilę. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
DELETE/v1/account/spend-cap
Usuwa miesięczny limit wydatków; konto zachowuje się tak jak przed jego ustawieniem. Usunięcie limitu, który nie jest ustawiony, również się udaje.
Żądanie
curl -X DELETE https://volai.cz/v1/account/spend-cap \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"spendCap": {
"enabled": false,
"capHal": null,
"warnShare": null,
"inboundAboveCap": null,
"month": "2026-11",
"monthSpendHal": 183400,
"remainingHal": null,
"reached": false,
"updatedAt": null
}
}Stany błędów
- 502
ledger_failedNie udało się teraz zapisać limitu - nic nie zostało zmienione. Prosimy spróbować za chwilę. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
GET/v1/billing/documents
Lista wystawionych dokumentów podatkowych (doładowania salda, płatności abonamentu miesięcznego, zamówienia na klucz) oraz not korygujących, od najnowszych. before to kursor (Unix ms, wyłączny) - należy wysłać nextBefore z poprzedniej strony. Magazyn danych nigdy nie zwraca więcej niż 100 rekordów na wywołanie, nawet gdy limit żąda więcej.
Żądanie
curl "https://volai.cz/v1/billing/documents?limit=2" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"documents": [
{
"id": "inv_4kX9mQ2pRtLz",
"number": "VF2026000042",
"issuedAt": 1757280000000,
"taxableAt": 1757280000000,
"netHal": 50000,
"vatHal": 10500,
"grossHal": 60500,
"vatRatePercent": 21,
"reverseCharge": false,
"kind": "invoice",
"description": "Prepaid credit for volai telecommunications services",
"customer": { "name": "Radek Klein", "street": "Na Zámku 636", "city": "Nehvizdy", "zip": "250 81" },
"paymentRef": "pi_3PQr7s2eZvKYlo2C1a2b3c4d",
"pdfUrl": "https://volai.cz/v1/billing/documents/inv_4kX9mQ2pRtLz/pdf"
}
],
"hasMore": false,
"nextBefore": null
}Stany błędów
- 400
validationlimitlubbeforejest poza dozwolonym zakresem albo żądanie zawiera dodatkowy nieznany parametr.
GET/v1/billing/documents/{id}
Dane jednego dokumentu podatkowego - kwoty, VAT, nabywca, odniesienie do płatności i link do pliku PDF.
Żądanie
curl https://volai.cz/v1/billing/documents/inv_4kX9mQ2pRtLz \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"document": {
"id": "inv_4kX9mQ2pRtLz",
"number": "VF2026000042",
"issuedAt": 1757280000000,
"taxableAt": 1757280000000,
"netHal": 50000,
"vatHal": 10500,
"grossHal": 60500,
"vatRatePercent": 21,
"reverseCharge": false,
"kind": "invoice",
"description": "Prepaid credit for volai telecommunications services",
"customer": { "name": "Radek Klein", "street": "Na Zámku 636", "city": "Nehvizdy", "zip": "250 81" },
"paymentRef": "pi_3PQr7s2eZvKYlo2C1a2b3c4d",
"pdfUrl": "https://volai.cz/v1/billing/documents/inv_4kX9mQ2pRtLz/pdf"
}
}Stany błędów
- 404
invoice_not_foundNa koncie nie ma dokumentu o tymid(obce lub brakująceidzgłasza ten sam błąd).
GET/v1/billing/documents/{id}/pdf
Pobiera ten sam dokument jako application/pdf - jest renderowany na nowo przy każdym pobraniu, nie jest przechowywany.
Żądanie
curl https://volai.cz/v1/billing/documents/inv_4kX9mQ2pRtLz/pdf \
-H "Authorization: Bearer vk_YOUR_KEY" \
-o invoice.pdfStany błędów
- 404
invoice_not_foundNa koncie nie ma dokumentu o tymid(obce lub brakująceidzgłasza ten sam błąd).
Abonament miesięczny
Abonament miesięczny obciąża zapisaną kartę raz w miesiącu i dodaje miesięczne saldo, które jest zużywane przed saldem z doładowań i wygasa z końcem okresu. Każdy poziom ma cenę i większe miesięczne saldo, oba bez VAT (zob. tiers w GET /v1/subscription). Pieniądze ruszają tylko wtedy, gdy przy karcie jest człowiek: to API nigdy samo nie rozpoczyna ani nie zwiększa płatności cyklicznych. Rozpoczęcie abonamentu, opłacenie nieudanego odnowienia i wymiana karty zwracają link Stripe Checkout, a podniesienie poziomu, wznowienie abonamentu ustawionego na zakończenie lub przejście na doładowania z karty zwraca confirmUrl, który człowiek musi potwierdzić w portalu. Pełny opis znajduje się pod /docs/kredit.
GET/v1/subscription
Abonament miesięczny konta i wszystko, co potrzebne, by go zaproponować. subscription ma wartość null, gdy konto nigdy nie miało abonamentu; zakończony abonament jest nadal zwracany, z status: "canceled". tiers wymienia oferowane poziomy z ich priceHal i creditHal (halerze, bez VAT). topupBalanceHal to saldo z doładowań; GET /v1/balance zawiera te same dwa bloki obok sumy. available mówi, czy to konto może teraz rozpocząć NOWY abonament: oferta jest włączona dla konta, a konto nie ma działającego abonamentu (active, past_due), ponieważ drugiego nie można rozpocząć, a checkout zwróciłby subscription_exists; po zakończeniu abonamentu (canceled) znów ma wartość true. Istniejący abonament jest zawsze zwracany i nadal działa, gdy ma wartość false. recommendation to najtańszy sposób opłacenia tego, co konto faktycznie wydało w ostatnich 30 dniach (to samo obliczenie co kalkulator na stronie): spendHal, cheapestPath (per_second lub plan), najtańszy poziom z doładowaniami tier i jego koszt tierTotalHal, oszczędność savingsHal, worthIt - czy oszczędność jest wystarczająco duża, steady - czy wydatki są stałe, a nie jednorazowym szczytem (zużycie z ostatnich 14 dni przeliczone na 30 dni plus miesięczne opłaty za numery osiąga co najmniej cenę polecanego poziomu; portal oferuje abonament tylko wtedy, gdy worthIt i steady mają wartość true) oraz options, czyli każdy sposób opłacenia tych samych wydatków (za sekundę i każdy poziom z doładowaniami; te same liczby, które portal pokazuje przy zmianie poziomu abonamentu). Ma wartość null, gdy abonament nie jest oferowany kontu albo historia salda nie sięga 30 dni wstecz (nowe konto). previousPeriod podsumowuje opłacony okres poprzedzający bieżący (subscription.periodStart), z tymi samymi liczbami co e-mail o odnowieniu: tier, periodStart i periodEnd (ISO 8601), jego miesięczne saldo creditHal, ile z niego zużyto usedHal i usedPercent (zaokrąglone w dół), wydatki ponad miesięczne saldo overHal (z doładowaniami bez długu, który spłaciło następne saldo, przy limicie wraz z długiem; null, gdy historia salda nie może tego udowodnić), co wygasło expiredHal, exhaustionMode i exhaustedAt, czyli kiedy skończyło się miesięczne saldo (null, gdy się nie skończyło albo historia salda nie pokazuje kiedy). Jest zwracane tylko, dopóki abonament jest aktywny, niezależnie od available; ma wartość null bez abonamentu, przy statusie past_due lub canceled, w pierwszym okresie, gdy historia salda nie mogła udowodnić zużycia, oraz 45 dni po odnowieniu, które je zapisało.
Żądanie
curl https://volai.cz/v1/subscription \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"subscription": {
"tier": "990",
"status": "active",
"periodStart": "2026-10-17T08:00:00.000Z",
"periodEnd": "2026-11-17T09:00:00.000Z",
"monthlyCreditHal": 130000,
"monthlyRemainingHal": 98500,
"exhaustionMode": "topup",
"cancelAtPeriodEnd": false,
"nextTier": null,
"card": {
"brand": "visa",
"last4": "4242"
}
},
"tiers": [
{
"id": "490",
"priceHal": 49000,
"creditHal": 60000
},
{
"id": "990",
"priceHal": 99000,
"creditHal": 130000
},
{
"id": "1990",
"priceHal": 199000,
"creditHal": 280000
}
],
"topupBalanceHal": 8500,
"available": false,
"recommendation": {
"spendHal": 105500,
"days": 30,
"cheapestPath": "plan",
"tier": "490",
"tierTotalHal": 94500,
"savingsHal": 11000,
"worthIt": true,
"steady": true,
"options": [
{
"path": "per_second",
"tier": null,
"totalHal": 105500
},
{
"path": "plan",
"tier": "490",
"totalHal": 94500
},
{
"path": "plan",
"tier": "990",
"totalHal": 99000
},
{
"path": "plan",
"tier": "1990",
"totalHal": 199000
}
]
},
"previousPeriod": {
"tier": "990",
"periodStart": "2026-09-17T08:00:00.000Z",
"periodEnd": "2026-10-17T08:00:00.000Z",
"creditHal": 130000,
"usedHal": 105500,
"usedPercent": 81,
"overHal": 0,
"expiredHal": 24500,
"exhaustionMode": "topup",
"exhaustedAt": null
}
}Stany błędów
- 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
POST/v1/subscription/checkout
Tworzy link Stripe Checkout do pierwszego okresu abonamentu miesięcznego. Stąd nic nie jest pobierane: CZŁOWIEK płaci na stronie Stripe, karta jest zapisywana na miesięczne odnowienia i dopiero wtedy dodawane jest miesięczne saldo. exhaustionMode nie ma wartości domyślnej i trzeba go wybrać: topup kupuje saldo zapisaną kartą po wyczerpaniu salda miesięcznego (jak automatyczne doładowanie), cap niczego nie kupuje i ogranicza miesiąc do ceny abonamentu. Konto konsumenta (bez numeru firmy, numeru VAT ani nazwy firmy) nie dostaje linku Checkout: wywołanie zwraca 409 subscription_confirmation_required z confirmUrl do formularza w portalu, w którym osoba wyraźnie prosi o rozpoczęcie usługi przed upływem 14-dniowego terminu na odstąpienie od umowy, wybiera zachowanie po wyczerpaniu salda i płaci; consumerConsent nie jest już wysyłane. Adres do faktury jest wymagany. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/subscription/checkout \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: plan-2026-10-17-01" \
-d '{"tier": "990", "exhaustionMode": "cap", "consumerConsent": true}'Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 400
validationBrakujetierlubexhaustionModealbo nie są jedną z oferowanych wartości,consumerConsentnie jest wartością logiczną albo treść zawiera nieznane pole (adres powrotu nigdy nie jest akceptowany). - 404
pausal_unavailableAbonament miesięczny nie jest jeszcze oferowany temu kontu (jest wdrażany stopniowo). Ponawianie nie pomoże;availablewGET /v1/subscriptioninformuje, kiedy będzie. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 400
billing_address_requiredAdres do faktury konta brakuje lub jest niekompletny - bez niego nie można wystawić prawidłowego dokumentu podatkowego. Należy go uzupełnić przezPUT /v1/account/billing. - 409
subscription_existsKonto ma już działający abonament. Zamiast tego należy go zmienić przezPATCH /v1/subscription; zakończony abonament nie blokuje nowego. - 409
subscription_confirmation_requiredKonto konsumenta (bez numeru firmy, numeru VAT ani nazwy firmy): przez API nie jest tworzony żaden Checkout, treść błędu zawieraconfirmUrldo formularza w portalu, w którym osoba prosi o rozpoczęcie usługi i płaci. Nic nie zostało naliczone. - 400
consumer_consent_requiredTylko portal: konsument wysłał formularz bez zaznaczenia prośby o rozpoczęcie usługi. Przez API konsument dostaje zamiast tego 409subscription_confirmation_required. Nic nie zostało naliczone. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
PATCH/v1/subscription
Zmienia abonament bez obciążania karty. Te zmiany obowiązują natychmiast, ponieważ nigdy nie zwiększają kwoty, jaką wolno pobrać: obniżenie tier (wchodzi w życie od następnego okresu, widać je w nextTier), cancelAtPeriodEnd: true (brak dalszych obciążeń, saldo pozostaje do wykorzystania do periodEnd) oraz exhaustionMode: "cap" (wyłącza też automatyczne doładowanie). Podniesienie tier, cancelAtPeriodEnd: false w abonamencie ustawionym na zakończenie (czyli jego wznowienie) i exhaustionMode: "topup" NIE są stosowane: wywołanie zwraca 409 subscription_confirmation_required, a nic w żądaniu się nie zmienia. Treść błędu zawiera confirmUrl, link do strony portalu, na której osoba zalogowana na konto potwierdza zmianę; link jest ważny 24 godziny i działa raz. Zmiany wymagające potwierdzenia należy wysyłać po jednej na żądanie. Żądanie, które niczego nie zmienia (wznowienie abonamentu, który nie jest ustawiony na zakończenie, topup gdy doładowania już działają, poziom, na który abonament już się przełącza), kończy się sukcesem bez potwierdzenia i bez błędu. Tę samą treść można więc bezpiecznie wysłać ponownie po awarii: to, co już obowiązuje, jest pomijane, a wykonywane jest tylko to, co zostało. Jedynym wyjątkiem jest powrót do bieżącego poziomu (anuluje zaplanowaną zmianę): ponowienie nie ma już czego anulować i kończy się invalid_tier, więc pozostałe pola należy wysłać osobno. W abonamencie ustawionym na zakończenie sama zmiana tier zwraca invalid_state (najpierw należy cofnąć anulowanie); wysłana razem z cancelAtPeriodEnd: true jest powtórzeniem i liczy się jako wykonana.
Żądanie
curl -X PATCH https://volai.cz/v1/subscription \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tier": "490"}'Odpowiedź
{
"subscription": {
"tier": "990",
"status": "active",
"periodStart": "2026-10-17T08:00:00.000Z",
"periodEnd": "2026-11-17T09:00:00.000Z",
"monthlyCreditHal": 130000,
"monthlyRemainingHal": 98500,
"exhaustionMode": "topup",
"cancelAtPeriodEnd": false,
"nextTier": "490",
"card": {
"brand": "visa",
"last4": "4242"
}
}
}Stany błędów
- 400
validationTreść jest pusta, zawiera nieznane pole albo wartość spoza oferowanych, albo prosi w jednym żądaniu o więcej niż jedną zmianę wymagającą potwierdzenia. Nic nie zostało zmienione. - 400
invalid_tierKonto jest już na tym poziomie i nie zaplanowano żadnego innego, więc nie ma czego zmieniać. Poziom, na który konto już się przełącza od następnego okresu, nie jest błędem: liczy się jako wykonany. - 400
invalid_stateZmianatierw abonamencie ustawionym na zakończenie z końcem okresu: żaden następny okres się nie zacznie, więc poziom nigdy by nie zaczął obowiązywać. Najpierw należy cofnąć anulowanie (cancelAtPeriodEnd: false, co osoba potwierdza w portalu). Treść zawierająca takżecancelAtPeriodEnd: truejest powtórzeniem i przechodzi. - 404
subscription_missingKonto nie ma abonamentu miesięcznego - można go rozpocząć przezPOST /v1/subscription/checkout. - 409
subscription_canceledAbonament już się zakończył, więc nie można go już zmienić, wznowić ani opłacić. Nowy należy rozpocząć przezPOST /v1/subscription/checkout. - 409
subscription_confirmation_requiredZmiana zwiększa kwotę, jaką wolno pobrać z karty, więc potwierdzić ją może tylko człowiek. Nic nie zostało zmienione; błąd zawieraconfirmUrl. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
POST/v1/subscription/pay
Dla abonamentu, którego ostatnie odnowienie się nie powiodło (status: "past_due"): tworzy link Stripe Checkout, który jednorazowo pobiera kwotę odnowienia i zapisuje nową kartę; nowy okres zaczyna się od płatności. Stąd nic nie jest pobierane, CZŁOWIEK płaci na stronie Stripe. Abonament, który nie jest przeterminowany, nie ma nic do zapłacenia i zwraca invalid_state. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/subscription/pay \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Idempotency-Key: plan-pay-2026-11-18-01"Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 404
subscription_missingKonto nie ma abonamentu miesięcznego - można go rozpocząć przezPOST /v1/subscription/checkout. - 409
subscription_canceledAbonament już się zakończył, więc nie można go już zmienić, wznowić ani opłacić. Nowy należy rozpocząć przezPOST /v1/subscription/checkout. - 400
invalid_stateOstatnie odnowienie się nie nie powiodło (statusnie jestpast_due), więc nie ma czego opłacać ręcznie. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
POST/v1/subscription/card
Tworzy link Stripe Checkout w trybie zapisu karty, aby wymienić kartę, którą odnawiany jest abonament. Stąd nic nie jest pobierane, CZŁOWIEK wpisuje nową kartę na stronie Stripe; jest ona następnie używana do abonamentu i do doładowań z karty, a stara zostaje odłączona. Działa w każdym statusie abonamentu poza canceled. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/subscription/card \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Idempotency-Key: plan-card-2026-11-18-01"Odpowiedź
{
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4"
}Stany błędów
- 404
subscription_missingKonto nie ma abonamentu miesięcznego - można go rozpocząć przezPOST /v1/subscription/checkout. - 409
subscription_canceledAbonament już się zakończył, więc nie można go już zmienić, wznowić ani opłacić. Nowy należy rozpocząć przezPOST /v1/subscription/checkout. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną. - 503
stripe_not_configuredPłatności są wyłączone w tym wdrożeniu volai - nie dotyczy Państwa żądania. - 502
stripe_errorStripe odmówił utworzenia sesji płatności - prosimy spróbować za chwilę. - 503
ledger_unavailableNie udało się teraz odczytać salda ani historii ruchów - prosimy spróbować za chwilę.
Zadania
Zadanie dzwoni do listy odbiorców jednym agentem, według reguł ustalonych z góry (budżet, okno dzwonienia, liczba prób) - nic nie jest wykręcane, dopóki sami go nie uruchomią Państwo (POST .../start). Pełny przewodnik z importem CSV, statusami i globalną przepustowością znajduje się pod /docs/ukoly. Agent z produktem "numer do połączeń i SMS" dzwoni w kampanii wyłącznie na numery czeskie (+420) i z numeru trwa naraz tylko jedno połączenie: kontakt z jakimkolwiek innym numerem trafia do przeglądu, podczas gdy zadanie dzwoni dalej do pozostałych, a zajęty numer sprawia, że zadanie chwilę czeka.
GET/v1/tasks
Bez ids wyświetla do 100 ostatnich zadań. Z ids=id1,id2 odczytuje od jednego do pięciu unikalnych zadań należących do konta i zwraca requestedIdsComplete: true, requestedIds i hasMore: false; nieznane lub cudze ID zwracają wszystkie ten sam błąd 404. Wybrana odpowiedź jest ograniczona do 4 MiB; po task_inventory_payload_limit należy podzielić ID na mniejsze grupy. Jeśli pojedyncze zadanie samo przekracza limit, należy sprawdzić je przez GET /v1/tasks/{id} lub skontaktować się z pomocą techniczną; nie należy zmieniać jego niezmiennej listy odbiorców, by obejść limit odpowiedzi.
Żądanie
curl https://volai.cz/v1/tasks \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"tasks": [
{
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 50000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "draft",
"revision": 0,
"createdAt": 1757280000000,
"updatedAt": 1757280000000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [] }
]
}
]
}Stany błędów
- 400
validationParametridsmusi zawierać od jednego do pięciu unikalnych prawidłowych ID zadań wychodzących i może wystąpić tylko raz. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 413
task_inventory_payload_limitWybrane rekordy zadań przekraczają 4 MiB. Należy podzielić ID na mniejsze grupy i spróbować ponownie; jeśli pojedyncze zadanie samo przekracza limit, należy sprawdzić je przez GET /v1/tasks/{id} lub skontaktować się z pomocą techniczną. Nie należy zmieniać jego niezmiennej listy odbiorców, by obejść ten limit.
GET/v1/outbound/dispatch-scopes/petcenter/registrations
Odczyt operacyjny tylko dla właściciela dla zakresu wysyłki wychodzącej PetCenter. Bez operationId zwraca podsumowanie bieżącego zarejestrowanego manifestu; z operationId zwraca trwały wynik tej operacji po niepewnym POST. Ten endpoint nie może zmienić składu zadań.
Żądanie
curl https://volai.cz/v1/outbound/dispatch-scopes/petcenter/registrations \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{"registration":null,"operation":null}Stany błędów
- 400
validationID operacji, skrót manifestu lub lista zadań ma nieprawidłowy kształt albo koliduje z zarejestrowanym manifestem tylko do dopisywania.
POST/v1/outbound/dispatch-scopes/petcenter/registrations
Rejestracja operacyjna tylko dla właściciela dla minutowego dyspozytora PetCenter. Należy wysłać stabilne operationId, SHA-256 manifestu źródłowego i uporządkowane taskIds. Serwer weryfikuje własność konta i agenta; pierwszy manifest musi odpowiadać przypiętej liście 19 zadań, a późniejsze manifesty mogą jedynie dopisywać zadania. Aby jawnie odświeżyć tylko skrót, należy wysłać też expectedManifestSha z bieżącego GET i zachować całą uporządkowaną listę zadań. Compare-and-swap odrzuca nieaktualny skrót; to pole nie może dopisywać, usuwać ani zmieniać kolejności zadań. Warunek jest częścią niezmiennej treści operacji. Ponowne użycie ID operacji z tą samą treścią jest bezpieczne; zmiana treści powoduje konflikt. To zmienia wyłącznie skład harmonogramu i nigdy nie uruchamia ani nie wznawia zadania.
Żądanie
curl https://volai.cz/v1/outbound/dispatch-scopes/petcenter/registrations \
-H "Authorization: Bearer vk_YOUR_KEY" -H "Content-Type: application/json" \
--data-binary @petcenter-manifest-operation.jsonOdpowiedź
{"registration":{"manifestSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","taskCount":19,"updatedAt":1791802800000},"operation":{"operationId":"registration-2026-10-12","manifestSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","taskCount":19,"acceptedAt":1791802800000,"replayed":false}}Stany błędów
- 400
validationID operacji, skrót manifestu lub lista zadań ma nieprawidłowy kształt albo koliduje z zarejestrowanym manifestem tylko do dopisywania. - 409
revision_conflictID operacji jest już powiązane z inną treścią, manifest się zmienił albo rewizja zadania jest nieaktualna. Przed rozpoczęciem nowej operacji należy odczytać operację i bieżące zadania. Tylko konflikt rejestracji zerror.details.reason: task_changed_during_registrationzawiera teżproviderActionStarted: falsei pierwotneoperationId: kontrole zatrzymały się, zanim rejestr się zmienił. Przed kolejną próbą należy odczytać rejestrację przez GET z tym samymoperationId, potwierdzićoperation: nulli odświeżyć zadania. Przekroczenie czasu lub inny konflikt nie ustanawia tego bezpiecznego warunku ponowienia.
GET/v1/outbound/dispatch-scopes/petcenter/pauses
Trwały odczyt tylko dla właściciela dla zbiorczego wstrzymania PetCenter. Należy wysłać jego operationId; odpowiedź opakowuje operację w operation i zawiera dokładny manifestSha, kanoniczny requestHash, uporządkowane ID zadań i rewizje oraz wynik każdego zatwierdzonego zadania. requestHash to SHA-256 zwartego JSON w UTF-8 z kluczami obiektów posortowanymi leksykograficznie dla {manifestSha,tasks} (kolejność tablicy zadań zostaje zachowana); operationId jest pomijane. Należy go użyć po każdym niepewnym POST, zanim zostanie utworzona kolejna operacja.
Żądanie
curl 'https://volai.cz/v1/outbound/dispatch-scopes/petcenter/pauses?operationId=pause-batch-2026-10-12-01' \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"operation": {
"operationId": "pause-batch-2026-10-12-01",
"manifestSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"requestHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"tasks": [{ "taskId": "task_abcdefgh", "revision": 12 }],
"acceptedAt": 1791802800000,
"complete": true,
"outcomes": [{ "taskId": "task_abcdefgh", "outcome": "paused", "revision": 13 }]
}
}Stany błędów
- 400
validationID operacji, skrót manifestu lub od jednego do pięciu unikalnych danych wejściowych ID zadania/rewizja mają nieprawidłowy kształt.
POST/v1/outbound/dispatch-scopes/petcenter/pauses
Operacja wstrzymania tylko dla właściciela dla jednego do pięciu aktualnie zarejestrowanych zadań PetCenter. Należy wysłać stabilne operationId, bieżący SHA manifestu i świeżo odczytaną rewizję każdego zadania. Odpowiedź opakowuje operację w operation, w tym kanoniczny skrót żądania i uporządkowane dane wejściowe zadań. Zmiana rewizji zadania i trwały wynik dla zadania są atomowe; odtworzenie tej samej operacji jest bezpieczne, a zmiana jej treści powoduje konflikt. Konflikt rewizji jest zgłaszany dla każdego zadania osobno i nie wstrzymuje tego zadania.
Żądanie
curl https://volai.cz/v1/outbound/dispatch-scopes/petcenter/pauses \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"operationId":"pause-batch-2026-10-12-01","manifestSha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","tasks":[{"taskId":"task_abcdefgh","revision":12}]}'Odpowiedź
{
"operation": {
"operationId": "pause-batch-2026-10-12-01",
"manifestSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"requestHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"tasks": [{ "taskId": "task_abcdefgh", "revision": 12 }],
"acceptedAt": 1791802800000,
"complete": true,
"outcomes": [{ "taskId": "task_abcdefgh", "outcome": "paused", "revision": 13 }]
}
}Stany błędów
- 400
validationID operacji, skrót manifestu lub od jednego do pięciu unikalnych danych wejściowych ID zadania/rewizja mają nieprawidłowy kształt. - 409
revision_conflictID operacji jest już powiązane z inną treścią, manifest się zmienił albo rewizja zadania jest nieaktualna. Przed rozpoczęciem nowej operacji należy odczytać operację i bieżące zadania. Tylko konflikt rejestracji zerror.details.reason: task_changed_during_registrationzawiera teżproviderActionStarted: falsei pierwotneoperationId: kontrole zatrzymały się, zanim rejestr się zmienił. Przed kolejną próbą należy odczytać rejestrację przez GET z tym samymoperationId, potwierdzićoperation: nulli odświeżyć zadania. Przekroczenie czasu lub inny konflikt nie ustanawia tego bezpiecznego warunku ponowienia.
POST/v1/tasks
Tworzy zadanie w statusie draft - nic nie jest wykręcane. agentId może być dowolnym aktywnym agentem, także agentem na ElevenLabs - wymóg silnika (provider: "engine", wybierany przy tworzeniu przez useForOutboundTasks: true) jest sprawdzany dopiero w POST .../start, które wtedy kończy się engine_required. Najwyżej 1000 recipients. Bez retryPolicy maxAttempts wynosi 1-3 i domyślnie 1; z opcjonalnym retryPolicy: "lorela_enterprise" wynosi 1-4 i domyślnie 4. Opcjonalne expiresAt (dodatnia bezpieczna liczba całkowita, milisekundy Unix) zapobiega nowym połączeniom w terminie końca kampanii lub po nim, także pierwszemu. Opcjonalne retryMaxAgeDays (liczba całkowita 1-30, domyślnie 5; tylko z polityką, w tym samym żądaniu) ustala horyzont ponowień: N × 24 godziny od pierwszej zliczonej próby każdego odbiorcy. Polityka, horyzont i termin są zwracane w szczegółach zadania. Obsługuje Idempotency-Key. Opcjonalne niezmienne purpose domyślnie ma wartość standard. coupon_reminder wymaga source: manual, taskType: custom, jawnego maxAttempts: 1 i expiresAt. Przed dzwonieniem zaufana bramka serwerowa weryfikuje dokładne przypomnienie; zmienne użytkownika nie mogą zastąpić jego powitania ani kuponu. Bez skonfigurowanej bramki przypomnienie nie jest wykręcane.
Żądanie
curl -X POST https://volai.cz/v1/tasks \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: task-2026-09-08-01" \
-d '{
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"maxAttempts": 2,
"budgetHal": 50000,
"recipients": [
{ "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" } }
]
}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 50000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "draft",
"revision": 0,
"createdAt": 1757280000000,
"updatedAt": 1757280000000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [] }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (brakujące pole, zły typ, ponad 1000 odbiorców lub zadanie większe niż 2 MB, nieprawidłowy numer telefonu u jednego z odbiorców,retryMaxAgeDayspoza zakresem 1-30 albo wysłane bezretryPolicy: "lorela_enterprise"...). - 404
agent_not_foundNa koncie nie ma agenta o tymagentIdalbo nie jest aktywny. - 400
budget_too_lowbudgetHalnie pokrywa nawet jednej próby przy maksymalnym czasie trwania połączenia (maxDurationSecs).
GET/v1/tasks/{id}
Szczegóły zadania, WRAZ z issues - próbnym przebiegiem tego, co aktualnie blokuje uruchomienie zadania (liczone na świeżo, nic nie jest zapisywane). Integracje przypomnień mogą też użyć GET /v1/tasks/{id}/items/{itemId}/reminder-call?callId=c_..., aby odczytać w reminderCall dokładną, powiązaną z właścicielem rzeczywistą próbę i zanonimizowaną jakość. Brakujące metryki mają observed: false i null, nigdy zero. Każde udokumentowane dismissReminder.invoked: true blokuje SMS.
Żądanie
curl https://volai.cz/v1/tasks/task_9f3a2c1d \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 50000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "draft",
"revision": 0,
"createdAt": 1757280000000,
"updatedAt": 1757280000000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [] }
],
"issues": []
}
}Stany błędów
- 404
task_not_foundNa koncie nie ma zadania o tymid.
PATCH/v1/tasks/{id}
Aktualizuje reguły edytowalnego zadania (name, recipients, callingWindow, maxAttempts, retryPolicy, retryMaxAgeDays, expiresAt, maxDurationSecs, budgetHal) - agentId, taskType i source nie mogą być zmieniane po utworzeniu. revision jest WYMAGANE (optymistyczna współbieżność). Politykę można włączyć tylko przed pierwszą próbą; tę samą politykę można później wysłać ponownie. Pominięte maxAttempts, retryPolicy, retryMaxAgeDays i expiresAt pozostają bez zmian. maxAttempts: 4 wymaga polityki w tym żądaniu lub już zapisanej w zadaniu. retryMaxAgeDays (1-30, domyślnie 5; horyzont kończy się N × 24 godziny od pierwszej zliczonej próby każdego odbiorcy) również wymaga polityki i, w przeciwieństwie do polityki, może się zmieniać po rozpoczęciu prób: jego podniesienie przywraca do stanu failed (oczekiwanie na kolejną próbę) odbiorców pominiętych wyłącznie z powodu horyzontu ponowień (reviewReason: "retry_age_expired"), z nowym nextAttemptAt, gdy mają jeszcze próbę do wykorzystania, a następny termin harmonogramu mieści się w nowym horyzoncie i przed expiresAt; jego obniżenie niczego nie otwiera ponownie. Odbiorców ponownie otwiera tylko PATCH podnoszący horyzont, dlatego retryMaxAgeDays należy wysłać razem z przedłużonym expiresAt w jednym żądaniu. Zadania running lub completed nie można edytować, więc działające zadanie należy najpierw wstrzymać.
Żądanie
curl -X PATCH https://volai.cz/v1/tasks/task_9f3a2c1d \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"revision": 0, "budgetHal": 80000}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 80000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "draft",
"revision": 1,
"createdAt": 1757280000000,
"updatedAt": 1757280060000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [] }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (na przykładretryMaxAgeDayspoza zakresem 1-30 albo bez politykilorela_enterprise) albo brakujerevision. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision. - 409
task_not_editableZadanie jestrunning/completedalboneeds_attentionbez możliwości odzyskania - jego reguł nie można teraz zmienić. - 409
items_immutableListy odbiorców (recipients) nie można zastąpić, gdy odbyła się co najmniej jedna próba połączenia albo pozycja ma audytowane rozstrzygnięcie pominięcia (po zakupie lub rezygnacji z subskrypcji).
POST/v1/tasks/{id}/start
Rozpoczyna (lub po wstrzymaniu wznawia) dzwonienie do odbiorców - KAŻDE odebrane połączenie jest naliczane natychmiast, budżet jest rezerwowany przed każdym wybraniem numeru. Obsługuje Idempotency-Key.
Żądanie
curl -X POST https://volai.cz/v1/tasks/task_9f3a2c1d/start \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: task-start-2026-09-08-01" \
-d '{"revision": 1}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 80000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "running",
"revision": 2,
"createdAt": 1757280000000,
"updatedAt": 1757280120000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [] }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem, brakujerevisionalboexpiresAtjuż minęło. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision. - 409
task_not_startableZadanie nie ma statusudraft/ready/pausedalbo nie ma już odbiorcy, do którego można zadzwonić. - 409
task_needs_attentionPrzed następnym uruchomieniem potrzebne jestPOST .../reconcile- coś zmieniło się w trakcie działania zadania. - 404
agent_not_foundNa koncie nie ma agenta o tymagentIdalbo nie jest aktywny. - 403
agent_suspendedOperator volai ręcznie zawiesił agenta zadania - zadania nie można uruchomić ani wznowić po wstrzymaniu, dopóki zawieszenie nie zostanie zniesione. Zawieszone konto zwraca zamiast tegoaccount_suspended. - 409
engine_requiredAgent zadania nie działa na silniku volai - zadania wychodzące wymagają dziś silnika, a nie ElevenLabs. - 409
agent_not_readyWersja robocza agenta nie jest opublikowana albo opublikowana wersja nie została przetestowana - przed uruchomieniem należy ją opublikować (POST /v1/agents/{id}/draft/publish), a następnie zasymulować opublikowaną wersję (POST /v1/agents/{id}/simulate). - 503
engine_contract_unavailableSilnik obecnie nie deklaruje obsługi zmiennych i limitów zadań wychodzących - uruchomienie należy ponowić później. - 409
window_closedPoza skonfigurowanym oknem dzwonienia (callingWindow) - należy poczekać na jego ponowne otwarcie albo zmienić je przezPATCH.
POST/v1/tasks/{id}/pause
Zatrzymuje dalsze dzwonienie PRZED następną próbą; trwające już połączenie nie jest przerywane. Bez Idempotency-Key - powtórzenie z tą samą rewizją jest samo w sobie bezpieczne.
Żądanie
curl -X POST https://volai.cz/v1/tasks/task_9f3a2c1d/pause \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"revision": 2}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"status": "paused",
"revision": 3,
"budgetHal": 80000,
"reservedBudgetHal": 0,
"spentHal": 684,
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"createdAt": 1757280000000,
"updatedAt": 1757280180000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "completed", "attempts": [{ "id": "attempt_5d4c3b", "startedAt": 1757280100000, "finishedAt": 1757280160000, "providerCallId": "c_7a2b9c1d", "outcome": "completed", "costHal": 684 }], "actualCostHal": 684 }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem albo brakujerevision. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
task_not_runningWstrzymanie działa tylko na zadaniu uruchomionym (running). - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision.
GET/v1/tasks/{id}/items/{itemId}/reminder-call
Odczyt dowodu rzeczywistego zliczonego połączenia z zadania z purpose: coupon_reminder. Wymagany parametr zapytania callId=c_... należy podać dokładnie raz. Serwer weryfikuje dokładne powiązanie konta, zadania, pozycji, połączenia i autoryzacji przypomnienia. reminderCall zawiera skrót numeru telefonu, status, cenę i zanonimizowaną jakość; nie zwraca numerów telefonów, transkrypcji ani argumentów narzędzi. Brakujące metryki mają observed: false i count: null, nigdy zero. dismissReminder.invoked: true lub optOutRequested: true blokuje SMS, nawet jeśli narzędzie zawiedzie lub przekroczy czas. Tylko udowodniona zamknięta próba bez odpowiedzi z zerowym czasem trwania i ceną, bez sprzecznych dowodów rozmowy, może ustalić, że odrzucenie i rezygnacja mają wartość false; brakujące metryki tur/narzędzi pozostają nieznane. Przed podjęciem działania następczego należy sprawdzić status połączenia. Ten endpoint tylko odczytuje dowody i nie wysyła SMS.
Żądanie
curl "https://volai.cz/v1/tasks/task_reminder_9f3a/items/item_1a2b3c/reminder-call?callId=c_reminder_7e2d" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"reminderCall": {
"callId": "c_reminder_7e2d",
"tenantId": "u_4a8d2c",
"agentId": "ag_4e91a2f0",
"taskId": "task_reminder_9f3a",
"taskItemId": "item_1a2b3c",
"dispatchIntentId": "dispatch_8d2a4f",
"purpose": "coupon_reminder",
"phoneSha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"direction": "out",
"status": "no_answer",
"startedAt": 1791882000000,
"durationSecs": 0,
"priceHal": 0,
"endReason": "no_answer",
"optOutRequested": false,
"hasConversation": false,
"quality": {
"turnFailures": { "observed": false, "count": null },
"toolFailures": { "observed": false, "count": null },
"dismissReminder": { "observed": true, "invoked": false }
}
}
}Stany błędów
- 400
validationNależy podać dokładnie jeden prawidłowy parametr zapytaniacallIdw postacic_..., bez innych parametrów. - 404
task_not_foundNie znaleziono dokładnego, powiązanego z właścicielem zliczonego połączenia przypomnienia; cudze ID, inny cel i nieprawidłowe powiązania zwracają tę samą odpowiedź.
POST/v1/tasks/{id}/items/{itemId}/resolve
Osobny wariant po zweryfikowanym zakupie lub po rezygnacji klienta przyjmuje action: skip, reason (customer_purchased = klient dokonał zakupu, unsubscribed = klient zrezygnował), revision, operationId i evidenceRef (nieprzezroczyste identyfikatory do 128 znaków każdy; evidenceRef wskazuje rekord zakupu lub rezygnacji i nie zawiera numeru telefonu, adresu e-mail ani imienia i nazwiska), na przykład {"action":"skip","revision":12,"reason":"unsubscribed","operationId":"unsub-001","evidenceRef":"unsub-record-001"}. Pomija wyłącznie nieaktywną pozycję pending w zadaniu draft albo pending/failed w zadaniu paused bez nierozliczonych połączeń ani rezerwacji. Historia, koszty i DNC zostają zachowane: numer nie jest dodawany do listy zakazu dzwonienia, nawet dla unsubscribed. GET zwraca niezmienne potwierdzenie items[].resolution, którego reason to customer_purchased lub unsubscribed. Po niepewnej odpowiedzi tę samą operację można odtworzyć bez kolejnego zapisu; inne rozstrzygnięcie dla już rozstrzygniętej pozycji (w tym inny reason z tym samym operationId) zwraca 409 revision_conflict. Pierwotne zachowanie bez reason: rozstrzyga o kontakcie oczekującym na przegląd (items[].status: "review"): zwykle retry odkłada go z powrotem do kolejki bez zużywania kolejnej próby, a skip zamyka go jako pominięty. Z retryPolicy: "lorela_enterprise" ręczne retry zwraca validation; dozwolone są tylko zaplanowane próby automatyczne. Kontakt w review może pojawić się, gdy zadanie wciąż działa, ale resolve działa tylko wtedy, gdy zadanie nie jest uruchomione - na uruchomionym zadaniu zwraca 409 task_not_editable, więc należy poczekać na needs_attention albo wstrzymać zadanie (pause). Dla reviewReason: "dial:destination_auto_blocked" najpierw należy zdjąć blokadę (POST /v1/dnc/{e164}/unblock) i dopiero potem użyć retry; dla dial:cannot_call_own_number, dial:cannot_call_volai_number, dial:invalid_number, dial:unsupported_country i dial:blocked_destination pomaga tylko skip. reviewReason: "billing_unknown" jest inny: providerCallId i zarezerwowany budżet pozostają powiązane do czasu POST .../reconcile; po retry zastępcza próba czeka na rozliczenie pierwotnego połączenia, a po skip nie jest wykonywane żadne połączenie zastępcze, ale pierwotne połączenie może jeszcze zostać naliczone później (jego rezerwacja nadal liczy się do budżetu). Po retry zadanie pozostaje więc w needs_attention, dopóki cena się nie rozliczy; po skip przechodzi do paused i można je uruchomić, gdy zostaje praca, w przeciwnym razie czeka w needs_attention i kończy się po rozliczeniu ceny. Połączenie, do którego nigdy nie wybrano numeru, rozlicza się od razu na 0. W przypadku innych powodów przeglądu rozstrzygnięcie ostatniego kontaktu przenosi zadanie z needs_attention do paused (start je wznawia) lub completed.
Żądanie
curl -X POST https://volai.cz/v1/tasks/task_9f3a2c1d/items/item_1a2b3c/resolve \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "retry", "revision": 5}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 80000,
"reservedBudgetHal": 2400,
"spentHal": 0,
"status": "needs_attention",
"revision": 6,
"createdAt": 1757280000000,
"updatedAt": 1757280240000,
"reviewReason": "billing_unknown",
"items": [
{
"id": "item_1a2b3c",
"phone": "+420777123456",
"variables": { "jmeno": "Jana Novakova" },
"paymentStatus": "not_applicable",
"status": "pending",
"attempts": [
{ "id": "attempt_7c2d", "startedAt": 1757280180000, "finishedAt": 1757280230000, "providerCallId": "c_late_price", "outcome": "completed" }
],
"reviewReason": "billing_unknown",
"providerCallId": "c_late_price",
"estimatedCostHal": 2400,
"reservedCostHal": 2400
}
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (actiontoretrylubskip,revisionnieujemna liczba całkowita, opcjonalnereasontocustomer_purchasedlubunsubscribedi wymagaoperationIdorazevidenceRef), alboitemIdnie istnieje w zadaniu, albo politykalorela_enterpriseodrzuca ręczneretry. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
task_not_editableZadanie nie jest w dozwolonym stanie. Pominięcie po zakupie lub rezygnacji wymaga stanu draft lub paused; działające zadanie należy wstrzymać, a aktywne połączenia rozliczyć. - 409
item_not_resolvablePozycja nie kwalifikuje się do tego rozstrzygnięcia. Pierwotny wariant wymaga przeglądu; pominięcie zreason(customer_purchasedlubunsubscribed) wymaga nieaktywnej oczekującej pozycji w wersji roboczej albo pozycji pending/failed w zadaniu wstrzymanym bez nierozliczonych połączeń. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision.
POST/v1/tasks/{id}/reconcile
Uzgadnia stan zadania z jego rzeczywistymi połączeniami po przerwie - można je bezpiecznie wywoływać w dowolnym momencie, także wielokrotnie. W PRZECIWIEŃSTWIE do start/pause revision jest OPCJONALNE. Rozstrzyga zadanie zatrzymane z jednej z czterech przyczyn: wstrzymania w trakcie wybierania numeru (dispatch_in_flight), wygasłego przejęcia (dispatch_claim_expired), połączenia, którego cena nie była jeszcze znana (billing_unknown) lub niepewnego wybierania numeru (dial:*), którego połączenie zostało znalezione. Gdy żadne połączenie nie czeka na rozliczenie, takie zadanie staje się paused, dopóki jakikolwiek kontakt czeka na próbę (nawet jeśli inne są w review), needs_attention, gdy zostają tylko kontakty w review, oraz completed, gdy nic nie zostaje. Osobno zwalnia zamiar wybrania numeru, którego pracownik zginął, zanim połączenie zostało zestawione (nie istnieje rekord połączenia ani zapisana próba), gdy jego przejęcie pracy i 5-minutowy margines już wygasły, czyli najwcześniej 15 minut po rozpoczęciu wybierania: kontakt wraca do oczekiwania (lub staje się completed po zweryfikowanym oddzwonieniu), a jego rezerwacja budżetu jest zwracana. Nic innego się nie zmienia: zadanie running nadal działa, a zadanie zatrzymane z innej przyczyny (variables_limit, nieudana kontrola faktury lub kalendarza, niepewne wybieranie numeru, którego połączenia nie można znaleźć, albo kontakty w review, które zatrzymały je wcześniej) pozostaje w needs_attention, dopóki nie rozstrzygną Państwo tych kontaktów przez resolve.
Żądanie
curl -X POST https://volai.cz/v1/tasks/task_9f3a2c1d/reconcile \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{}'Odpowiedź
{
"task": {
"id": "task_9f3a2c1d",
"status": "completed",
"revision": 3,
"budgetHal": 80000,
"reservedBudgetHal": 0,
"spentHal": 684,
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"createdAt": 1757280000000,
"updatedAt": 1757280240000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "completed", "attempts": [{ "id": "attempt_5d4c3b", "startedAt": 1757280100000, "finishedAt": 1757280160000, "providerCallId": "c_7a2b9c1d", "outcome": "completed", "costHal": 684 }], "actualCostHal": 684 }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (revision, jeśli podane, musi być nieujemną liczbą całkowitą). - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision.
POST/v1/tasks/{id}/process
Uzgadnia rzeczywiste połączenia i próbuje przetworzyć najwyżej jednego należnego odbiorcę już uruchomionego zadania. Wymaga bieżącego revision; nigdy nie uruchamia ani nie wznawia zadania. Używa tego samego okna dzwonienia, tłumienia, rezerwacji budżetu, salda, pojemności i atomowego przejęcia co cron. Zapisane opt-in dispatchOrder: "first_attempts_first" nadaje priorytet należnym odbiorcom bez zliczonej próby; odrzucenia przed wybraniem numeru (countsAsAttempt: false) zachowują ten priorytet. Domyślnie zachowana jest dotychczasowa kolejność pozycji, a przyszłe wartości nextAttemptAt są respektowane. Po niepewnym wyniku należy odczytać bieżący stan zamiast ślepo powtarzać żądanie. Każdy nowy logiczny takt wymaga nowego Idempotency-Key, nawet gdy rewizja się nie zmieniła.
Żądanie
curl -X POST https://volai.cz/v1/tasks/task_9f3a2c1d/process \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: task-process-2026-10-08-tick-001" \
-d '{"revision": 2}'Odpowiedź
{
"status": "idle",
"task": {
"id": "task_9f3a2c1d",
"name": "Payment reminder - September",
"agentId": "ag_4e91a2f0",
"taskType": "custom",
"purpose": "standard",
"source": "manual",
"callingWindow": { "timezone": "Europe/Prague", "start": "09:00", "end": "18:00", "days": [1, 2, 3, 4, 5] },
"maxAttempts": 2,
"maxDurationSecs": 120,
"budgetHal": 80000,
"reservedBudgetHal": 0,
"spentHal": 0,
"status": "running",
"revision": 2,
"createdAt": 1757280000000,
"updatedAt": 1757280120000,
"items": [
{ "id": "item_1a2b3c", "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "paymentStatus": "not_applicable", "status": "pending", "attempts": [], "nextAttemptAt": 1757280180000 }
]
}
}Stany błędów
- 400
validationTreść nie zgadza się ze schematem albo brakujerevision. - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision. - 409
task_not_runningZadanie nie jest uruchomione albo zostało wstrzymane przed wybieraniem numeru. Należy odczytać jego bieżący stan; process nigdy go nie wznawia. - 409
task_pausedZadanie nie jest uruchomione albo zostało wstrzymane przed wybieraniem numeru. Należy odczytać jego bieżący stan; process nigdy go nie wznawia. - 409
claim_lostWyniku wysyłki nie można bezpiecznie potwierdzić. Zamiast ślepo powtarzać żądanie, należy odczytać lub uzgodnić zadanie i sprawdzić jego połączenie. - 500
dispatch_result_unpersistedWyniku wysyłki nie można bezpiecznie potwierdzić. Zamiast ślepo powtarzać żądanie, należy odczytać lub uzgodnić zadanie i sprawdzić jego połączenie. - 409
agent_version_changedOpublikowana wersja agenta się zmieniła. Przed ponownym uruchomieniem zadania należy ją przejrzeć i ponownie przetestować. - 403
agent_suspendedOperator volai ręcznie zawiesił agenta zadania - zadania nie można uruchomić ani wznowić po wstrzymaniu, dopóki zawieszenie nie zostanie zniesione. Zawieszone konto zwraca zamiast tegoaccount_suspended. - 503
engine_contract_unavailableSilnik obecnie nie deklaruje obsługi zmiennych i limitów zadań wychodzących - uruchomienie należy ponowić później. - 409
window_closedPoza skonfigurowanym oknem dzwonienia (callingWindow) - należy poczekać na jego ponowne otwarcie albo zmienić je przezPATCH.
DELETE/v1/tasks/{id}
Usuwa zadanie i wszystkie jego pozycje - tylko w statusie draft/ready/paused/completed/needs_attention, BEZ pozycji in_progress. revision jest odczytywane z ?revision= LUB z treści JSON {"revision": ...} (parametr zapytania ma pierwszeństwo).
Żądanie
curl -X DELETE "https://volai.cz/v1/tasks/task_9f3a2c1d?revision=3" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"deleted": true
}Stany błędów
- 400
validationBrakujerevisionalbo nie jest nieujemną liczbą całkowitą (w zapytaniu lub w treści). - 404
task_not_foundNa koncie nie ma zadania o tymid. - 409
revision_conflictZadanie w międzyczasie się zmieniło - należy je odczytać ponownie (GET) i powtórzyć z bieżącymrevision. - 409
task_not_deletableZadanie ma pozycjęin_progressalbo jestrunning- należy poczekać, aż się zakończy, lub najpierw je wstrzymać.
POST/v1/tasks/csv-preview
Analizuje i waliduje CSV z odbiorcami PRZED utworzeniem lub aktualizacją zadania - nic nie jest zapisywane, błędy poszczególnych wierszy trafiają do tablicy errors odpowiedzi 200, a nie do błędu HTTP.
Żądanie
curl -X POST https://volai.cz/v1/tasks/csv-preview \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"csv": "phone,jmeno\n+420777123456,Jana Novakova\nneplatne,Petr Svoboda\n"}'Odpowiedź
{
"headers": ["phone", "jmeno"],
"delimiter": ",",
"rows": [
{ "line": 2, "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" }, "errors": [] },
{ "line": 3, "variables": { "jmeno": "Petr Svoboda" }, "errors": [{ "field": "phone", "message": "Invalid phone number", "row": 3 }] }
],
"validRows": [
{ "phone": "+420777123456", "variables": { "jmeno": "Jana Novakova" } }
],
"errors": [{ "field": "phone", "message": "Invalid phone number", "row": 3 }]
}Stany błędów
- 400
validationPolecsvw treści brakuje albo nie jest ciągiem znaków, albocsvprzekracza 1 000 000 znaków.
Numery
GET/v1/numbers/{e164}/callback-routing
Odczytuje opt-in politykę oddzwaniania numeru należącego do konta. Domyślne kierowanie numeru pozostaje bez zmian.
Żądanie
curl "https://volai.cz/v1/numbers/+420601234567/callback-routing" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{"callbackRouting":{"agentIds":["ag_campaign"],"lookbackSecs":604800}}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta.
PUT/v1/numbers/{e164}/callback-routing
Zapisuje jawną listę dozwolonych aktywnych agentów silnika należących do konta dla dzwoniących wracających, którzy w ciągu lookbackSecs (60-604800 sekund) mieli z tego numeru jedno unikalne rzeczywiste połączenie wychodzące. Nieznani, niejednoznaczni lub niezweryfikowani dzwoniący zachowują agenta domyślnego. Puste agentIds wyłącza politykę. Opcjonalne firstMessage dotyczy tylko zweryfikowanych oddzwonień. Opcjonalne missedFirstMessage (do 500 znaków) zastępuje firstMessage dla dzwoniącego, którego poprzednie połączenie wychodzące nie zawierało rozmowy (bez odebrania, poczta głosowa, menu głosowe, nikt nie mówił) - ten dzwoniący nie usłyszał jeszcze oferty; bez niego firstMessage obowiązuje wszystkich. Silnik otrzymuje też volai_callback_kind (missed lub contacted) przy zweryfikowanym oddzwonieniu. engineSyncRequired: true oznacza, że konfiguracja silnika agenta domyślnego musi osobno aktywować webhooks.inbound_route_url z odczytem zwrotnym; to żądanie nie przepisuje działającej konfiguracji silnika. Zweryfikowane połączenia przychodzące udostępniają w szczegółach połączenia callbackOf.callId oraz opcjonalnie należące do serwera taskId i taskItemId.
Żądanie
curl -X PUT "https://volai.cz/v1/numbers/+420601234567/callback-routing" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"agentIds":["ag_campaign"],"lookbackSecs":604800}'Odpowiedź
{"callbackRouting":{"agentIds":["ag_campaign"],"lookbackSecs":604800},"engineSyncRequired":true}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta.
GET/v1/numbers
Numery głosowe na koncie, każdy z bieżącym kierowaniem. Numery komórkowe są wymienione osobno w GET /v1/sms-numbers (MCP list_sms_numbers), a produkty "numer do połączeń i SMS" w GET /v1/voice-sms-numbers (MCP list_voice_sms_numbers).
Żądanie
curl https://volai.cz/v1/numbers \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"numbers": [
{
"e164": "+420601234567",
"routing": { "mode": "agent", "agentId": "ag_kx91fa2b", "hasSipPassword": false, "sipTarget": "engine" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000,
"region": "objednavka",
"regionLabel": "Czech number",
"billingMonths": 1
}
]
}GET/v1/numbers/{e164}
Szczegóły jednego numeru należącego do konta - w tym samym kształcie co pozycja listy GET /v1/numbers. Przydatne, gdy znany jest tylko numer E.164 (zwykle agent przez MCP) i nie chce się przeglądać całej listy.
Żądanie
curl "https://volai.cz/v1/numbers/+420601234567" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"number": {
"e164": "+420601234567",
"routing": { "mode": "agent", "agentId": "ag_kx91fa2b", "hasSipPassword": false, "sipTarget": "engine" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000,
"region": "objednavka",
"regionLabel": "Czech number",
"billingMonths": 1
}
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta.
POST/v1/numbers
Kupuje numer. Pusta treść {} przydziela dowolny wolny numer z bieżącej oferty, albo można wysłać konkretny { "e164": "..." } z GET /v1/numbers/available. Region wybiera się przez { "region": "brno" } - dozwolone wartości to praha, brno, internet, bratislava oraz aktualnie sprzedawane oferty regionalne (wymienione w regions w GET /v1/numbers/available); numer z regionu, który nie jest w sprzedaży, zamawia się przez POST /v1/numbers/orders. Naliczana jest opłata miesięczna 25.00 CZK (~EUR 1.04) (dla numeru słowackiego, oferta bratislava, jest to 75.00 CZK (~EUR 3.13) z góry - billingMonths miesięcy płatnych z góry, a ta sama kwota jest pobierana ponownie co billingMonths miesięcy), a kierowanie zaczyna się od none - należy je od razu ustawić przez PATCH.
Żądanie
curl -X POST https://volai.cz/v1/numbers \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{}'Odpowiedź
{
"number": {
"e164": "+420266266647",
"routing": { "mode": "none", "hasSipPassword": false },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000,
"region": "praha",
"regionLabel": "Prague",
"billingMonths": 1
}
}Stany błędów
- 400
validationTreść żądania nie zgadza się ze schematem (e164/region ma ponad 32 znaki albo zły typ) alboregionnie jest rozpoznanym identyfikatorem oferty. - 402
insufficient_creditSaldo nie pokrywa opłaty miesięcznej 25.00 CZK (~EUR 1.04). W przypadku numeru słowackiego (ofertabratislava) saldo musi z góry pokryćmonthlyFeeHal * billingMonths. Konto, które nigdy nie było doładowane, może mieć jeden numer z salda powitalnego (otwarte zamówienie też się liczy); kolejny numer można kupić dopiero po pierwszym doładowaniu. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 404
number_unavailableTen konkretny numer został już kupiony przez kogoś innego - prosimy wybrać inny z bieżącej oferty (GET /v1/numbers/available). - 503
pool_emptyW bieżącej ofercie brakuje numerów - należy zamówić numer z innego regionu (POST /v1/numbers/orders) albo zapisać się na listę oczekujących w portalu.
GET/v1/numbers/waitlist
Status listy oczekujących dla każdej oferty z puli (praha/brno/internet/bratislava oraz sprzedawane oferty regionalne, ta sama lista co regions w GET /v1/numbers/available) - joined mówi, czy konto na nią czeka, available - czy oferta jest teraz na stanie.
Żądanie
curl https://volai.cz/v1/numbers/waitlist \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"offers": [
{ "offerId": "praha", "label": "Prague", "joined": false, "available": true },
{ "offerId": "brno", "label": "Brno", "joined": true, "available": false },
{ "offerId": "internet", "label": "Internet number", "joined": false, "available": true },
{ "offerId": "bratislava", "label": "Bratislava", "joined": false, "available": true }
]
}Stany błędów
- 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
POST/v1/numbers/waitlist
Zapisuje na listę oczekujących na wyprzedaną ofertę (POST /v1/numbers zakończyło się pool_empty) - nie kupuje numeru, tylko rejestruje zainteresowanie. Gdy oferta znów będzie na stanie, otrzymają Państwo e-mail. Pusta treść {} używa oferty domyślnej (praha).
Żądanie
curl -X POST https://volai.cz/v1/numbers/waitlist \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"offerId": "brno"}'Odpowiedź
{
"joined": true
}Stany błędów
- 400
validationofferIdnie jest jedną ze sprzedawanych ofert (praha/brno/internet/bratislavaoraz oferty regionalne wregions). - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
DELETE/v1/numbers/waitlist/{offerId}
Wypisuje z listy oczekujących na JEDNĄ ofertę (offerId w ścieżce) - nie zmienia listy oczekujących na inne oferty.
Żądanie
curl -X DELETE https://volai.cz/v1/numbers/waitlist/brno \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"left": true
}Stany błędów
- 400
validationofferIdw ścieżce nie jest jedną z wartości ofert. - 404
user_not_foundUżytkownika, do którego należy ten klucz API, nie ma na koncie - to nietypowy stan; prosimy skontaktować się z pomocą techniczną.
GET/v1/numbers/available
Oferta numerów dostępnych do zakupu (maksymalnie 5 na region). numbers zawiera numery z wybranego regionu (Praga, jeśli parametr zostanie pominięty), a regions zawsze zwraca całą ofertę naraz - w tym bratislava (numer słowacki z inną opłatą, zob. POST /v1/numbers powyżej). Oferty regionalne (jihocesky, karlovarsky, kralovehradecky, liberecky, moravskoslezsky, olomoucky, pardubicky, plzensky, stredocesky, ustecky, vysocina, zlinsky) pojawiają się w regions dopiero wtedy, gdy region jest w sprzedaży (volai ma tam adres służb ratunkowych); do tego czasu taki region jest odrzucany jako nieznany, a numer z tego regionu zamawia się przez POST /v1/numbers/orders.
Żądanie
curl "https://volai.cz/v1/numbers/available?region=brno" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"region": "brno",
"numbers": ["+420510510129", "+420510510132"],
"regions": [
{
"id": "praha",
"label": "Prague",
"description": "A landline with a Prague area code.",
"numbers": ["+420266266643", "+420266266645"]
},
{
"id": "brno",
"label": "Brno",
"description": "A landline with a Brno area code.",
"numbers": ["+420510510129", "+420510510132"]
},
{
"id": "internet",
"label": "Internet number",
"description": "Area code 910, not tied to any region - works from anywhere.",
"numbers": ["+420910084099"]
},
{
"id": "bratislava",
"label": "Bratislava",
"description": "A Slovak number with the Bratislava area code +421 2. Billed three months upfront.",
"numbers": ["+421222205798"]
}
]
}Stany błędów
- 400
validationregionmusi być jednym z identyfikatorów w ofercie (zob./cenik).
GET/v1/numbers/address-options
Adresy do zamówienia numeru z regionu spoza oferty. Przechodzi kaskadowy katalog operatora: po wysłaniu psc otrzymuje się gminy, po dodaniu obec - dzielnice, cobce zwraca ulice, a ulice zwraca numery budynków. Po wybraniu cp czytelny adres trafia do pola recap. Nie należy wymyślać kodów - ważne są tylko te zwrócone tutaj. Szybszą drogą z pominięciem kaskady jest query, zob. ramka poniżej.
Żądanie
curl "https://volai.cz/v1/numbers/address-options?psc=70200&obec=554821&cobce=413950" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"obce": [{ "value": "554821", "label": "Ostrava" }],
"casti": [{ "value": "413950", "label": "Moravská Ostrava" }],
"ulice": [{ "value": "353710", "label": "28. října" }],
"cp": [],
"recap": null
}Stany błędów
- 400
validationquery ma ponad 200 znaków, nie zawiera kodu pocztowego albo operator nie ma pasującej opcji dla poziomu, do którego doszło wyszukiwanie. - 502
provisioning_failedNie udało się w ogóle wczytać adresów od operatora. - 503
orders_disabledZamówienia są tymczasowo wstrzymane.
Szybsza droga: cały adres w jednym zapytaniu
Zamiast kaskady można wysłać query z całym adresem (np. "Nadrazni 100, 702 00 Ostrava") - serwer przeprowadzi kaskadę za Państwa. Bezpośrednio z tekstu odczytywany jest tylko kod pocztowy; wszystko inne jest jedynie dopasowywane do opcji, które operator zwrócił dla danego poziomu - serwer nigdy nie wymyśla kodu.
curl -G https://volai.cz/v1/numbers/address-options \
-H "Authorization: Bearer vk_YOUR_KEY" \
--data-urlencode "query=Nádražní 100, 702 00 Ostrava"{
"obce": [],
"casti": [],
"ulice": [],
"cp": [],
"recap": "28. října 102/1, Ostrava, 70200",
"selection": {
"psc": "70200",
"obec": "554821",
"cobce": "413950",
"ulice": "353710",
"cp": "3180026"
}
}Gdy adres jest niejednoznaczny, zwracany jest tylko poziom, na którym to się wyjaśnia (ambiguousLevel: obec/cobce/ulice/cp), wraz z opcjami do wyboru w odpowiednim polu, na przykład:
{
"obce": [
{ "value": "554821", "label": "Ostrava" },
{ "value": "554813", "label": "Fulnek" }
],
"casti": [],
"ulice": [],
"cp": [],
"recap": null,
"selection": { "psc": "70200", "obec": "", "cobce": "", "ulice": "", "cp": "" },
"ambiguousLevel": "obec",
"message": "The given address is ambiguous - choose the municipality (obec) from the options. Repeat the query WITHOUT query and with the step parameters: psc=70200, obec=<value from the options>."
}selection zawsze zawiera to, co zostało już rozstrzygnięte - należy kontynuować tym samym parametrem kroku (psc/obec/cobce/ulice/cp), którego użyto by bez query. Jeśli adresu nie da się w ogóle rozstrzygnąć albo w trakcie wyszukiwania skończy się budżet czasu, zamiast recap w polu message pojawia się czytelny komunikat wraz z dotychczas rozstrzygniętym selection - stamtąd należy kontynuować parametrami kroków.
query i parametry kroków NIE MOGĄ SIĘ MIESZAĆ w jednym żądaniu - po wysłaniu obu naraz endpoint zwraca validation. Dlatego po niejednoznacznej odpowiedzi nie należy powtarzać tego samego query z dodanym obec (lub innym poziomem) z opcji - następne żądanie należy wysłać BEZ query, używając wyłącznie parametrów kroków. Dokładny kształt (wraz z już rozstrzygniętymi wartościami) podano wprost w polu message tej niejednoznacznej odpowiedzi.
POST/v1/numbers/orders
Zamawia numer pod adresem siedziby klienta - gdy ma być tam zarejestrowany albo region jest wyprzedany lub spoza oferty. Kody adresowe muszą pochodzić z GET /v1/numbers/address-options. Endpoint czeka, podczas gdy u operatora zakładamy adres służb ratunkowych i kupujemy numer - zwykle krócej niż minutę; gotowy numer wraca wtedy w polu e164 ze statusem done. Jeśli za pierwszym razem się nie uda, zwraca zamówienie ze statusem pending, a zadanie cron kończy je później. Opłata 25.00 CZK (~EUR 1.04) jest naliczana dopiero wtedy, gdy numer jest gotowy.
Żądanie
curl -X POST https://volai.cz/v1/numbers/orders \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"psc":"70200","obec":"554821","cobce":"413950","ulice":"353710","cp":"3180026"}'Odpowiedź
{
"order": {
"id": "Xa3Kd9pQmR2v",
"status": "pending",
"address": "70200",
"createdAt": 1756111640000,
"updatedAt": 1756111640000
}
}Stany błędów
- 400
validationTreść żądania nie zgadza się ze schematem - psc/obec/cobce/cp brakuje lub jest puste, albo pole przekracza limit długości (ulice jest opcjonalne). - 400
incomplete_addressBrakuje psc, obec, cobce lub cp (ulice jest opcjonalne - nie każdy adres ją ma). - 402
insufficient_creditSaldo nie pokrywa opłaty miesięcznej 25.00 CZK (~EUR 1.04). Konto, które nigdy nie było doładowane, może mieć jeden numer z salda powitalnego (otwarte zamówienie też się liczy); kolejny można zamówić dopiero po pierwszym doładowaniu. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 503
orders_disabledZamówienia są tymczasowo wstrzymane. - 409
idempotency_conflictTen Idempotency-Key został już użyty dla zamówienia z innym adresem.
GET/v1/numbers/orders
Status zamówienia: pending (w kolejce), provisioning (właśnie jest konfigurowane), done (numer znajduje się w polu e164 i należy do konta), failed (powód w polu error, nic nie zostało naliczone), cancelled (anulowane przez klienta, gdy jeszcze było w kolejce - nic nie zostało naliczone).
Żądanie
curl https://volai.cz/v1/numbers/orders \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"orders": [
{
"id": "Xa3Kd9pQmR2v",
"status": "done",
"address": "28. října 102/1, Ostrava, 70200",
"e164": "+420596123456",
"createdAt": 1756111640000,
"updatedAt": 1756118840000
}
]
}POST/v1/numbers/orders/{id}/cancel
Anuluje zamówienie, które jest jeszcze w kolejce (pending) - bez treści, zwraca zamówienie ze statusem cancelled. Nic nie jest naliczane (opłata jest pobierana dopiero, gdy numer jest gotowy), a konto, które nigdy nie było doładowane, może zamówić ponownie. Zamówienia, które właśnie jest konfigurowane (provisioning), nie można anulować - zwraca 409 in_progress, należy spróbować za kilka minut; nie można też anulować zamówienia done ani failed (409 invalid_transition) - gotowy numer zwalnia się przez DELETE /v1/numbers/{e164}. Anulowanie już anulowanego zamówienia jest nieszkodliwe (jest zwracane bez zmian).
Żądanie
curl -X POST https://volai.cz/v1/numbers/orders/Xa3Kd9pQmR2v/cancel \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"order": {
"id": "Xa3Kd9pQmR2v",
"status": "cancelled",
"address": "28. října 102/1, Ostrava, 70200",
"createdAt": 1756111640000,
"updatedAt": 1756112240000
}
}Stany błędów
- 404
not_foundZamówienie nie istnieje albo nie należy do Państwa konta. - 409
in_progressZamówienie jest właśnie konfigurowane (provisioningalbo zadanie cron dopiero je podejmuje) - później można je zwolnić tylko jako gotowy numer. Prosimy spróbować za kilka minut. - 409
invalid_transitionZamówienie jest zakończone (done- numer zwalnia się przezDELETE /v1/numbers/{e164}) albo już się nie powiodło (failed- nie ma czego anulować).
DELETE/v1/numbers/{e164}
Zwalnia numer z powrotem do puli - odłącza kierowanie i każdą rejestrację agenta. Miesiąc, który został już opłacony, nie jest zwracany.
Żądanie
curl -X DELETE "https://volai.cz/v1/numbers/+420601234567" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"released": true
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta. - 502
release_failedNie udało się bezpiecznie potwierdzić odłączenia kierowania w sieci, więc numer nie został zwolniony do puli. Prosimy spróbować za chwilę lub skontaktować się z pomocą techniczną.
PATCH/v1/numbers/{e164}
Zmienia kierowanie numeru.
mode: "agent"+agentId- połączenia odbiera agent głosowy.mode: "forward"+forwardTo(E.164) - przekierowanie połączeń - płacą Państwo za obie nogi.mode: "sip"+sipUri- kieruje do Państwa własnego serwera SIP (zob. SIP) - opcjonalnie z uwierzytelnianiem przez sipUsername i sipPassword, jeśli docelowa centrala tego wymaga. Pole całkowicie pominięte w treści PATCH zachowuje zapisaną wartość - tak samo dla obu pól, więc zmiana samego sipUri nie wymaga ponownego wysyłania zapisanej nazwy użytkownika ani hasła. Wyjątek: jeśli zmiana sipUri zmienia także serwer (część po znaku @), hasło trzeba wysłać ponownie - nigdy nie wysyłamy zapisanego hasła na inny serwer niż ten, dla którego zostało wpisane. Wyczyszczenie uwierzytelniania jest tylko jawne - należy wysłać zarówno sipUsername, jak i sipPassword jako puste ciągi. Odpowiedź zawsze zawiera też hasSipPassword (opcjonalne, tylko do odczytu) - true oznacza, że numer ma zapisane hasło SIP; samo sipPassword nigdy nie jest zwracane przez API.mode: "none"- połączenia na numer nie są nigdzie kierowane, a sieć je odrzuca.
sipTarget w routing to opcjonalne, informacyjne pole ustawiane przez volai - nigdy nie wysyłają go Państwo sami. Wartość "engine" oznacza, że agent numeru działa na własnym silniku volai. Każda inna wartość albo brak pola oznacza, że tak nie jest (zwykle agent na ElevenLabs) - samego braku pola nie należy traktować jako pewnego dowodu ElevenLabs; "engine" to jedyny wiarygodny sygnał.
Żądanie
curl -X PATCH "https://volai.cz/v1/numbers/+420601234567" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"routing": {"mode": "agent", "agentId": "ag_kx91fa2b"}}'Odpowiedź
{
"number": {
"e164": "+420601234567",
"routing": { "mode": "agent", "agentId": "ag_kx91fa2b", "hasSipPassword": false, "sipTarget": "engine" },
"monthlyFeeHal": 2500,
"boughtAt": 1756111640000,
"region": "objednavka",
"regionLabel": "Czech number",
"billingMonths": 1
}
}Stany błędów
- 400
validationrouting.mode wymaga odpowiadającego pola (agentId / forwardTo / sipUri). Dla routing.mode: "sip" mają zastosowanie także sipUsername i sipPassword: należy wypełnić oba albo żadne, bez dwukropka, znaku @, białych znaków, cudzysłowu i ukośnika odwrotnego, po najwyżej 64 znaki. Gdy oba dane logowania są wypełnione, wybierany numer w sipUri (przed znakiem @) nie może zawierać dwukropka, a serwer w sipUri (po znaku @) nie może mieć portu - razem z danymi logowania nie jest to jeszcze obsługiwane. Ten kod otrzymują Państwo także wtedy, gdy zmieniają serwer w sipUri na numerze z zapisanym hasłem bez ponownego wysłania sipPassword, albo gdy sipUri wskazuje trunk Vapi BYO SIP (<credentialId>.sip.vapi.ai lub .sip.eu.vapi.ai), a dane logowania są wysyłane lub zapisane - oba pola muszą tam pozostać puste, ponieważ Vapi rozpoznaje nasze połączenia po adresie IP w bramach. - 400
invalid_sip_urisipUri nie ma postaci sip:user@server (opcjonalnie z portem) - dotyczy tylko routing.mode: "sip". Ten sam kod otrzymują Państwo, gdy operator ostatecznie odmawia zapisania trasy Z UWIERZYTELNIANIEM (z wypełnionymi sipUsername i sipPassword) albo odczytuje inny cel niż ten, który wysłaliśmy - ponawianie bez zmian nie pomoże. Dla trasy bez danych logowania ta sama sytuacja jest zgłaszana jako routing_failed. - 402
insufficient_creditKonto nigdy nie było doładowane, a numer nie jest jeszcze przekierowywany - przekierowanie połączeń (routing.mode: "forward") można włączyć dopiero po pierwszym doładowaniu, ponieważ jest rozliczane później na podstawie zapisów operatora, bez hamulca podczas połączenia. Agent, SIP i połączenia przychodzące nadal działają. Numer, który jest już przekierowywany, można skierować na inny cel bez doładowania. Nic nie zostało naliczone. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta. - 502
routing_failedŻądane kierowanie nie zostało przed chwilą potwierdzone (przekierowanie, SIP, jego odłączenie albo potwierdzenie trasy do silnika) - tymczasowa awaria sieci, prosimy spróbować za chwilę. - 502
engine_unavailablePrzy routing.mode: "agent" dla agenta na własnym silniku volai: silnik teraz nie odpowiada, przydzielenie numeru się nie powiodło. Prosimy spróbować za chwilę lub skontaktować się z pomocą techniczną. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji): komunikat to mówi, zmiana nie została zapisana, a to samo żądanie można powtórzyć za kilka minut. - 502
engine_readback_mismatchPrzy routing.mode: "agent" dla agenta na własnym silniku volai: silnik odpowiedział, ale odczyt zwrotny pokazał inne mapowanie niż to, które wysłaliśmy. Prosimy spróbować ponownie.
GET/v1/numbers/{e164}/sip
Dane SIP numeru - dla Państwa własnego softfonu/centrali oraz dla trunku wychodzącego platformy głosowej (ElevenLabs, Vapi...). Konfigurację krok po kroku opisano na stronie SIP.
| Pole | Znaczenie |
|---|---|
server | Domena rejestracji dla Państwa własnego softfonu lub centrali (SIP REGISTER). Nie dla trunku wychodzącego platformy - zob. outboundTrunkAddress. |
outboundTrunkAddress | Adres dla trunku SIP wychodzącego Państwa platformy (ElevenLabs outbound_trunk_config.address). Należy użyć dokładnie tej wartości, niezależnie od tego, czy zgadza się z server - inny adres oznacza, że połączenie nie jest kierowane w sieci, a platforma zgłasza przekroczenie czasu. |
username / password | Dane logowania linii - te same dla REGISTER i dla trunku z uwierzytelnianiem digest. |
port / transport | Port i zalecany transport sygnalizacji SIP - TCP (duży INVITE z SDP może nie zmieścić się w MTU UDP i platforma w ogóle odmawia jego wysłania); UDP również działa. |
inboundSignallingCidrs | Skąd pochodzi nasza sygnalizacja SIP do Państwa centrali lub platformy - lista dozwolonych dla Państwa trunku przychodzącego (np. ElevenLabs allowed_addresses, bramy Vapi). |
server i outboundTrunkAddress nie są wymienne: server służy do REGISTER z softfonu lub centrali, outboundTrunkAddress do trunku wychodzącego platformy - trunk zawsze bierze wartość z outboundTrunkAddress, niezależnie od tego, czy zgadza się z server; inny adres oznacza, że połączenie nie jest kierowane w sieci, a platforma zgłasza przekroczenie czasu (szczegóły na stronie SIP i w przewodniku po własnym agencie).
Żądanie
curl "https://volai.cz/v1/numbers/+420601234567/sip" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"server": "sip.volai.cz",
"username": "123456",
"password": "a1b2c3d4e5f6",
"outboundTrunkAddress": "sip.volai.cz",
"port": 5060,
"transport": "tcp",
"inboundSignallingCidrs": ["81.31.45.0/24"]
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta. - 502
sip_credentials_unavailableNie udało się znaleźć hasła linii SIP. Prosimy spróbować za chwilę lub skontaktować się z pomocą techniczną.
GET/v1/numbers/{e164}/sip/status
Prawdziwy status rejestracji SIP numeru kierowanego do własnej centrali oraz jego ostatnie połączenie przychodzące - w przeciwieństwie do pola routing.mode jest to status PRAWDZIWIE zweryfikowany, a nie wyprowadzony z ustawień. registration ma wartość null, gdy numer wskazuje OBCĄ centralę (cel sipa: bez wyzwania) - znaczące jest tam tylko lastInbound.
Żądanie
curl "https://volai.cz/v1/numbers/+420601234567/sip/status" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"target": { "kind": "own_line", "host": null },
"registration": { "registered": true },
"lastInbound": {
"at": 1756118900000,
"status": "completed",
"from": "+420777123456",
"recent": true
}
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta. - 400
not_sip_modeNumer nie jest kierowany do "własnej centrali".
Wiadomości
SMS odbierają dwa rodzaje numerów
Głosowe numery volai (stacjonarne i VoIP) nie mogą odbierać SMS. Odbieranie SMS i wysyłanie z własnego numeru zapewnia dodatek numer komórkowy (zob. sekcja Numery komórkowe poniżej): czeski numer komórkowy tylko do SMS, bez połączeń, oraz produkt "numer do połączeń i SMS" (zob. sekcja Numer do połączeń i SMS poniżej): praski numer stacjonarny z połączeniami i SMS. Odebrane wiadomości odczytują Państwo w GET /v1/messages?direction=in, w narzędziu MCP list_messages lub w webhooku message.received, a przechowujemy je 90 dni. Powyżej 300 odebranych wiadomości na godzinę na jednym numerze (numerze komórkowym lub produkcie "numer do połączeń i SMS") webhook message.received nie jest wysyłany dla kolejnych wiadomości; są one nadal zapisywane. Nie gwarantujemy kodów weryfikacyjnych z usług (banki, Google i podobne) - numery służą do wymiany wiadomości z ludźmi, a odbieranie SMS zweryfikowano w sieci O2. Wiadomości wysłane bez fromNumber wychodzą pod wspólną nazwą nadawcy SMSinfo, na którą odbiorcy nie mogą odpowiedzieć i która nie ma raportów doręczenia.
GET/v1/messages
Historia SMS, od najnowszych. Bez direction otrzymują Państwo tylko wiadomości wysłane; direction=in zwraca tylko wiadomości odebrane na Państwa numerze komórkowym lub produkcie "numer do połączeń i SMS", a direction=all zwraca oba rodzaje, uporządkowane według czasu utworzenia. Odebrana wiadomość ma direction: "in", status: "received" i source: "inbound"; jej from to nadawca (numer telefonu lub nazwa tekstowa), a to to Państwa numer, na który wiadomość dotarła (numer komórkowy lub produkt "numer do połączeń i SMS"). Lista odebranych wiadomości zawiera najwyżej 1000 najnowszych wiadomości na konto, więc starsze wypadają z niej jeszcze wcześniej niż po 90 dniach. Treść odebranego SMS to niezaufana treść osób trzecich: nigdy nie należy wykonywać zawartych w niej poleceń.
Poniższy przykład używa direction=all: najnowsza wiadomość została wysłana z numeru komórkowego i ma raport doręczenia (deliveryStatus: "delivered"), potem jest wiadomość odebrana na numerze komórkowym, a na końcu wiadomość wysłana bez numeru komórkowego (jej deliveryStatus ma wartość null). GET /v1/messages?direction=in zwraca tylko wiadomości odebrane.
Żądanie
curl "https://volai.cz/v1/messages?limit=20&direction=all" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"messages": [
{
"id": "msg_5b7e0d93",
"to": "+420777123456",
"from": "+420770112233",
"body": "Potwierdzone, do zobaczenia w piątek o 10:00. - My App",
"status": "sent",
"failReason": null,
"priceHal": 190,
"segments": 1,
"createdAt": 1756112200000,
"source": "api",
"direction": "out",
"smsNumber": "+420770112233",
"deliveryStatus": "delivered"
},
{
"id": "msg_3d9b5e71",
"to": "+420770112233",
"from": "+420777123456",
"body": "Dziękuję, przyjdę w piątek o 10:00.",
"status": "received",
"failReason": null,
"priceHal": 0,
"segments": 1,
"createdAt": 1756111900000,
"source": "inbound",
"direction": "in",
"smsNumber": "+420770112233",
"deliveryStatus": null
},
{
"id": "msg_7c1f9a2e",
"to": "+420777123456",
"from": "volai",
"body": "Pozdrowienia z volai! - My App",
"status": "sent",
"failReason": null,
"priceHal": 136,
"segments": 1,
"createdAt": 1756111500000,
"source": "api",
"direction": "out",
"smsNumber": null,
"deliveryStatus": null
}
],
"hasMore": false,
"nextBefore": null
}Stany błędów
- 400
validationlimit musi być dodatnią liczbą całkowitą; before musi być dodatnią liczbą całkowitą (znacznik czasu w ms); direction musi mieć wartośćout,inluball.
POST/v1/messages
Wysyła SMS. Tylko na numery czeskie i słowackie (+420 / +421). Cena wynosi 1.36 CZK (~EUR 0.06) za segment - dłuższe wiadomości (lub zawierające znaki spoza alfabetu GSM-7, zwykle czeskie znaki diakrytyczne) są dzielone na więcej segmentów, z których każdy jest naliczany osobno - zob. segments w odpowiedzi. Wymaga pierwszego doładowania salda - saldo powitalne nie odblokowuje SMS.
Bez fromNumber wiadomości wychodzą pod wspólną nazwą nadawcy SMSinfo - operatorzy w sieciach czeskich dopuszczają tylko zatwierdzone nazwy nadawców, więc własna nazwa nadawcy nie jest możliwa przy wspólnym nadawcy, a odbiorcy nie mogą odpowiedzieć na SMSinfo. Pole from w odpowiedzi jest wtedy tylko wewnętrzną etykietą volai; odbiorcy nigdy jej nie widzą. W wiadomości wysłanej z numeru komórkowego from to ten numer. Własną tożsamość należy zamiast tego umieścić bezpośrednio w treści wiadomości, jak w przykładzie poniżej.
Aby wysłać z własnego numeru komórkowego, należy ustawić fromNumber (numer z GET /v1/sms-numbers). Odbiorca widzi wtedy Państwa numer zamiast SMSinfo. Wysyłka z numeru działa tylko na numery czeskie (+420), kosztuje 1.90 CZK (~EUR 0.08) za segment (bez VAT) i ma dzienny limit na konto: 20 wiadomości w pierwszych 30 dniach po zakupie numeru, potem 50. Pole from w żądaniu jest nadal ignorowane.
fromNumber przyjmuje także produkt "numer do połączeń i SMS" (numer z GET /v1/voice-sms-numbers). SMS z niego kosztuje 2.50 CZK (~EUR 0.10) za segment (bez VAT), trafia tylko na numery czeskie (+420), nie ma raportów doręczenia (deliveryStatus pozostaje null), a dzienny limit wysyłki jest wspólny z numerami komórkowymi: 20 wiadomości w pierwszych 30 dniach po zamówieniu numeru, potem 50. Odpowiedź STOP nie blokuje niczego na tym numerze (recipient_opted_out jest zwracane tylko dla numerów komórkowych), więc rezygnacje odbiorców należy śledzić we własnej aplikacji.
- Bez znaków diakrytycznych (alfabet GSM-7) jeden segment mieści 160 znaków; dłuższy tekst jest dzielony na fragmenty po 153.
- Ze znakami diakrytycznymi lub dowolnym znakiem spoza GSM-7 (UCS-2) limit wynosi 70 znaków na segment; dłuższy tekst jest dzielony na fragmenty po 67.
- Wskazówka: aby zmieścić się w jednym segmencie, należy pisać bez znaków diakrytycznych -
Prilis zlutoucky kunzamiastPříliš žluťoučký kůň.
Żądanie
curl -X POST "https://volai.cz/v1/messages" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_MESSAGES_ID" \
-d '{"to":"+420777123456","body":"Pozdrowienia z volai! - My App"}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"id": "msg_7c1f9a2e",
"status": "sent",
"priceHal": 136,
"segments": 1
}Stany błędów
- 400
validationTreść żądania nie zgadza się ze schematem (brakujetolubbody,bodyma ponad 2000 znaków,fromNumbernie jest ciągiem do 32 znaków) albotojest tym samym numerem cofromNumber(numer nie może wysłać SMS do siebie). Nie naliczamy za to opłaty. - 400
invalid_numberTo nie jest prawidłowy czeski ani słowacki numer telefonu. - 400
invalid_bodyTreść wiadomości musi mieć od 1 do 765 znaków. - 400
unsupported_countryNumer spoza CZ/SK - nie wysyłamy tam SMS. - 400
blocked_destinationCel jest numerem o podwyższonej opłacie (prefiksy +420 900, +420 906, +420 908, +420 909, +420 976 i +421 900). Nie wysyłamy do nich SMS, więc nie można niezamierzenie wygenerować rachunku. Nie naliczamy za to opłaty. - 400
recipient_cannot_receive_smsNumer stacjonarny - SMS by nie dotarł. Nie naliczamy za to opłaty. - 400
unsupported_charactersTekst zawiera emoji lub inny znak, którego sieć telefoniczna nie może dostarczyć. Nie naliczamy za to opłaty. - 400
recipient_is_virtual_numberOdbiorcą jest numer volai. SMS nigdy nie dociera na głosowy numer volai (w tym Państwa własny) - te obsługują tylko połączenia. SMS ze wspólnej nazwy SMSinfo nie dociera na numer komórkowy volai ani na produkt "numer do połączeń i SMS": należy wysłać go z własnego numeru komórkowego lub produktu "numer do połączeń i SMS" za pomocą polafromNumber. Nie naliczamy za to opłaty. - 400
recipient_rejectedSieć telefoniczna nie przyjmuje tego numeru - zwykle jest to numer wirtualny lub nieistniejący. Nie naliczamy za to opłaty. - 400
invalid_from_numberfromNumbernie jest ani aktywnym numerem komórkowym, ani aktywnym produktem "numer do połączeń i SMS" Państwa konta (nieznany numer lub numer z innego konta zwraca ten sam błąd). Należy sprawdzić go wGET /v1/sms-numbersi wGET /v1/voice-sms-numbers; numeru, którego status nie jestactive(na przykładreleasinglubawaiting_registration), nie można użyć. Nic nie zostało naliczone. - 400
from_number_unsupported_destinationWysyłka z numeru komórkowego działa tylko na numery czeskie (+420). Numer słowacki należy wysłać bezfromNumber, pod wspólną nazwą SMSinfo. Nic nie zostało naliczone. - 400
destination_not_allowedWysyłka z produktu "numer do połączeń i SMS" działa tylko na numery czeskie (+420). Numer słowacki należy wysłać bezfromNumber, pod wspólną nazwą SMSinfo. Nic nie zostało naliczone. - 402
insufficient_creditSaldo nie pokrywa wszystkich segmentów wiadomości albo konto nigdy nie było doładowane - SMS ze wspólnego nadawcy SMSinfo wymagają pierwszego doładowania, a saldo powitalne ich nie pokrywa nawet przy własnym numerze. Nie naliczamy za to opłaty. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 403
sms_numbers_unavailableWysyłka z numeru komórkowego nie jest teraz dostępna dla tego konta. Nic nie zostało naliczone; bezfromNumberwiadomość nadal może wyjść pod SMSinfo. - 403
voice_sms_numbers_unavailableWysyłka z produktu "numer do połączeń i SMS" nie jest teraz dostępna dla tego konta. Nic nie zostało naliczone; bezfromNumberwiadomość nadal może wyjść pod SMSinfo. - 409
recipient_opted_outOdbiorca odpowiedział STOP na Państwa numer komórkowy i nie przyjmuje z niego dalszych wiadomości, dopóki nie wyśle START. Ponawianie nie pomoże i nic nie zostanie naliczone. - 429
rate_limitedWięcej niż 1 SMS na 2 sekundy albo ponad 100 SMS na koncie w ciągu 24 godzin od pierwszego (komunikat podaje, jak długo do zresetowania limitu). Wysyłka z numeru komórkowego lub produktu "numer do połączeń i SMS" (fromNumber) ma dalszy dzienny limit na konto, wspólny dla obu rodzajów: 20 wiadomości w pierwszych 30 dniach po zakupie numeru, potem 50. - 502
send_failedSieć telefoniczna odrzuciła żądanie. Nie obciążyliśmy Państwa salda - prosimy spróbować ponownie. - 502
send_failed_operatorBłąd po naszej stronie (nadawca lub saldo operatora). Nie obciążyliśmy Państwa salda - prosimy spróbować za chwilę. - 502
sms_gateway_failedBrama operatora tymczasowo zawiodła. Nie obciążyliśmy Państwa salda - prosimy spróbować za kilka minut. - 502
send_unknownNie udało się wiarygodnie potwierdzić wyniku wysyłki; wiadomość mogła zostać wysłana - jest nadal naliczana, więc prosimy nie wysyłać jej ślepo ponownie. - 503
ledger_unavailableNie udało się teraz odczytać salda, prosimy spróbować za chwilę. Nic nie zostało naliczone.
status wiadomości: pending (zapisana, jeszcze w trakcie wysyłania), sent (operator przyjął wiadomość do wysłania, w tym wiadomości w kolejce; to nie jest potwierdzenie doręczenia), failed (operator ją odrzucił, w przypadku wiadomości wysłanej z numeru komórkowego możliwe, że dopiero później - nie jest naliczana, każda już pobrana opłata jest zwracana, priceHal 0), received (wiadomość odebrana na Państwa numerze komórkowym lub produkcie "numer do połączeń i SMS", zawsze z direction: "in" i priceHal 0) oraz unknown (operator nie potwierdził przyjęcia, zwykle dlatego, że jego odpowiedź przekroczyła czas) - jest nadal naliczana w pełnej cenie, ponieważ wiadomość mogła zostać dostarczona. W przypadku wiadomości wysłanej z numeru komórkowego unknown może jeszcze zmienić się samo: na sent z deliveryStatus, gdy operator potwierdzi wiadomość, albo na failed ze zwrotem opłaty, jeśli ją odrzuci. Po otrzymaniu unknown prosimy nie wysyłać wiadomości ślepo ponownie - prosimy skontaktować się z pomocą techniczną, a zwrócimy każdą zdublowaną opłatę.
Dla status: "failed" otrzymują Państwo także failReason: unsupported_characters (emoji lub znak, którego sieć nie może dostarczyć), unsupported_recipient (sieć nie przyjmuje numeru), forbidden_sender i low_balance (błąd po naszej stronie), gateway_failed (brama operatora), network_rejected (inne odrzucenie), opted_out (odbiorca odpowiedział STOP na Państwa numer komórkowy). Dla innych statusów ma wartość null.
deliveryStatus (delivered lub undelivered) to raport doręczenia operatora i istnieje tylko dla wiadomości wysłanych z Państwa numeru komórkowego. Ma wartość null dla wiadomości wysłanych pod wspólną nazwą SMSinfo, która nie ma raportów doręczenia, dla wiadomości wysłanych z produktu "numer do połączeń i SMS", który ich również nie ma, oraz dla wiadomości odebranych. sent nadal oznacza tylko, że operator przyjął wiadomość, i nie dowodzi doręczenia. Wiadomość z undelivered jest nadal naliczana, ponieważ operator ją przyjął. Jeśli operator później zgłosi niepowodzenie, status wiadomości zmieni się na failed, priceHal spadnie do 0, a opłata zostanie zwrócona na saldo. Raport doręczenia przychodzi, gdy tylko operator go zgłosi; jeśli powiadomienie o statusie nie dotrze, uzupełnia go regularna kontrola co 15 minut. Jeśli operator nigdy go nie zgłosi, pozostaje null.
Przykład: wysyłka z Państwa numeru komórkowego
Aby odpowiedzieć na odebrany SMS, należy wysłać z tego samego numeru: ustawić fromNumber na Państwa numer komórkowy z GET /v1/sms-numbers, a odbiorca zobaczy Państwa numer. Raport doręczenia przychodzi później: deliveryStatus wiadomości zaczyna jako null, a delivered lub undelivered odczytują Państwo z GET /v1/messages/{id}.
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "fromNumber": "+420770112233", "body": "Potwierdzone, do zobaczenia w piątek o 10:00. - My App"}'{
"id": "msg_5b7e0d93",
"status": "sent",
"priceHal": 190,
"segments": 1
}Przykład: wiadomość ze znakami diakrytycznymi powyżej 70 znaków oznacza więcej segmentów
Ta wiadomość ma 112 znaków i zawiera znaki diakrytyczne, więc jest liczona jako UCS-2 (limit 67 znaków na segment dla wiadomości wieloczęściowych) - wychodzi 2 segmenty, czyli 2.72 CZK (~EUR 0.11), a nie 1.36 CZK (~EUR 0.06):
curl -X POST https://volai.cz/v1/messages \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "body": "Příliš žluťoučký kůň úpěl ďábelské ódy - a tahle věta má přes sedmdesát znaků, takže spadne do druhého segmentu."}'{
"id": "msg_9a3f1c7d",
"status": "sent",
"priceHal": 272,
"segments": 2
}GET/v1/messages/{id}
Szczegóły pojedynczej wiadomości.
Żądanie
curl https://volai.cz/v1/messages/msg_7c1f9a2e \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"message": {
"id": "msg_7c1f9a2e",
"to": "+420777123456",
"from": "volai",
"body": "Pozdrowienia z volai! - My App",
"status": "sent",
"failReason": null,
"priceHal": 136,
"segments": 1,
"createdAt": 1756111500000,
"source": "api",
"direction": "out",
"smsNumber": null,
"deliveryStatus": null
}
}Stany błędów
- 404
not_foundWiadomość nie istnieje, nie należy do Państwa konta albo jest wiadomością odebraną starszą niż 90 dni.
Numery komórkowe
GET/v1/sms-numbers
Numery komórkowe na koncie (dodatek numer komórkowy): czeskie numery komórkowe, które odbierają SMS i wysyłają je z własnego numeru. Każdy zawiera status (active albo releasing, gdy trwa zwalnianie lub czeka na ponowienie), opłatę monthlyFeeHal za 30-dniowy okres oraz nextChargeAt, czyli termin kolejnej. Limit wysyłki podają dailySendLimit (20 SMS na 24-godzinne okno w pierwszych 30 dniach numeru, potem 50), dailySendRemaining (ile zostało, 0 dla numeru w trakcie zwalniania) i dailySendResetsAt (kiedy okno się kończy, null, gdy żadne nie trwa); okno jest wspólne dla wszystkich numerów komórkowych konta, zaczyna się od pierwszego wysłanego SMS i liczy także próby ponad limit oraz SMS odrzucone przez operatora. Wyświetlanie listy działa nawet wtedy, gdy nowe zakupy są niedostępne.
Żądanie
curl https://volai.cz/v1/sms-numbers \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"smsNumbers": [
{
"number": "+420770112233",
"status": "active",
"createdAt": 1756111640000,
"monthlyFeeHal": 29000,
"nextChargeAt": 1758703640000,
"dailySendLimit": 20,
"dailySendRemaining": 17,
"dailySendResetsAt": 1756198040000
}
]
}POST/v1/sms-numbers
Kupuje czeski komórkowy numer komórkowy dla konta. Nie wysyła się treści; numer wybieramy my. Numer odbiera SMS (można je odczytać przez GET /v1/messages?direction=in lub zdarzenie webhooka message.received), a wysyła się z niego przez fromNumber w POST /v1/messages. Nie wykonuje ani nie odbiera połączeń. Kwota 290.00 CZK (~EUR 12.08) bez VAT jest pobierana z salda od razu za pierwszy 30-dniowy okres i ponownie co 30 dni; opłacony okres nie jest zwracany po zwolnieniu numeru. Konto może mieć najwyżej dwa numery komórkowe, a zakup wymaga pierwszego doładowania salda. Jeśli po zakupie coś zawiedzie u operatora, numer jest ponownie zwalniany i nic nie jest naliczane. Dopóki trwa inny zakup dla konta, otrzymują Państwo 429 rate_limited; należy odczekać kilka sekund i powtórzyć. Obsługuje Idempotency-Key: po przekroczeniu czasu należy wysłać tę samą wartość, aby ponowienie nie kupiło drugiego numeru. Dostarczenie wiadomości od każdego nadawcy, w tym kodów weryfikacyjnych, nie jest gwarantowane.
Żądanie
curl -X POST "https://volai.cz/v1/sms-numbers" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_SMS_NUMBERS_ID"Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
201 Created
{
"smsNumber": {
"number": "+420770112233",
"status": "active",
"createdAt": 1756111640000,
"monthlyFeeHal": 29000,
"nextChargeAt": 1758703640000,
"dailySendLimit": 20,
"dailySendRemaining": 20,
"dailySendResetsAt": null
}
}Stany błędów
- 402
insufficient_creditSaldo nie pokrywa opłaty 290.00 CZK (~EUR 12.08) za pierwszy okres albo konto nigdy nie było doładowane - numer komórkowy można kupić dopiero po pierwszym doładowaniu, a saldo powitalne go nie odblokowuje. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 403
sms_numbers_unavailableDodatek numer komórkowy nie jest teraz dostępny dla tego konta. Nic nie zostało naliczone; jeśli spodziewali się Państwo dostępu, prosimy skontaktować się z pomocą techniczną. - 409
sms_number_limitKonto ma już maksymalną liczbę numerów komórkowych (dwa). Jeden należy zwolnić przezDELETE /v1/sms-numbers/{e164}i kupić ponownie. Nic nie zostało naliczone. - 429
rate_limitedInny zakup numeru na tym koncie (głosowego lub SMS) wciąż trwa. Należy odczekać kilka sekund i powtórzyć to samo żądanie. Nic nie zostało naliczone. - 503
sms_number_out_of_stockOperator nie ma teraz wolnego czeskiego numeru komórkowego. Nic nie zostało naliczone; prosimy spróbować później. - 503
ledger_unavailableNie udało się teraz odczytać salda, prosimy spróbować za chwilę. Nic nie zostało naliczone.
DELETE/v1/sms-numbers/{e164}
Zwalnia numer komórkowy: wraca do operatora i nie można go odzyskać, a wiadomości wysłane na niego później już do Państwa nie dotrą. Wiadomości już odebrane pozostają na liście do końca swoich 90 dni. Opłata za trwający 30-dniowy okres nie jest zwracana. Jeśli zwolnienie u operatora się nie powiedzie, numer pozostaje w status: "releasing"; aby go zwolnić, należy wysłać to samo żądanie ponownie.
Żądanie
curl -X DELETE "https://volai.cz/v1/sms-numbers/+420770112233" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"released": true
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu (znak plus należy zapisać dosłownie lub jako %2B). - 404
not_foundNumer komórkowy nie istnieje albo nie należy do Państwa konta. - 502
release_failedNie udało się teraz dokończyć zwolnienia u operatora. Numer pozostaje Państwa wstatus: "releasing"; prosimy wysłać to samo żądanie ponownie za chwilę. Opłacony już okres nie jest zwracany.
Numer do połączeń i SMS
Produkt "numer do połączeń i SMS" to trzeci rodzaj numeru obok numerów głosowych i numerów komórkowych: praski numer stacjonarny (+420 2xx), który odbiera połączenia i SMS i z którego Państwa agent wykonuje połączenia i wysyła SMS. Numer głosowy nie może odbierać SMS, a numer komórkowy nie ma połączeń. Numer jest zarejestrowany u operatora na nazwę dostawcy usługi volai, a Państwo z niego korzystają; po zamówieniu jest zwykle aktywny w ciągu 1 dnia roboczego, najpóźniej w ciągu 48 godzin, a opłata jest pobierana dopiero po aktywacji. Połączenia przychodzące odbiera agent działający na własnym silniku głosowym volai, a numer obsługuje najwyżej dwa jednoczesne połączenia. Odbieranie SMS zweryfikowano w sieci O2 i nie gwarantujemy kodów weryfikacyjnych z usług. Z country w treści zamówienia otrzymują Państwo zamiast numeru praskiego produkt "numer w innym kraju": numer stacjonarny, który odbiera połączenia (odpowiada na nie agent), ale nie ma SMS (ani ich nie odbiera, ani nie wysyła) i który dzwoni tylko do kraju numeru i do Czech. Cena zależy od kraju (odczytują ją Państwo z monthlyFeeHal kraju w GET /v1/voice-sms-numbers/countries), a pozostałe zasady są takie same jak dla numeru praskiego; numer jest rejestrowany u operatora na nazwę dostawcy z adresem w tym kraju i jest aktywny zwykle w ciągu 1 dnia roboczego, najpóźniej w ciągu 3 dni roboczych (w ciągu godziny dla krajów bez rejestracji). Kraje, w których można zamawiać, się zmieniają, dlatego należy odczytywać je z GET /v1/voice-sms-numbers/countries (MCP list_voice_sms_countries) i nigdy nie wymieniać ich z pamięci. Zamawiają go Państwo sami w portalu, przez REST (POST /v1/voice-sms-numbers) lub MCP (order_voice_sms_number), po pierwszym doładowaniu salda. Gdy zamówienia są tymczasowo wstrzymane, zamówienie zwraca voice_sms_numbers_unavailable i nic nie jest naliczane; prosimy spróbować później lub skontaktować się z pomocą techniczną.
GET/v1/voice-sms-numbers
Produkty "numer do połączeń i SMS" i "numer w innym kraju" na koncie: praskie numery stacjonarne, które odbierają połączenia i SMS i z których Państwo dzwonią i wysyłają SMS, oraz numery stacjonarne w innych krajach, które odbierają połączenia i z których dzwoni agent, bez SMS. Każdy zawiera country (kod ISO), capabilities (calls, sms i outboundTo: kraje, do których agent dzwoni z numeru) oraz status (awaiting_registration, active, rejected lub releasing), routing (gdzie dzwonią połączenia przychodzące), opłatę monthlyFeeHal za 30-dniowy okres, cenę smsFromNumberHal jednego segmentu SMS wysłanego z numeru (null dla numeru bez SMS), nextChargeAt (termin kolejnej opłaty, null, dopóki numer nie jest aktywny), smsInboundReady (true, gdy numer jest aktywny i odbieranie SMS jest skonfigurowane) oraz czasy createdAt i activatedAt. pendingOrders wymienia zamówienia, które przyjęto, ale nie przydzielono im numeru (po tymczasowym problemie u operatora); każde zawiera tylko createdAt. Takie zamówienie kończy się samo, a numer pojawia się wtedy wśród numerów, i do tego czasu liczy się do limitów, więc nie należy zamawiać ponownie. Każdy wpis zawiera country zamówienia. Jeśli zamówienie się nie powiedzie, znika z pendingOrders bez numeru, nic nie jest naliczane, a konto dostaje e-mail; można wtedy zamówić ponownie. Wyświetlanie listy działa nawet wtedy, gdy nowe zamówienia są niedostępne.
Żądanie
curl https://volai.cz/v1/voice-sms-numbers \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"voiceSmsNumbers": [
{
"number": "+420292000001",
"country": "CZ",
"capabilities": { "calls": true, "sms": true, "outboundTo": ["CZ"] },
"status": "active",
"routing": { "mode": "agent", "agentId": "ag_9f3a2b7c4d1e", "sipTarget": "engine", "hasSipPassword": false },
"monthlyFeeHal": 4900,
"smsFromNumberHal": 250,
"nextChargeAt": 1758703640000,
"smsInboundReady": true,
"createdAt": 1756101640000,
"activatedAt": 1756111640000
}
],
"pendingOrders": [{ "createdAt": 1758790000000, "country": "CZ" }]
}GET/v1/voice-sms-numbers/countries
Kraje, w których można teraz zamówić numer, w kolejności oferty (Czechy pierwsze). Każdy wpis zawiera country (kod ISO przekazywany jako country do POST /v1/voice-sms-numbers), name, prefix, numberKind, sms (true tylko dla numeru czeskiego, numer poza Czechami obsługuje wyłącznie połączenia), outboundTo (kraje, do których agent może dzwonić z numeru), activation (within_hour lub within_business_days) i monthlyFeeHal (cenę numeru w danym kraju za 30 dni w halerzach, bez VAT; różni się w zależności od kraju, więc tę kwotę należy pokazać użytkownikowi przed zamówieniem). Oferta zależy od konta i zmienia się w czasie, dlatego należy ją odczytać, a nie zakładać kraj. Tylko do odczytu, nic nie jest naliczane.
Żądanie
curl https://volai.cz/v1/voice-sms-numbers/countries \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"countries": [
{
"country": "CZ",
"name": "Czechia",
"prefix": "+420",
"numberKind": "landline, Prague (+420 2)",
"sms": true,
"outboundTo": ["CZ"],
"activation": "within_business_days",
"monthlyFeeHal": 4900
},
{
"country": "GB",
"name": "United Kingdom",
"prefix": "+44",
"numberKind": "landline, London (+44 20)",
"sms": false,
"outboundTo": ["CZ"],
"activation": "within_hour",
"monthlyFeeHal": 2900
}
]
}POST/v1/voice-sms-numbers
Zamawia dla konta praski produkt "numer do połączeń i SMS" albo z country produkt "numer w innym kraju". Treść jest opcjonalna: bez treści (lub z {"country": "CZ"}) otrzymują Państwo numer praski, {"country": "<kod ISO>"} zamawia produkt "numer w innym kraju". Kraj należy zawsze wybierać z GET /v1/voice-sms-numbers/countries; kraj spoza oferty zwraca 400 country_not_offered i nic nie jest naliczane. Produkt "numer w innym kraju" służy wyłącznie do połączeń, bez SMS: ani ich nie odbiera, ani nie wysyła, dzwoni tylko do kraju numeru i do Czech, a aktywny jest najpóźniej 3 dni robocze po zamówieniu (w ciągu godziny dla krajów bez rejestracji). Cena produktu "numer w innym kraju" zależy od kraju: to monthlyFeeHal tego kraju z GET /v1/voice-sms-numbers/countries, opłata jest pobierana dopiero po aktywacji, a pozostałe zasady są takie same jak dla numeru praskiego. Numer jest przydzielany automatycznie i nie można go wybrać. Numer praski staje się aktywny po rejestracji u operatora, zwykle w ciągu jednego dnia roboczego (najwyżej 48 godzin): do tego czasu jego status to awaiting_registration, nie dzwoni ani nie odbiera SMS, a nic nie jest naliczane. Opłata 49.00 CZK (~EUR 2.04) bez VAT za 30 dni jest pobierana z salda dopiero wtedy, gdy numer stanie się aktywny, i ponownie co 30 dni; opłacony okres nie jest zwracany po zwolnieniu numeru. Połączenia wykonują Państwo z niego przez agenta (POST /v1/calls z agentId) i, z numeru praskiego, wysyłają SMS (fromNumber w POST /v1/messages, 2.50 CZK (~EUR 0.10) za segment, tylko na numery czeskie i bez raportów doręczenia); połączenia przychodzące kierują Państwo przez PATCH /v1/voice-sms-numbers/{e164}. Konto może mieć najwyżej dwa takie numery w jednym kraju i najwyżej cztery łącznie (zamówienie, które wciąż czeka na rejestrację lub na przydzielenie numeru, też się liczy), a zamówienie wymaga pierwszego doładowania salda. Odpowiedź to 202; voiceSmsNumber ma wartość null, gdy tymczasowy problem u operatora zostawił zamówienie bez przydzielonego numeru: zamówienie jest kontynuowane w tle, jest widoczne w pendingOrders w GET /v1/voice-sms-numbers, dopóki numer nie zostanie przydzielony, a następnie pojawia się tam wśród numerów, więc nie należy zamawiać ponownie. Jeśli zamówienie się nie powiedzie, znika z pendingOrders bez numeru, nic nie jest naliczane, a konto dostaje e-mail; można wtedy zamówić ponownie. Obsługuje Idempotency-Key: po przekroczeniu czasu należy wysłać tę samą wartość, aby ponowienie nie utworzyło drugiego zamówienia. W przypadku numeru praskiego odbieranie SMS zweryfikowano tylko w sieci O2, a dostarczenie wiadomości od każdego nadawcy, w tym kodów weryfikacyjnych, nie jest gwarantowane.
# Example: a number in another country (choose the country from GET /v1/voice-sms-numbers/countries)
curl -X POST https://volai.cz/v1/voice-sms-numbers \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"country":"GB"}'{
"voiceSmsNumber": {
"number": "+442000000001",
"country": "GB",
"capabilities": { "calls": true, "sms": false, "outboundTo": ["CZ"] },
"status": "awaiting_registration",
"routing": { "mode": "none", "hasSipPassword": false },
"monthlyFeeHal": 2900,
"smsFromNumberHal": null,
"nextChargeAt": null,
"smsInboundReady": false,
"createdAt": 1756101640000,
"activatedAt": null
}
}Żądanie
curl -X POST "https://volai.cz/v1/voice-sms-numbers" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_VOICE_SMS_NUMBERS_ID"Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
202 Accepted
{
"voiceSmsNumber": {
"number": "+420292000001",
"country": "CZ",
"capabilities": { "calls": true, "sms": true, "outboundTo": ["CZ"] },
"status": "awaiting_registration",
"routing": { "mode": "none", "hasSipPassword": false },
"monthlyFeeHal": 4900,
"smsFromNumberHal": 250,
"nextChargeAt": null,
"smsInboundReady": false,
"createdAt": 1756101640000,
"activatedAt": null
}
}Stany błędów
- 400
validationTreść nie jest prawidłowym obiektem JSON albocountrynie jest dwuliterowym kodem ISO wielkimi literami (na przykładPL). Nic nie zostało naliczone. - 400
country_not_offeredTo konto nie może teraz zamawiać w żądanymcountry, ponieważ nie ma go w ofercie.GET /v1/voice-sms-numbers/countrieszwraca aktualną ofertę. Nic nie zostało naliczone. - 402
insufficient_creditSaldo nie pokrywa opłaty za pierwszy okres (49.00 CZK (~EUR 2.04) za numer praski, od 29.00 CZK (~EUR 1.21) zależnie od kraju za numer w innym kraju) albo konto nigdy nie było doładowane - produkt "numer do połączeń i SMS" można zamówić dopiero po pierwszym doładowaniu, a saldo powitalne go nie odblokowuje. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 403
voice_sms_numbers_unavailableProdukt "numer do połączeń i SMS" nie jest teraz dostępny dla tego konta: funkcja jest dla niego wyłączona albo nowe zamówienia są wstrzymane po problemie z rejestracją. Ponawianie nie pomoże; nic nie zostało naliczone, prosimy skontaktować się z pomocą techniczną. - 409
voice_sms_number_limitKonto ma już maksymalną liczbę produktów "numer do połączeń i SMS" (dwa w jednym kraju, cztery łącznie na koncie); zamówienie, które wciąż czeka na rejestrację lub nie ma przydzielonego numeru (pendingOrdersna liście), też się liczy. Jeden należy zwolnić (DELETE /v1/voice-sms-numbers/{e164}) albo anulować zamówienie czekające na rejestrację (POST /v1/voice-sms-numbers/{e164}/cancel) i zamówić ponownie; zamówienie bez przydzielonego numeru kończy się samo i nie można go anulować. Nic nie zostało naliczone. - 409
idempotency_conflictTenIdempotency-Keyzostał już użyty dla innego zamówienia. - 429
rate_limitedNa tym koncie trwa już inne zamówienie numeru (głosowego, SMS albo połączeń i SMS). Należy odczekać kilka sekund i powtórzyć to samo żądanie. Nic nie zostało naliczone. - 429
voice_sms_order_limitW ciągu ostatnich 30 dni konto anulowało dwa zamówienia produktu "numer do połączeń i SMS", więc teraz nie można złożyć kolejnego; limit zatrzymuje pętle zamawiania i anulowania. Należy poczekać, aż minie 30-dniowe okno, lub skontaktować się z pomocą techniczną. Nic nie zostało naliczone. - 503
voice_sms_number_out_of_stockOperator nie ma teraz wolnego numeru w żądanym kraju (numeru praskiego, gdycountryjest pominięte). Nic nie zostało naliczone; prosimy spróbować później. - 503
order_failedNie udało się skonfigurować numeru u operatora (tymczasowy problem lub odrzucona rejestracja), więc zamówienia nie dało się zrealizować. Nic nie zostało naliczone; prosimy spróbować później lub skontaktować się z pomocą techniczną. - 503
ledger_unavailableNie udało się teraz odczytać salda, prosimy spróbować za chwilę. Nic nie zostało naliczone.
GET/v1/voice-sms-numbers/{e164}
Szczegóły jednego produktu "numer do połączeń i SMS" w tym samym kształcie co pozycja listy. Numer innego konta i nieistniejący numer zwracają ten sam błąd 404.
Żądanie
curl "https://volai.cz/v1/voice-sms-numbers/+420292000001" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"voiceSmsNumber": {
"number": "+420292000001",
"country": "CZ",
"capabilities": { "calls": true, "sms": true, "outboundTo": ["CZ"] },
"status": "active",
"routing": { "mode": "none", "hasSipPassword": false },
"monthlyFeeHal": 4900,
"smsFromNumberHal": 250,
"nextChargeAt": 1758703640000,
"smsInboundReady": true,
"createdAt": 1756101640000,
"activatedAt": 1756111640000
}
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu (znak plus należy zapisać dosłownie lub jako %2B). - 404
not_foundProdukt "numer do połączeń i SMS" nie istnieje albo nie należy do Państwa konta.
PATCH/v1/voice-sms-numbers/{e164}
Ustawia, gdzie dzwonią połączenia przychodzące na produkt "numer do połączeń i SMS": routing ma postać {"mode": "agent", "agentId": "ag_..."} (odpowiada agent działający na własnym silniku głosowym volai) lub {"mode": "none"} (połączenia nie są odbierane, a agent jest odłączony). Przekierowanie na inny numer (forward) i własny serwer SIP (sip) nie są obsługiwane dla tego typu numeru i zwracają unsupported_for_number_type, podobnie jak agent, który nie działa na własnym silniku. Agenta można podłączyć tylko do aktywnego numeru (invalid_transition); wyłączenie kierowania (none) działa w każdym statusie. Gdy numer jest właśnie przetwarzany przez inną operację (aktywację, rozliczenie lub zwolnienie), otrzymują Państwo 409 in_progress: nic nie zostało zmienione, a żądanie można powtórzyć za minutę. Do usługi trafiają tylko mode i agentId; pozostałe pola treści są odrzucane.
Żądanie
curl -X PATCH "https://volai.cz/v1/voice-sms-numbers/+420292000001" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"routing":{"mode":"agent","agentId":"ag_9f3a2b7c4d1e"}}'Odpowiedź
{
"voiceSmsNumber": {
"number": "+420292000001",
"country": "CZ",
"capabilities": { "calls": true, "sms": true, "outboundTo": ["CZ"] },
"status": "active",
"routing": { "mode": "agent", "agentId": "ag_9f3a2b7c4d1e", "sipTarget": "engine", "hasSipPassword": false },
"monthlyFeeHal": 4900,
"smsFromNumberHal": 250,
"nextChargeAt": 1758703640000,
"smsInboundReady": true,
"createdAt": 1756101640000,
"activatedAt": 1756111640000
}
}Stany błędów
- 400
validationTreść nie jest prawidłowym obiektem JSON zrouting,modenie jest aniagent, aninone, brakujeagentIddlaagentlub ma więcej niż 64 znaki, albo e164 w URL nie jest prawidłowym numerem telefonu. - 400
unsupported_for_number_typeTrybforwardlubsipalbo agent, który nie działa na własnym silniku głosowym volai. Należy wybrać agenta na silniku albonone. Nic nie zostało zmienione. - 400
destination_not_allowedAgent ma numer przełączenia spoza+420, na który produkt "numer do połączeń i SMS" nie może przełączać. Należy zmienić numer przełączenia agenta na czeski albo go usunąć, a potem podłączyć numer. Dla produktu "numer w innym kraju" numer przełączenia musi znajdować się w kraju numeru (gdy dzwonienie tam jest włączone) lub w Czechach. Nic nie zostało zmienione. - 404
not_foundNumer nie istnieje albo nie należy do Państwa konta, alboagentIdnie jest aktywnym agentem Państwa konta. - 409
invalid_transitionNumer nie jest aktywny: czeka na rejestrację, jego rejestracja została odrzucona albo jest zwalniany. Agenta można podłączyć tylko do aktywnego numeru (nonedziała zawsze). Status znajduje się wGET /v1/voice-sms-numbers/{e164}. - 409
in_progressNumer jest przetwarzany przez inną operację (aktywację, rozliczenie lub zwolnienie). Nic nie zostało zmienione; prosimy powtórzyć to samo żądanie za minutę. - 502
routing_failedOdłączenie agenta we własnym silniku głosowym się nie powiodło; numer pozostaje skierowany tak jak wcześniej. Prosimy powtórzyć żądanie za chwilę. - 502
engine_unavailableWłasny silnik głosowy teraz nie odpowiada, agent nie został podłączony do numeru. Prosimy spróbować za chwilę lub skontaktować się z pomocą techniczną. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji): komunikat to mówi, zmiana nie została zapisana, a to samo żądanie można powtórzyć za kilka minut. - 502
engine_readback_mismatchSilnik odpowiedział, ale odczyt zwrotny pokazał inne mapowanie niż to, które wysłaliśmy. Prosimy spróbować ponownie.
POST/v1/voice-sms-numbers/{e164}/cancel
Anuluje produkt "numer do połączeń i SMS", który wciąż czeka na rejestrację u operatora (awaiting_registration): numer jest anulowany i nic Państwo nie płacą, ponieważ opłata jest pobierana dopiero po aktywacji. Aktywnego numeru nie można anulować (409 invalid_transition); zwalnia się go przez DELETE /v1/voice-sms-numbers/{e164}. Anulowanie wlicza się do limitu dwóch anulowanych zamówień w 30 dni; po jego osiągnięciu POST /v1/voice-sms-numbers zwraca voice_sms_order_limit. Bez treści.
Żądanie
curl -X POST "https://volai.cz/v1/voice-sms-numbers/+420292000001/cancel" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"cancelled": true
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu (znak plus należy zapisać dosłownie lub jako %2B). - 404
not_foundProdukt "numer do połączeń i SMS" nie istnieje albo nie należy do Państwa konta. - 409
invalid_transitionNumer jest już aktywny (opłacony; zwalnia się go przezDELETE), odrzucony (nie ma czego anulować) albo zwalniany. - 409
in_progressNumer jest właśnie aktywowany albo jego zamówienie przetwarza inna operacja. Prosimy spróbować za kilka minut; gdy będzie aktywny, można go zwolnić. - 500
internal_errorNie udało się teraz potwierdzić anulowania u operatora. Zamówienie pozostaje w oczekiwaniu i nic nie jest naliczane; prosimy powtórzyć żądanie za chwilę.
DELETE/v1/voice-sms-numbers/{e164}
Zwalnia aktywny produkt "numer do połączeń i SMS": odłącza agenta, wraca do operatora i nie można go odzyskać. Opłata za trwający 30-dniowy okres nie jest zwracana. Numer czekający na rejestrację nie jest zwalniany, lecz anulowany (POST /v1/voice-sms-numbers/{e164}/cancel). Jeśli zwolnienie się nie powiedzie (odłączenie agenta w silniku lub anulowanie u operatora), zwracane jest 502 release_failed: numer pozostaje active albo przechodzi do releasing, w którym już nie dzwoni i nie może dzwonić ani wysyłać, a ponowne wysłanie tego samego żądania kończy zwolnienie.
Żądanie
curl -X DELETE "https://volai.cz/v1/voice-sms-numbers/+420292000001" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"released": true
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu (znak plus należy zapisać dosłownie lub jako %2B). - 404
not_foundProdukt "numer do połączeń i SMS" nie istnieje albo nie należy do Państwa konta. - 409
invalid_transitionNumer czeka na rejestrację (należy go anulować przezPOST /v1/voice-sms-numbers/{e164}/cancel, nic Państwo nie płacą) albo operator odrzucił rejestrację i numer jest już anulowany. - 409
in_progressNumer jest przetwarzany przez inną operację (aktywację, anulowanie lub inne zwolnienie). Prosimy spróbować za kilka minut. - 502
release_failedNie udało się teraz dokończyć zwolnienia. Numer pozostajeactivealboreleasing(nie można go już używać); prosimy wysłać to samo żądanie ponownie za chwilę. Opłacony już okres nie jest zwracany.
Połączenia
GET/v1/calls
Historia połączeń, od najnowszych. direction (in lub out) ogranicza listę do jednego kierunku. Filtry from/to (Unix ms, startedAt w zakresie, from <= to), agentId, flagged (true/false), goal (success/failure/unclear/no_conversation/not_evaluated lub wstecznie zgodne unknown - rekordy bez jasnego wyniku: goal.result brakuje lub ma wartość unknown (zawsze prawda dla unclear i not_evaluated, a dla no_conversation tylko wtedy, gdy jego wynik to unknown) - zob. pole goal połączenia), outcome (agent/transferred/no_answer/other, zob. kolumna Rozliczenie w tabeli kind poniżej; dzwonek bramy, kind: "ring", to zawsze other, nawet gdy jego status to no_answer). Nieznane parametry są ignorowane. Połączenia a rozmowy: GET /v1/calls zwraca poszczególne REKORDY połączeń - każda noga telefoniczna to osobny wiersz. Tabela /hovory w portalu grupuje powiązane nogi (przychodzącą nogę agenta i wychodzącą nogę przełączenia) w jedną ROZMOWĘ - pomyślnie przełączone połączenie jest więc w portalu jednym wierszem, ale przez API/MCP maksymalnie dwoma rekordami (noga, która wykonała przełączenie, ma outcome: "transferred"; noga przyjmująca zachowuje własne outcome). Liczby połączeń z API należy sumować z uwzględnieniem tego.
Żądanie
curl "https://volai.cz/v1/calls?limit=20" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"calls": [
{
"id": "c_8f2ac1d4",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 236,
"agentId": "ag_kx91fa2b",
"note": null,
"flag": null,
"handledAt": null,
"rating": null,
"goal": null,
"hasConversation": true,
"source": "inbound",
"kind": "inbound",
"endReason": "completed",
"hasRecording": true,
"data": {
"name": "Anna Kowalska",
"coffee_count": 2,
"urgency": "normal"
}
}
],
"hasMore": false,
"nextBefore": null
}Stany błędów
- 400
validationlimit/before musi być dodatnią liczbą całkowitą, direction musi mieć wartość "in" lub "out"; from musi być mniejsze lub równe to, gdy wysłano oba; agentId nie może przekraczać 64 znaków; flagged/goal/outcome poza dozwolonymi wartościami.
POST/v1/calls
Rozpoczyna połączenie wychodzące do to. Należy wysłać dokładnie JEDNO z: agentId (Państwa agent głosowy prowadzi je zgodnie ze swoim systemPrompt), from (bezpośrednie połączenie dwóch numerów, bez agenta - most, zob. niżej) lub systemPrompt (połączenie próbne bez własnego numeru - bez zakupu ani konfiguracji agenta, zob. niżej). Nigdy więcej niż jedno, nigdy żadne. Przy wariancie z agentId można też wysłać variables (własne wartości dla promptu) i ringingTimeoutSecs (jak długo pozwolić dzwonić, zob. ramka poniżej).
Żądanie
curl -X POST "https://volai.cz/v1/calls" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_CALLS_ID" \
-d '{"to":"+420777123456","agentId":"ag_kx91fa2b","variables":{"jmeno_zakaznika":"Jana","cislo_objednavky":"A-42"},"ringingTimeoutSecs":30}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"id": "c_9d4e2b7f",
"status": "initiated"
}Stany błędów
- 400
invalid_numberto nie jest prawidłowym numerem telefonu. - 400
validationBrakuje albo jest więcej niż jedno z agentId/from/systemPrompt; przy agentId variables przekracza limity (maks. 20 kluczy, klucze do 64 znaków, wartości do 512 znaków) lub ringingTimeoutSecs jest poza zakresem 5-60; przy systemPrompt brakuje go albo ma ponad 1200 znaków, firstMessage ma ponad 300 znaków albo ringingTimeoutSecs jest poza zakresem 5-30. - 400
on_dncTen numer znajduje się na Państwa liście zakazu dzwonienia (/v1/dnc). - 400
destination_auto_blockedCel został automatycznie zablokowany na 30 dni po powtarzających się niepowodzeniach - blokadę można zdjąć w portalu (Ustawienia) lub przezPOST /v1/dnc/{e164}/unblock. - 400
cannot_call_own_numberOchrona przed pętlą - nie można zadzwonić na własny numer volai. - 400
cannot_call_volai_numberOchrona przed pętlą - cel jest numerem volai innego konta. - 400
destination_not_allowedProdukt "numer do połączeń i SMS" może dzwonić tylko na numery czeskie (+420); produkt "numer w innym kraju" może dzwonić tylko do kraju numeru (gdy dzwonienie tam jest włączone, zob.capabilities.outboundTonumeru) i do Czech.toto inny numer. Należy podać dozwolonetoalbo dzwonić od agenta używającego numeru głosowego. Nic nie zostało naliczone. - 400
unsupported_for_number_typeWariantfrom(zwykłe połączenie dwóch numerów bez agenta) nie jest dostępny z produktu "numer do połączeń i SMS":fromto Państwa własny produkt "numer do połączeń i SMS", a połączenia z niego wykonuje wyłącznie agent. Jakofromnależy podać własny telefon (numer czeski lub słowacki) albo wysłaćagentId. Nic nie zostało naliczone. - 404
bridge_needs_numberMost (from) wymaga co najmniej jednego Państwa własnego numeru głosowego na koncie. Produkt "numer do połączeń i SMS" się nie liczy: połączenia z niego wykonuje wyłącznie agent. Numer głosowy można kupić przezPOST /v1/numbers. - 409
destination_busyTrwa już inne połączenie na ten sam numer. - 404
agent_not_foundagentId nie istnieje albo nie należy do Państwa konta. - 404
agent_no_numberAgent nie ma przypisanego numeru telefonu. - 403
agent_suspendedOperator volai ręcznie zawiesił tego agenta - w tej chwili nie można przez niego wykonać żadnego połączenia. Zawieszone konto zwraca zamiast tegoaccount_suspended. - 402
insufficient_creditSaldo nie pokrywa minimum potrzebnego do rozpoczęcia połączenia. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 503
capacity_busyWszystkie linie wychodzące są teraz zajęte, prosimy spróbować za minutę. Produkt "numer do połączeń i SMS" dopuszcza tylko jedno połączenie wychodzące naraz: gdy jedno już trwa, odpowiedź zawiera nagłówekRetry-Afterz liczbą sekund do kolejnej próby. - 503
trial_calls_disabledZ systemPrompt: połączenia próbne są tymczasowo wyłączone przełącznikiem operacyjnym - zamiast tego należy zadzwonić przez własnego agenta (agentId). - 403
trial_email_unverifiedZ systemPrompt: tylko dla kont ze zweryfikowanym adresem e-mail. - 500
trial_not_configuredZ systemPrompt: wspólny agent demonstracyjny volai nie jest poprawnie skonfigurowany - tymczasowy błąd po naszej stronie. - 502
call_rejectedZ systemPrompt: platforma głosowa od razu odrzuciła połączenie, nic nie zostało naliczone. - 429
rate_limitedLimit połączeń, nic nie zostało naliczone. Z agentId: najwyżej 10 połączeń na minutę i 1000 na dobę na konto albo wyczerpany limit minut wychodzących własnego silnika głosowego - odpowiedź zawiera wtedyRetry-After(sekundy, po których warto ponowić). Z systemPrompt: wyczerpany dzienny limit 3 połączeń próbnych na konto albo tymczasowo nasycony wspólny limit dla wszystkich kont. - 502
callback_failedZ mostem (from): operator telefoniczny od razu odrzucił żądanie połączenia, połączenie nie zostało zestawione. Saldo nie zostało obciążone. - 502
engine_unavailableZ agentId dla agenta na własnym silniku głosowym volai: silnik teraz nie odpowiada (awaria, nie limit) - prosimy spróbować za chwilę, a jeśli problem trwa, skontaktować się z pomocą techniczną, podając requestId z błędu. Jeśli silnik przyjął żądanie, ale jego odpowiedź nie dotarła na czas, połączenie mogło jednak nastąpić: pozostaje w historii jakoinitiated, uzupełniamy jego rzeczywisty wynik i cenę, a ten sam numer pozostaje zablokowany przez około 15 minut (destination_busy), aby nie zadzwonić na niego dwa razy. Wyczerpany limit połączeń wychodzących to zamiast tego 429rate_limited.
Własne zmienne i czas dzwonienia
variables to obiekt ciąg-ciąg, który agent otrzymuje jako wartości dynamiczne - w prompcie odwołują się do nich Państwo jako {{jmeno_zakaznika}}. Limity: najwyżej 20 kluczy, klucze do 64 znaków, wartości do 512 znaków. Klucz może zawierać tylko litery, cyfry i podkreślenia, nie może zaczynać się od cyfry ani od prefiksu system__ lub volai_ (oba są zarezerwowane, a żądanie z takim kluczem jest odrzucane). Nazwy attempt_id, caller_number, called_number, trial_prompt i trial_first_message są zarezerwowane - wypełniamy je sami, a wszelkie wartości przesłane pod tymi nazwami są odrzucane.
ringingTimeoutSecs wynosi od 5 do 60 sekund, domyślnie 25. Po jego upływie nieodebrane połączenie się kończy, a jego szczegóły dostają endReason: "no_answer". Dłuższe dzwonienie to większa szansa, że klient odbierze, ale też dłużej zajęty slot wychodzący.
Bez agenta: bezpośrednie łączenie dwóch numerów
Zamiast agentId należy wysłać from - Państwa własny numer lub inny numer należący do klienta. volai dzwoni najpierw na from, a gdy ktoś odbierze, wybiera to. Obie nogi są naliczane osobno po 0.92 CZK/min (~EUR 0.04). Jeśli operator nie odpowie na zlecenie (awaria sieci), zwracamy połączenie jako rozpoczęte (initiated): mogło dojść do skutku, a jego rzeczywisty wynik i cenę uzupełnia synchronizacja z operatorem. Nie należy ponawiać go od razu - ten sam numer pozostaje zablokowany przez około 10 minut.
curl -X POST https://volai.cz/v1/calls \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "from": "+420601234567"}'Połączenie próbne bez własnego numeru
Zamiast agentId/from należy wysłać systemPrompt (instrukcje dla agenta, tylko na to jedno połączenie, do 1200 znaków) i opcjonalnie firstMessage (pierwsza kwestia, do 300 znaków). Połączenie wychodzi ze wspólnego numeru demonstracyjnego volai, a nie z Państwa własnego - dlatego odpowiedź zawiera też trial: true i from (numer demonstracyjny). Dla tego wariantu nie można wysłać voiceId, działa na głosie domyślnym. ringingTimeoutSecs ma niższy limit od 5 do 30 (domyślnie 25) - zajmuje slot ze wspólnej puli.
curl -X POST https://volai.cz/v1/calls \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+420777123456",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty, odpowiadasz krótko i uprzejmie.",
"firstMessage": "Dzień dobry, tu próbny agent volai, w czym mogę pomóc?"
}'{
"id": "c_2f7a91mn",
"status": "initiated",
"trial": true,
"from": "+420266266641"
}Tylko dla kont ze zweryfikowanym adresem e-mail, najwyżej 3 połączenia na konto w ciągu 24 godzin (plus wspólny limit dla wszystkich kont, aby nagły wzrost nie przeciążył numeru demonstracyjnego). Naliczane dokładnie jak zwykłe połączenie, wraz z dopłatą za agenta - bez zniżki. Kto potrzebuje więcej, kupuje własny numer (POST /v1/numbers) i tworzy agenta (POST /v1/agents) - ten limit tam nie obowiązuje.
Jeśli połączenia próbne są tymczasowo wyłączone (przełącznik operacyjny), systemPrompt zwraca czytelny błąd (trial_calls_disabled) zamiast wykonać połączenie; w międzyczasie należy użyć własnego agenta (agentId).
POST/v1/voice-messages
Dzwoni na to, odczytuje text słowo w słowo (od 1 do 500 znaków, repeat od 1 do 3 razy, domyślnie 1) i się rozłącza. Bez agenta AI i bez własnego numeru: odbiorca widzi wspólny numer volai, a oddzwonienie na niego trafia na linię demonstracyjną volai. Naliczana jest wyłącznie minuta wychodząca, co do sekundy (0.92 CZK/min bez VAT); synteza mowy jest wliczona i nie obowiązuje dopłata za agenta. Połączenie, którego nikt nie odbierze, kosztuje 0. Odpowiedź przychodzi zaraz po wykonaniu połączenia; wynik można odczytać w GET /v1/calls/{id} (kind: "voice_message", blok voiceMessage) lub z webhooka.
Żądanie
curl -X POST "https://volai.cz/v1/voice-messages" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_VOICE_MESSAGES_ID" \
-d '{"to":"+420777123456","text":"Alarm: serwer api-1 nie odpowiada od pięciu minut. Prosimy o sprawdzenie jego stanu.","repeat":2}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"id": "c_k4m8p2q7r1s5",
"status": "ringing",
"from": "+420266266641"
}Stany błędów
- 400
validationBrakujetolubtext,textjest pusty lub ma ponad 500 znaków,repeatnie jest liczbą całkowitą od 1 do 3, treść zawiera nieznane pole (na przykładringingTimeoutSecs, którego wiadomość głosowa nie ma) albo treść nie jest prawidłowym JSON. - 400
invalid_numbertonie jest prawidłowym numerem telefonu. - 400
unsupported_countrytonie jest numerem czeskim (+420) ani słowackim (+421). - 400
blocked_destinationtojest linią o podwyższonej opłacie, więc połączenia na nią są blokowane, aby nie powstał niezamierzenie wysoki rachunek. - 400
on_dncTen numer znajduje się na Państwa liście zakazu dzwonienia (/v1/dnc) lub podlega automatycznej 30-dniowej blokadzie po powtarzających się nieodebranych połączeniach (destination_auto_blocked). - 400
destination_auto_blockedTen numer znajduje się na Państwa liście zakazu dzwonienia (/v1/dnc) lub podlega automatycznej 30-dniowej blokadzie po powtarzających się nieodebranych połączeniach (destination_auto_blocked). - 400
cannot_call_own_numberOchrona przed pętlą -tojest jednym z Państwa własnych numerów volai. - 400
cannot_call_volai_numberOchrona przed pętlą -tojest numerem volai innego konta lub linią demonstracyjną volai. - 402
insufficient_creditSaldo nie pokrywa minimum potrzebnego do rozpoczęcia połączenia albo konto nie dokonało jeszcze pierwszego doładowania (saldo powitalne nie pokrywa wiadomości głosowych). Należy doładować saldo i spróbować ponownie. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 403
email_unverifiedWiadomość głosową mogą wysyłać tylko konta ze zweryfikowanym adresem e-mail. Należy zweryfikować adres e-mail i spróbować ponownie. Nic nie zostało naliczone. - 409
destination_busyInne połączenie lub wiadomość głosowa na ten sam numer wciąż trwa. Numer pozostaje zablokowany, dopóki trwa połączenie, więc prosimy spróbować za chwilę. - 429
rate_limitedWyczerpano limit połączeń lub dzienny limit 200 wiadomości głosowych na konto, nic nie zostało naliczone. Prosimy spróbować później albo skontaktować się z pomocą techniczną, a podniesiemy limit. - 502
engine_unavailableSilnik głosowy volai nie przyjął teraz połączenia. Nic nie zostało naliczone, prosimy spróbować za chwilę. Nie należy ślepo ponawiać bezIdempotency-Key. - 503
capacity_busyWszystkie linie wychodzące są teraz zajęte. Nic nie zostało naliczone, prosimy spróbować za minutę. - 503
ledger_unavailableNie udało się teraz odczytać salda, prosimy spróbować za chwilę. Nic nie zostało naliczone. - 503
voice_messages_disabledWiadomości głosowe nie są jeszcze włączone. Nic nie zostało naliczone, prosimy spróbować później. - 503
voice_message_unavailableSilnik głosowy nie może teraz przyjąć wiadomości głosowych. Połączenie nie zostało wykonane i nic nie zostało naliczone, prosimy spróbować za chwilę.
Wiadomość głosowa: zadzwonić, odczytać tekst, rozłączyć się
Do alertów i powiadomień, na które nikt nie odpowiada: incydent dla inżyniera dyżurnego, przypomnienie o terminie, awizo dostawy. POST /v1/voice-messages dzwoni, odczytuje text słowo w słowo (po czesku, bez uzupełniania zmiennych {{...}}), robi krótką przerwę między odczytami i się rozłącza. Nie potrzeba ani agenta, ani własnego numeru. Do rozmowy, w której odbiorca ma odpowiadać, należy użyć POST /v1/calls z agentem.
Cena to wyłącznie minuta wychodząca, naliczana co do sekundy (0.92 CZK/min bez VAT); synteza mowy jest wliczona i nie obowiązuje dopłata za agenta, więc krótka wiadomość kosztuje kilkadziesiąt halerzy. Połączenie, którego nikt nie odbierze, kosztuje 0. Gdy odbierze poczta głosowa odbiorcy, wiadomość jest tam nagrywana, a połączenie jest naliczane jak każde połączone.
Odbiorca widzi wspólny numer volai, a nie Państwa. Każdy, kto na niego oddzwoni, trafi na linię demonstracyjną volai. Konto potrzebuje zweryfikowanego adresu e-mail i co najmniej jednego doładowania salda, a obowiązuje dzienny limit 200 wiadomości głosowych na konto.
Wynik należy odczytać z GET /v1/calls/{id}: kind to voice_message, a blok voiceMessage zawiera repeatPlayed (ile razy wiadomość została wysłuchana) i completed (true, gdy została odczytana do końca i volai się rozłączyło, false, gdy odbiorca rozłączył się wcześniej). Te same pola przychodzą w webhooku call.completed (call.no_answer, gdy nikt nie odebrał). Podsumowanie połączenia e-mailem nie jest wysyłane dla wiadomości głosowej. W statystykach dostarczona wiadomość ma kategorię other, a nieodebrana pozostaje no_answer.
{
"call": {
"id": "c_k4m8p2q7r1s5",
"direction": "out",
"from": "+420266266641",
"to": "+420777123456",
"status": "completed",
"kind": "voice_message",
"durationSecs": 17.4,
"voiceMessage": {
"text": "...",
"repeat": 2,
"repeatPlayed": 2,
"completed": true
}
}
}Nie należy używać tego do niezamówionego marketingu. Automatyczne połączenia bez zgody odbiorcy są nielegalne. Wiadomość głosowa jest przeznaczona do powiadomień operacyjnych dla osób, które chcą je otrzymywać.
POST/v1/rings
Dzwoni na to z jednego z Państwa własnych numerów głosowych (from) i nigdy nie łączy rozmowy: dzwonienie ustaje po około ringSecs sekundach (liczba całkowita od 3 do 30, domyślnie 10) albo wcześniej, gdy cel je odrzuci; operator kończy dzwonienie z opóźnieniem do 3 sekund, więc czas dzwonienia w zapisie (ring.rangSecs) jest nieco dłuższy. Przeznaczone dla bram GSM, zamków i skrzynek, które otwierają się po rozpoznaniu numeru dzwoniącego. Cel widzi jako dzwoniącego dokładnie from (bez from używany jest Państwa pierwszy aktywny numer), więc brama musi mieć go na liście dozwolonych numerów. Odpowiedź przychodzi OD RAZU po przyjęciu zlecenia przez operatora, w ciągu około sekundy i przed zakończeniem dzwonienia: status to initiated i nie ma jeszcze wyniku. Wynik przychodzi webhookiem (call.no_answer dla rang i ended_early, call.completed dla answered) i znajduje się w GET /v1/calls/{id} jako ring.outcome (z ?waitSecs= serwer czeka, aż próba się zakończy). Wartością ring.outcome jest rang, ended_early, answered lub unknown (ostatnia, dopóki wynik jest jeszcze rozliczany z zapisów operatora). Jedna próba kosztuje 0.25 CZK bez VAT, naliczane po przyjęciu zlecenia przez operatora; jeśli cel odbierze, volai natychmiast się rozłącza, a połączone sekundy są doliczane jako połączenie wychodzące. Dzwonki nie wliczają się do automatycznej blokady celu, ale obowiązuje Państwa lista zakazu dzwonienia. Limity na konto, niezależne od limitów połączeń: 30 dzwonków na minutę, 500 na dobę i 120 na godzinę na jeden numer. Obsługuje nagłówek Idempotency-Key.
Żądanie
curl -X POST "https://volai.cz/v1/rings" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_RINGS_ID" \
-d '{"to":"+420777123456","from":"+420601234567","ringSecs":10}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"id": "c_x7f2k9a1b3c4",
"status": "initiated",
"kind": "ring",
"from": "+420601234567",
"to": "+420777123456",
"requestedSecs": 10,
"priceHal": 25
}Stany błędów
- 400
validationBrakujeto,ringSecsnie jest liczbą całkowitą od 3 do 30 (wartość spoza zakresu jest odrzucana, a nie przycinana) albo treść nie jest prawidłowym JSON. - 400
invalid_numbertonie jest prawidłowym numerem telefonu. - 400
unsupported_countrytonie jest numerem czeskim (+420) ani słowackim (+421). - 400
blocked_destinationtojest linią o podwyższonej opłacie, więc dzwonki na nią są blokowane, aby nie powstał niezamierzenie wysoki rachunek. - 400
on_dncTen numer znajduje się na Państwa liście zakazu dzwonienia (/v1/dnc). Automatyczna 30-dniowa blokada nie dotyczy dzwonków, lista ręczna tak. - 400
cannot_call_own_numberOchrona przed pętlą -tojest jednym z Państwa własnych numerów volai. - 400
cannot_call_volai_numberOchrona przed pętlą -tojest numerem volai innego konta. - 400
unsupported_for_number_typefromto Państwa własny produkt "numer do połączeń i SMS", a połączenia z niego wykonuje wyłącznie agent, więc nie może on dzwonić w trybie dzwonka. Należy pominąćfrom(używany jest Państwa pierwszy aktywny numer głosowy) albo użyć numeru głosowego. Nic nie zostało naliczone. - 402
insufficient_creditSaldo nie pokrywa minimum potrzebnego do rozpoczęcia połączenia. Nic nie zostało naliczone. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 404
from_number_not_ownedfromnie jest aktywnym numerem głosowym Państwa konta. Lista znajduje się wGET /v1/numbers. - 404
ring_needs_numberKonto nie ma aktywnego numeru głosowego, z którego można dzwonić. Produkt "numer do połączeń i SMS" się nie liczy: połączenia z niego wykonuje wyłącznie agent. Numer głosowy można kupić przezPOST /v1/numbers. - 409
destination_busyInny dzwonek lub połączenie na ten sam numer wciąż trwa. Numer pozostaje zablokowany przez czas dzwonienia plus około 30 sekund, więc po tym czasie można spróbować ponownie. - 429
rate_limitedOsiągnięto limit dzwonków, nic nie zostało naliczone: najwyżej 30 na minutę i 500 na dobę na konto oraz 120 na godzinę na jeden numer. Niezależnie od limitów połączeń. - 502
callback_failedOperator od razu odrzucił zlecenie dzwonka, więc nic nie zostało wybrane. Państwa saldo nie zostało obciążone. - 503
ledger_unavailableNie udało się teraz odczytać salda, prosimy spróbować za chwilę. Nic nie zostało naliczone.
Dzwonek do bramy: otwarcie jednym żądaniem
Brama GSM, zamek lub skrzynka mają własny numer telefonu: gdy dzwoni na nią numer z jej listy dozwolonych, odrzuca połączenie i się otwiera. POST /v1/rings robi to jednym żądaniem - dzwoni do bramy z Państwa numeru głosowego from i kończy dzwonienie po około ringSecs sekundach albo wcześniej, gdy brama je odrzuci. from należy dodać do listy dozwolonych numerów bramy: to ten numer ona zobaczy.
Odpowiedź (status: "initiated") przychodzi zaraz po zleceniu. Jak próba się zakończyła, widać w ring.outcome, w zdarzeniu webhooka i w GET /v1/calls/{id}: rang (dzwoniło, aż upłynął żądany czas), ended_early (połączenie skończyło się wcześniej, na przykład dlatego, że brama je odrzuciła), answered (cel odebrał, volai natychmiast się rozłącza, a połączone sekundy są doliczane jako połączenie wychodzące) lub unknown (wyniku nie dało się ustalić od razu; jest uzupełniany z zapisów operatora w ciągu kilku minut).
{
"call": {
"id": "c_x7f2k9a1b3c4",
"direction": "out",
"from": "+420601234567",
"to": "+420777123456",
"status": "no_answer",
"startedAt": 1756111640000,
"durationSecs": 0,
"priceHal": 25,
"source": "api",
"kind": "ring",
"ring": { "requestedSecs": 10, "outcome": "ended_early", "rangSecs": 2 },
"note": null,
"flag": null,
"handledAt": null,
"rating": null,
"goal": null,
"hasConversation": false,
"endReason": "no_answer"
}
}Nieodebrany dzwonek (rang, ended_early) dociera do Państwa integracji jako zdarzenie call.no_answer, jak każde inne nieodebrane połączenie wychodzące; answered przychodzi jako call.completed. Jeden wyjątek: gdy operator potwierdza odebranie dopiero w swoim zapisie połączenia (CDR), po tym jak volai zamknęło dzwonek jako nieodebrany, przychodzi tylko call.no_answer, a zapis w GET /v1/calls/{id} zmienia się później na completed z ring.outcome: "answered" i naliczonymi sekundami odebrania; drugie zdarzenie nie jest wysyłane. Treść zdarzenia zawiera kind: "ring" i blok ring (requestedSecs, outcome, rangSecs), więc kind mówi, że brama odrzuciła połączenie, a nie że nikt nie odebrał. Każde zdarzenie połączenia zawiera kind; blok ring jest tylko przy dzwonkach. Wynik unknown pozostaje initiated bez zdarzenia, dopóki nie uzupełnią go zapisy operatora; jeśli operator nigdy nie zgłosi próby, po 24 godzinach jest zamykana jako failed.
Dzwonki nie wliczają się do automatycznej blokady celu (obowiązuje tylko Państwa ręczna lista zakazu dzwonienia) i mają własne limity, niezależne od limitów połączeń: 30 na minutę i 500 na dobę na konto oraz 120 na godzinę na jeden numer. Numer pozostaje zablokowany, gdy dzwoni, plus około 30 sekund: kolejny dzwonek lub połączenie na niego zwróci w tym czasie destination_busy. Przy ponawianiu po błędzie sieci należy wysłać nagłówek Idempotency-Key, aby druga próba nie została ani zlecona, ani naliczona.
GET/v1/calls/active
Odczytuje każdy skonfigurowany natywny węzeł silnika dla jednego agentId należącego do konta. To migawka na żywo, w przeciwieństwie do historii połączeń. complete oznacza, że wszystkie odpowiedzi silnika były prawidłowe, a metadane własności kompletne. outboundCount obejmuje nierozstrzygnięte połączenia; inboundCount liczy oddzwonienia osobno. Każdy wiersz połączenia wychodzącego ma nieprzezroczyste nativeRef, startedAt (Unix ms) i resolved. Tylko zweryfikowane połączenia zadań zawierają callId, taskId, itemId i phoneSha256. Brakujące mapowanie pozostaje widoczne z resolved: false. checkedAt to czas zakończenia migawki w formacie ISO, a enginesChecked liczba węzłów. Brak połączeń wychodzących można udowodnić tylko przy complete: true, outboundCount: 0 i unresolvedOutboundCount: 0; błędy lub niekompletna migawka nigdy nie dowodzą zera. Ten endpoint obejmuje wyłącznie natywne połączenia telefoniczne, nie sesje internetowe ani innych dostawców. Korzysta z istniejącego wspólnego limitu zapytań klucza API.
Żądanie
curl 'https://volai.cz/v1/calls/active?agentId=ag_example' -H 'Authorization: Bearer vk_YOUR_KEY'Odpowiedź
{
"data": {
"agentId": "ag_example",
"complete": true,
"checkedAt": "2026-10-09T17:59:00.000Z",
"enginesChecked": 2,
"outboundCount": 0,
"inboundCount": 0,
"unresolvedOutboundCount": 0,
"outbound": []
}
}Stany błędów
- 400
validationOczekiwane powiązania lub zapytanie o agenta brakują, są nieprawidłowe albo już nie pasują do połączenia. - 404
agent_not_foundAgent nie istnieje albo należy do innego konta. - 502
engine_unavailableNie udało się zweryfikować stanu natywnego; nie należy zakładać, że połączenia się zakończyły. - 503
engine_not_configuredPołączenie z natywnym silnikiem nie jest skonfigurowane.
POST/v1/calls/{id}/hangup
Kończy jedno należące do konta natywne wychodzące połączenie zadania. Należy wysłać wszystkie cztery oczekiwane powiązania: expectedAgentId, expectedTaskId, expectedItemId i expectedPhoneSha256 (SHA-256 małymi literami dokładnego celu E.164). Serwer weryfikuje historię zadania, kontekst wysyłki, natywny pokój, najemcę, agenta, kierunek i czas rozpoczęcia, zanim wyśle jedno natywne rozłączenie. Trwałe przejęcie na każde połączenie zapobiega zdublowanym żądaniom do dostawcy, w tym żądaniom współbieżnym i przekroczeniom czasu; Idempotency-Key jest zbędny. Odpowiedź zawiera callId, outcome i endConfirmed: hung_up oznacza, że natywny silnik potwierdził dokładnie to połączenie, already_ended wymaga zakończonego zapisanego połączenia oraz jego braku na wszystkich węzłach, a unknown nigdy nie oznacza sukcesu. Po nieznanym wyniku należy sprawdzić aktywność na żywo i zapisany stan połączenia; powtórzenie tego endpointu tylko odczytuje stan i nigdy nie wysyła kolejnego rozłączenia. Po zakończeniu należy ponownie sprawdzić aktywne połączenia. Połączenia przychodzące, inni najemcy, połączenia samodzielne i inni dostawcy są odrzucani. Zakres kampanii, okna dzwonienia i terminy należą do kontrolera dzwonienia; ten endpoint nie zmienia rozliczeń ani stanu zadania.
Żądanie
curl -X POST https://volai.cz/v1/calls/c_example/hangup -H 'Authorization: Bearer vk_YOUR_KEY' -H 'Content-Type: application/json' -d '{"expectedAgentId":"ag_example","expectedTaskId":"task_example","expectedItemId":"item_example","expectedPhoneSha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}'Odpowiedź
{
"data": {
"callId": "c_example",
"outcome": "hung_up",
"endConfirmed": true
}
}Stany błędów
- 400
validationOczekiwane powiązania lub zapytanie o agenta brakują, są nieprawidłowe albo już nie pasują do połączenia. - 404
call_not_foundPołączenie nie istnieje albo nie należy do Państwa konta. - 502
engine_unavailableNie udało się zweryfikować stanu natywnego; nie należy zakładać, że połączenia się zakończyły. - 503
engine_not_configuredPołączenie z natywnym silnikiem nie jest skonfigurowane.
GET/v1/calls/{id}
Szczegóły połączenia. Opcjonalnie waitSecs (zob. ramka poniżej) sprawia, że serwer czeka na wynik, zamiast odpytywania go przez Państwa. Dla połączeń z agentem zawiera także transcript (transkrypcję tura po turze, każda tura z opcjonalnym polem timeInCallSecs - sekundy od rozpoczęcia połączenia, brak, jeśli dostawca nie opatrzył tej tury znacznikiem czasu; tura agenta przerwana przez dzwoniącego ma dodatkowo interrupted: true, a jej message jest nadal pełnym zaplanowanym tekstem, tylko własny silnik volai) i summary (krótkie podsumowanie) - zob. Webhooki dla tego samego kształtu dostarczanego automatycznie po zakończeniu połączenia. Zarówno lista, jak i szczegóły zawierają także answeredBy (tylko połączenia WYCHODZĄCE: human, voicemail, ivr (odebrało automatyczne menu głosowe; silnik od razu się rozłącza) lub unknown, gdy silnik nie podjął decyzji albo brakuje transkrypcji; połączenia przychodzące pomijają to pole, ponieważ krótka odpowiedź dzwoniącego nie jest pocztą głosową). Silnik rozstrzyga wartościami human, voicemail i ivr; unknown oznacza, że silnik nie podjął decyzji przed zamknięciem okna wykrywania, i od 1.33.0 pozostaje unknown (wcześniej zastępował je szacunek oparty na transkrypcji, który krótką odpowiedź, taką jak "Tak." po wstępie, traktował jako pocztę głosową). Szacunek oparty na transkrypcji (human lub voicemail, unknown dla pustej transkrypcji; nigdy nie wykrywa menu głosowego) dotyczy wyłącznie połączeń bez wartości z silnika. Połączenie wychodzące obsłużone przez własny silnik głosowy volai zawiera także voicemailReason (phrase, long_monologue, beep, human_reply, ivr_phrase lub digits - human_reply oznacza, że odebrała osoba z odpowiedzią tak krótką, że detektor ją odnotował (a nie że była to poczta głosowa), ivr_phrase to fraza menu głosowego, digits to powitanie operatora odczytujące wybrany numer cyfra po cyfrze), voicemailMessageLeft (silnik odczytał skonfigurowaną wiadomość do poczty głosowej) i voicemailDetectedAtSecs (sekundy od odebrania do decyzji detektora silnika - nawet w połączeniu odebranym przez człowieka) - wszystkie trzy pola pochodzą wyłącznie z silnika; klasyfikacja ElevenLabs oparta na transkrypcji (answeredBy bez jawnej wartości) nie zna powodu ani czasu. Połączenie przychodzące lub wychodzące na silniku zawiera także optOutRequested: true, gdy uczestnik po drugiej stronie wyraźnie poprosił, aby do niego więcej nie dzwonić (na przykład "nie dzwońcie do mnie więcej", nawet w ostatnich sekundach po pożegnaniu) - numer trafia wtedy automatycznie na listę zakazu dzwonienia konta (GET /v1/dnc), a polityka ponowień zadania nie planuje kolejnych prób; połączenie bez takiej wypowiedzi nie ma tego klucza. Zawiera też endReason (completed, no_answer, busy, rejected, capacity, blocked, loop_guard, credit_blocked, suspended, engine_error, caller_loop, failed) oraz kind (agent, bridge, relay, inbound, transfer, sip, ring, voice_message) - zob. tabela poniżej. Wiadomość głosowa (kind: "voice_message") zawiera także blok voiceMessage (text, repeat oraz po zakończeniu połączenia repeatPlayed i completed) - zob. POST /v1/voice-messages. Dzwonek (kind: "ring") zawiera także blok ring (requestedSecs, outcome, rangSecs) - zob. POST /v1/rings. Fala narzędzi i nagrań dodała jeszcze dwa pola: data (to, co agent zebrał z połączenia według swoich dataFields; wartość null oznacza, że nie zostało wspomniane) i hasRecording (połączenie ma nagranie dostępne do pobrania, zob. endpoint poniżej). Starsze połączenia nie mają tych pól i nie ma ich w odpowiedzi. durationSecs jest w sekundach; dla połączeń obsłużonych przez własny silnik volai może być liczbą dziesiętną (zaokrągloną do dwóch miejsc po przecinku). Odpowiedź zawiera własne adnotacje właściciela konta, zawsze obecne, null, gdy nieustawione: note (ciąg), flag ({reason, flaggedAt} | null), handledAt (Unix ms lub null) i goal ({result: "success"|"failure"|"unknown", rationale, reason} | null, wypełniane dopiero po ocenie celu agenta; reason to "no_caller_speech"|"evaluation_error"|null i mówi, dlaczego wynik wyszedł "unknown" - null dla naprawdę niejasnego wyniku) - zob. PATCH /v1/calls/{id} poniżej. Odpowiedź zawsze zawiera także hasConversation (boolean | null) - czy połączenie rzeczywiście zawierało rozmowę z dzwoniącym; false dla połączenia, które się nie zestawiło, poczty głosowej lub menu głosowego, null dla starszego rekordu bez wystarczającego sygnału do rozstrzygnięcia. Połączenie agenta z fazami (plan węzłów workflow) obsłużone przez własny silnik głosowy volai (provider: "engine") zawiera także workflowPath - zob. sekcja Fazy połączenia (workflow) w sekcji o agentach powyżej; połączenie z agentem hostowanym na ElevenLabs nigdy nie zawiera tego pola, nawet jeśli przeszło przez fazy, ponieważ zapisuje je wyłącznie post-call silnika. Połączenie obsłużone przez build silnika z 15 października 2026 lub nowszy zawiera także w szczegółach (nie na liście) blok engine z protectedUtterances i protectedUtterancesCount: wypowiedzi osoby, która odebrała, a które silnik odrzucił w oknie powitania - to, co powiedziała, gdy agent wypowiadał swoją pierwszą kwestię ("Halo?", swoje nazwisko albo cokolwiek w obrębie firstMessageProtectSecs), a czego silnik celowo nie przyjął, więc NIE ma tego w transkrypcji, a agent na to nie zareagował; transkrypcja zawierająca tylko agenta z niepustą listą oznacza zatem, że osoba rzeczywiście odebrała i się odezwała. Każda pozycja ma timeInCallSecs (początek wypowiedzi w nagraniu, brak, gdy silnik go nie zna), atSecs (kiedy silnik skończył ją słyszeć, sekundy od początku połączenia, zawsze), message (do 200 znaków), words i reason (first_message_protect, greeting_courtesy lub greeting_identification; lista otwarta). Najwyżej 10 pozycji, protectedUtterancesCount zlicza je wszystkie; połączenie, w którym nic nie odrzucono, zawiera [] i 0; starsze połączenia, agenci hostowani na ElevenLabs i połączenia przerwane restartem silnika (endReason: engine_error, webhook reason: unfinished; silnik nie zmierzył ich okna powitania) nie mają klucza engine w ogóle. Ten sam blok znajduje się w webhooku call.*.
Żądanie
curl https://volai.cz/v1/calls/c_8f2ac1d4 \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"call": {
"id": "c_8f2ac1d4",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 236,
"agentId": "ag_kx91fa2b",
"note": null,
"flag": null,
"handledAt": null,
"rating": null,
"goal": null,
"hasConversation": true,
"source": "inbound",
"kind": "inbound",
"endReason": "completed",
"hasRecording": true,
"data": {
"name": "Anna Kowalska",
"coffee_count": 2,
"urgency": "normal"
},
"transcript": [
{ "role": "agent", "message": "Dzień dobry, kawiarnia Nula, w czym mogę pomóc?", "timeInCallSecs": 0 },
{ "role": "caller", "message": "Chciałbym zamówić dwie latte na wynos.", "timeInCallSecs": 4 },
{ "role": "agent", "message": "Oczywiście, dwie latte na odbiór, będą gotowe za piętnaście minut.", "timeInCallSecs": 9 }
],
"summary": "Dzwoniący zamówił dwie latte na wynos, odbiór za 15 minut."
}
}Stany błędów
- 404
call_not_foundPołączenie nie istnieje albo nie należy do Państwa konta. - 400
validationwaitSecs nie jest liczbą całkowitą od 0 do 45.
Oczekiwanie na wynik zamiast odpytywania (waitSecs)
Zaraz po POST /v1/calls należy wywołać GET /v1/calls/{id} z waitSecs (0 do 45, domyślnie 0) - serwer odpowiada, gdy połączenie osiągnie stan końcowy (completed/failed/missed/no_answer), od razu zwracając transcript i summary, albo gdy upłynie waitSecs, w zależności od tego, co nastąpi pierwsze.
curl -X GET "https://volai.cz/v1/calls/c_9d4e2b7f?waitSecs=30" \
-H "Authorization: Bearer vk_YOUR_KEY"{
"call": {
"id": "c_9d4e2b7f",
"direction": "out",
"from": "+420601234567",
"to": "+420777123456",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 269,
"agentId": "ag_kx91fa2b",
"note": null,
"flag": null,
"handledAt": null,
"rating": null,
"goal": null,
"hasConversation": true,
"source": "api",
"kind": "agent",
"endReason": "completed",
"hasRecording": true,
"answeredBy": "human",
"engine": {
"protectedUtterances": [
{ "timeInCallSecs": 1.2, "atSecs": 2.4, "message": "Haló?", "words": 1, "reason": "greeting_courtesy" }
],
"protectedUtterancesCount": 1
}
}
}Jeśli czas upłynie przed końcem połączenia, stillRunning ma wartość true, a pola zawierają bieżący (nieostateczny) stan - nigdy nie kończy się to błędem. Można ponownie wywołać GET (lub MCP get_call), jeśli trzeba, z wyższym waitSecs.
{
"call": {
"id": "c_9d4e2b7f",
"direction": "out",
"from": "+420601234567",
"to": "+420777123456",
"status": "ringing",
"startedAt": 1756111400000,
"agentId": "ag_kx91fa2b",
"note": null,
"flag": null,
"handledAt": null,
"rating": null,
"goal": null,
"hasConversation": null,
"source": "api"
},
"stillRunning": true
}GET/v1/calls/{id}/recording
Nagranie połączenia. W przeciwieństwie do reszty API ta odpowiedź nie jest JSON - format treści wynika z content-type rzeczywistej odpowiedzi (rozszerzenie w content-disposition mu odpowiada): audio/mpeg (MP3, 128 kbps, 16 kHz mono) dla agenta na ElevenLabs, audio/ogg obecnie dla agenta na własnym silniku volai. Format nigdy nie jest konwertowany - należy kierować się tym nagłówkiem, a nie zakładanym typem. Sami nie przechowujemy kopii nagrania: strumień płynie z platformy głosowej wprost przez nas, a my tylko weryfikujemy, że połączenie należy do Państwa konta.
?stahnout=1 przełącza content-disposition na attachment (przeglądarka zapisuje plik zamiast go odtwarzać) - rozszerzenie pliku w nim (.mp3 / .ogg, w pozostałych przypadkach .bin) zawsze odpowiada rzeczywistemu content-type powyżej. Ta ścieżka nie oferuje zakresów (Range) - jest przeznaczona dla integracji maszynowych pobierających cały plik. Nagrywane są tylko połączenia obsłużone przez agenta z włączonym recordCalls, a przechowujemy je 90 dni po połączeniu.
Żądanie
curl https://volai.cz/v1/calls/c_8f2ac1d4/recording \
-H "Authorization: Bearer vk_YOUR_KEY" \
-OJStany błędów
- 404
call_not_foundPołączenie nie istnieje albo nie należy do Państwa konta. - 404
no_recordingPołączenie nie ma nagrania - nie obsługiwał go agent albo nagrywanie było wyłączone. - 410
recording_expiredNagranie istniało, ale minęło 90 dni i zostało usunięte. Transkrypcja pozostaje.
PATCH/v1/calls/{id}
Własna adnotacja właściciela konta do połączenia - notatka, oznaczenie agenta z powodem i ręczne "załatwione". Musi być obecne co najmniej jedno z trzech pól. note (string lub null, maks. 2000 znaków) - null czyści notatkę. flag ({reason} maks. 500 znaków lub null) - ustawienie oznaczenia CZYŚCI handledAt (oznaczone połączenie nie jest już "załatwione"); flag: null czyści oznaczenie bez zmiany handled. handled (boolean) - true ustawia handledAt na teraz, false resetuje go do null. flag (niepuste) i handled: true nie mogą być ustawione w TYM SAMYM żądaniu (400 validation) - oznaczenie i potwierdzone załatwienie wzajemnie się wykluczają; należy wysłać je dwoma wywołaniami.
Żądanie
curl -X PATCH https://volai.cz/v1/calls/c_8f2ac1d4 \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"note": "Volal kvůli reklamaci, přeposlat na podporu.", "flag": {"reason": "Agent nerozuměl dotazu na fakturu."}}'Odpowiedź
{
"call": {
"id": "c_8f2ac1d4",
"direction": "in",
"from": "+420777123456",
"to": "+420601234567",
"status": "completed",
"startedAt": 1756111400000,
"durationSecs": 47,
"priceHal": 236,
"agentId": "ag_kx91fa2b",
"source": "inbound",
"kind": "inbound",
"endReason": "completed",
"hasRecording": true,
"data": { "name": "Anna Kowalska" },
"note": "Volal kvůli reklamaci, přeposlat na podporu.",
"flag": { "reason": "Agent nerozuměl dotazu na fakturu.", "flaggedAt": 1756111460000 },
"handledAt": null,
"rating": null,
"goal": { "result": "failure", "rationale": "Dzwoniący zamówił dwie latte na wynos, odbiór za 15 minut.", "reason": null },
"hasConversation": true
}
}Stany błędów
- 400
validationW treści nie ma żadnego znote/flag/handled,notema ponad 2000 znaków,flag.reasonbrakuje lub ma ponad 500 znaków, albo treść ustawiaflag(niepuste) razem zhandled: true. - 404
call_not_foundPołączenie nie istnieje albo nie należy do Państwa konta.
Status połączenia: pole status
status to jedyne pole, które zmienia się w czasie w połączeniu. Może się jeszcze zmieniać w ciągu pierwszych pięciu minut od rozpoczęcia połączenia; potem jest ostateczne - cztery z tych stanów są końcowe.
| status | Znaczenie |
|---|---|
initiated | Połączenie zostało zlecone, ale jeszcze nie zaczęło dzwonić. |
ringing | Dzwoni u wywoływanej strony. |
in_progress | Połączenie trwa. |
completed | Końcowy. Połączenie zestawione, które zakończyło się normalnie - w zdecydowanej większości należy oczekiwać durationSecs > 0 (wyjątek: połączenie z endReason: credit_blocked, suspended lub spend_cap poniżej ma zarówno czas trwania, jak i cenę równe 0). Połączenie z endReason: caller_loop (po drugiej stronie automatyczny dzwoniący, do pięciu minut) również ma cenę 0, ale ma czas trwania. |
no_answer | Końcowy. Połączenie wychodzące dzwoniło, a wywoływana strona nie odebrała. Dla dzwonka (kind: "ring") jest to normalny wynik: brama odrzuciła połączenie albo dzwonienie trwało do końca żądanego czasu (ring.outcome je rozróżnia). |
missed | Końcowy. Połączenie przychodzące pozostało bez odpowiedzi (dotyczy to także połączenia odrzuconego, bo wszystkie linie były zajęte - mówi o tym endReason). |
failed | Końcowy. Zwykle połączenie w ogóle się nie zestawiło - platforma je odrzuciła albo zawiodła sieć. Wyjątek (karta K6, audyt R02): endReason: engine_error oznacza, że połączenie SIĘ zestawiło, ale zakończyło się z powodu błędu technicznego po naszej stronie albo zostało przerwane restartem silnika głosowego - zawiera transcript i zwykle nagranie (gdy silnik zdołał je zapisać; po restarcie silnika tylko częściową lub pustą transkrypcję i brak nagrania) i nigdy nie jest naliczane. endReason zawsze podaje konkretny powód. |
Typ połączenia: pole kind
kind mówi, w jaki sposób połączenie powstało - a więc jak jest naliczane. To jedyne pole, które pozwala na fakturze odróżnić połączenie własnego agenta (BYO) od połączenia agenta wbudowanego, ponieważ nogi relay nie niosą dopłaty za agenta.
| kind | Co to jest | Rozliczenie |
|---|---|---|
agent | Połączenie wychodzące od wbudowanego agenta głosowego. | 0.92 CZK/min + 2.50 CZK/min |
bridge | Bezpośrednie połączenie dwóch numerów, bez agenta. | 0.92 CZK/min (~EUR 0.04) za każdą z dwóch nóg |
relay | Połączenie wychodzące od Państwa własnego agenta przez POST /v1/relay. | 0.92 CZK/min (~EUR 0.04), bez dopłaty za agenta |
inbound | Połączenie przychodzące na Państwa numer. | 0.50 CZK/min (+ agent, jeśli je obsługiwał) |
transfer | Druga noga powstała przez przełączenie połączenia przychodzącego na człowieka. Agent opuszcza połączenie po przełączeniu, więc ta noga jest naliczana jak zwykłe połączenie wychodzące. | 0.92 CZK/min, bez dopłaty za agenta |
sip | Połączenie wybrane bezpośrednio z klienta SIP (softfon, Państwa własna centrala) zarejestrowanego na Państwa linii, poza naszym API - rozliczane wstecznie na podstawie zapisów połączeń z sieci. | 0.92 CZK/min, bez dopłaty za agenta |
voice_message | Wiadomość głosowa: dzwoni, odczytuje tekst i się rozłącza (POST /v1/voice-messages). Wynik znajduje się w bloku voiceMessage. | tylko minuta wychodząca (0.92 CZK/min), co do sekundy, bez dopłaty za agenta; połączenie nieodebrane to 0 |
ring | Dzwonek do bramy: krótkie dzwonienie, które nigdy nie jest łączone (POST /v1/rings). Wynik znajduje się w bloku ring. | 0.25 CZK za próbę, plus sekundy w razie odebrania |
Dlaczego połączenie się zakończyło: pole endReason
Do każdego zamkniętego połączenia dodajemy endReason. Uwaga: połączenie zablokowane z powodu niezapłaconego długu lub ręcznego zawieszenia ma status completed, ale endReason to odpowiednio credit_blocked lub suspended, a cena wynosi 0 (to samo dotyczy połączenia odrzuconego przez miesięczny limit wydatków: spend_cap) - samym status nie odróżni się go od połączenia, które rzeczywiście doszło do skutku. To samo dotyczy połączenia z automatycznym dzwoniącym po drugiej stronie, powtarzającym tę samą wiadomość (caller_loop): ma status completed, ale jeśli trwało do pięciu minut, jego cena wynosi 0. Trzy warianty "nieodebrane" są celowo rozróżnione, a nie jednym wspólnym stanem.
| endReason | Znaczenie |
|---|---|
completed | Połączenie doszło do skutku i zakończyło się normalnie. |
no_answer | Dzwoniło, a odbiorca nie odebrał. |
busy | Sygnał zajętości - to NIE to samo co nieodebranie. |
rejected | Platforma od razu odrzuciła połączenie. |
capacity | Linie wychodzące (pula relay) były pełne. Przy połączeniu przychodzącym: linie były pełne i silnik głosowy odrzucił połączenie, więc dzwoniący się nie dodzwonił; połączenie ma status missed, czas trwania 0 i cenę 0, a dla niego wysyłany jest webhook call.missed. |
blocked | Cel znajduje się na liście zakazu dzwonienia lub w automatycznej 30-dniowej blokadzie (zob. Lista zakazu dzwonienia poniżej). |
loop_guard | Ochrona przed dzwonieniem na własny numer volai lub numer volai innego konta. |
credit_blocked | Połączenie przychodzące do agenta, którego właściciel przekroczył limit zadłużenia - agent rozłącza się zaraz po swojej pierwszej kwestii, cena 0. |
suspended | Połączenie przychodzące do agenta, którego operator volai ręcznie zawiesił (lub którego całe konto jest zawieszone) - agent rozłącza się zaraz po swojej pierwszej kwestii, cena 0. |
spend_cap | Połączenie przychodzące do agenta konta, które osiągnęło miesięczny limit wydatków ustawiony przez właściciela (GET /v1/account/spend-cap) i nie wybrało inboundAboveCap - agent rozłącza się zaraz po swojej pierwszej kwestii, cena 0. |
failed | Połączenie w ogóle się nie zestawiło (platforma lub sieć). |
engine_error | Połączenie się zestawiło, ale zakończyło się z powodu błędu technicznego w naszym silniku głosowym - nie z winy odbiorcy. Nigdy nie jest naliczane. Obejmuje to także połączenie, które restart lub zatrzymanie silnika przerwało w trakcie rozmowy (na przykład podczas wdrożenia): ma tylko częściowy lub pusty transcript i brak nagrania, krótki tekst w summary, który to wyjaśnia, brak bloku engine, a reason jego webhooka call.failed to unfinished. |
caller_loop | Połączenie przychodzące, w którym dzwoniącym był automatyczny system powtarzający w kółko tę samą wiadomość (kolejka, dialer) - volai kończy je po krótkim oczekiwaniu na człowieka. Połączenie ma status completed i dodatni czas trwania, ale jest bezpłatne, jeśli trwa do pięciu minut (cena 0); dłuższe jest naliczane jak każde inne. |
Agenci
GET/v1/agents
Agenci głosowi na koncie.
Żądanie
curl https://volai.cz/v1/agents \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"agents": [
{
"id": "ag_pw7q2vnd",
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, w czym mogę pomóc?",
"voiceId": "7JbZPqJGWUfXXBim0T8U",
"provider": "elevenlabs",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"dataFields": [
{
"key": "name",
"type": "string",
"description": "Imię i nazwisko dzwoniącego, tak jak je podał."
}
],
"recordCalls": true,
"recordingNotice": false,
"goal": null,
"knowledge": null,
"notifyEmail": true,
"silencePromptSecs": 10,
"laughter": "auto",
"firstMessageProtectSecs": null,
"silencePromptText": null,
"sttLanguageHintsStrict": false,
"turnDigitTailFloorMs": null,
"turnLongSpeechFloorMs": null,
"turnLongSpeechSecs": null,
"missedCallSms": { "enabled": false },
"suspended": false
},
{
"id": "ag_kx91fa2b",
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, w czym mogę pomóc?",
"voiceId": "milena",
"provider": "engine",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "Dzwoniący wyraźnie prosi o połączenie z człowiekiem.",
"dataFields": [
{
"key": "name",
"type": "string",
"description": "Imię i nazwisko dzwoniącego, tak jak je podał."
},
{
"key": "urgency",
"type": "string",
"description": "Jak pilna była prośba.",
"enumValues": ["low","normal","high"]
}
],
"recordCalls": true,
"recordingNotice": false,
"goal": null,
"knowledge": null,
"notifyEmail": true,
"silencePromptSecs": 10,
"laughter": "auto",
"firstMessageProtectSecs": null,
"silencePromptText": null,
"sttLanguageHintsStrict": false,
"turnDigitTailFloorMs": null,
"turnLongSpeechFloorMs": null,
"turnLongSpeechSecs": null,
"missedCallSms": { "enabled": false },
"suspended": false
}
]
}POST/v1/agents
Tworzy nowego agenta głosowego. Wymagane są tylko name i systemPrompt - jak napisać dobry prompt, opisano na stronie Agent głosowy . Jeśli wyślą Państwo numberE164, agent zostaje od razu podłączony do tego numeru. POST zwraca tylko id; rzeczywisty provider należy odczytać kolejnym GET /v1/agents/{id}. transferTo wywołuje po zapisie próbę przeniesienia agenta ElevenLabs na silnik, ale wstrzymana próba, niedostępny silnik lub niepowodzenie mogą zostawić go na ElevenLabs, gdzie przekazanie nie działa. voiceId wybiera głos: jawne useForOutboundTasks: true zastępuje rozpoznany głos z katalogu niedostępny na silniku, w tym katty, domyślnym głosem silnika. Poza tym zastępowaniem silnik akceptuje głosy z katalogu z wyjątkiem głosów ElevenLabs (katty, starszy anet); surowy identyfikator ElevenLabs lub inny niezgodny głos zwraca 400. Z false początkowy głos musi być prawidłowy dla ElevenLabs (katty lub surowy identyfikator ElevenLabs; ElevenLabs nie przypisuje już anet nowym agentom, a volai odrzuca go błędem 400); udane późniejsze przełączenie dla transferTo może zmapować go na domyślny głos silnika. Bez voiceId silnik używa Mileny, a ElevenLabs głosu z szablonu (Katty). Pola opcjonalne (toolIds, transferTo, transferCondition, dataFields, recordCalls, recordingNotice, goal, knowledge, notifyEmail, useForOutboundTasks, voicemail) opisano poniżej.
Żądanie
curl -X POST "https://volai.cz/v1/agents" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_AGENTS_ID" \
-d '{"name":"Recepcja","systemPrompt":"Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty i odpowiadasz na pytania o godziny otwarcia. Nigdy nie wymyślaj cen, których nie znasz. Zakończ rozmowę podsumowaniem zamówienia.","firstMessage":"Dzień dobry, kawiarnia Nula, w czym mogę pomóc?","language":"pl","numberE164":"+420601234567","toolIds":["tl_5c2a91f4"],"transferTo":"+420777123456","transferCondition":"Dzwoniący chce rozmawiać z pracownikiem albo ma reklamację.","dataFields":[{"key":"name","type":"string","description":"Imię i nazwisko dzwoniącego, tak jak je podał."},{"key":"coffee_count","type":"number","description":"Ile kaw zamówił dzwoniący."}],"recordCalls":true}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"id": "ag_kx91fa2b"
}Stany błędów
- 400
invalid_nameNazwa agenta musi mieć od 1 do 60 znaków. - 400
invalid_system_promptInstrukcje agenta muszą mieć od 10 do 6000 znaków. - 400
invalid_languagelanguage musi być cs, sk, en, de lub pl. - 400
agent_limitKonto osiągnęło już maksymalną liczbę agentów (10). - 400
tool_limittoolIds zawiera więcej niż 10 narzędzi. - 400
invalid_data_fieldsdataFields zawiera nieprawidłowy klucz, typ, opis albo więcej niż 10 pozycji. - 400
invalid_goalgoal jest zbyt długie - mieści się w nim 300 znaków. - 400
invalid_knowledgeknowledge jest zbyt długie - mieści się w nim 20000 znaków. - 400
transfer_looptransferTo wskazuje numer volai na tym samym koncie - połączenie zapętliłoby się samo w sobie. - 400
blocked_destinationtransferTo jest linią o podwyższonej opłacie ze specjalnym cennikiem. - 400
destination_not_allowedAgent ma używać produktu "numer do połączeń i SMS", atransferTonie jest numerem czeskim (+420): połączenia przełączane z takiego numeru mogą trafiać tylko na numery czeskie. Należy podać czeskitransferToalbo nie podłączać tego numeru do agenta. Dla produktu "numer w innym kraju" numer przełączenia musi znajdować się w kraju numeru (gdy dzwonienie tam jest włączone) lub w Czechach. Nic nie zostało zmienione. - 400
unsupported_for_number_typeAgent ma używać produktu "numer do połączeń i SMS", ale nie zostałby utworzony na własnym silniku głosowym volai: tego rodzaju numer odbiera wyłącznie agent silnika (useForOutboundTasks: truealbo pominięcie pola, gdy silnik jest domyślny dla języka agenta). Agent nie został utworzony i nic nie zostało zmienione. - 400
invalid_numbertransferTo nie jest prawidłowym numerem telefonu. - 400
invalid_voicemail_messagevoicemail.message brakuje lub jest zbyt długie - jest wymagane, gdy action ma wartość "message", mieści się w nim 400 znaków. - 400
voicemail_requires_enginevoicemail wymaga silnika jako początkowego wyboru dostawcy: useForOutboundTasks: true albo pominięcia pola, gdy silnik jest domyślny. false kończy się niepowodzeniem nawet z transferTo, ponieważ walidacja poprzedza próbę przełączenia. - 400
invalid_workflowNieprawidłowy graf workflow (nieosiągalny węzeł, brak węzła wejściowego, zarezerwowana nazwa narzędzia...); nic nie jest zapisywane, konkretny powód zawsze znajduje się w message. - 404
calendar_integration_not_foundcalendar.integrationId nie wskazuje połączenia z kalendarzem na Państwa koncie - należy wybrać je ponownie z GET /v1/integrations. - 400
calendar_invalid_calendarcalendar.calendarId nie należy do kalendarzy tego połączenia albo jest tylko do odczytu (accessRole reader lub freeBusyReader), więc agent nie mógłby zapisać w nim terminu. Należy wybrać kalendarz z możliwością zapisu z list_calendars. - 400
calendar_invalid_timezonecalendar.timezone nie jest prawidłową strefą IANA, na przykład "Europe/Prague". - 400
calendar_invalid_windowsOkna w calendar.windows nie używają formatu HH:MM, nakładają się albo jest ich więcej niż cztery w jednym dniu. - 400
calendar_no_windowsKalendarz jest włączony, ale żaden dzień tygodnia nie ma okna - agent nie miałby nic do zaoferowania. - 400
calendar_invalid_rulesLiczbowe reguły kalendarza są poza zakresem albo minimalne wyprzedzenie jest dłuższe niż horyzont wyszukiwania. - 404
tool_not_foundNarzędzie z toolIds nie istnieje albo nie należy do Państwa konta. - 404
not_foundnumberE164 nie istnieje albo nie należy do Państwa konta. - 400
validationKształt danych wejściowych się nie zgadza - zwykle voiceId nie pasuje do żadnej pozycji z GET /v1/voices i nie wygląda na surowy identyfikator ElevenLabs (komunikat błędu wymienia prawidłowe wartości), albo silencePromptSecs jest poza zakresem od 3 do 25, firstMessageProtectSecs poza zakresem od 0 do 20, albo silencePromptText jest dłuższe niż 80 znaków, albo inboundVariables zawiera więcej niż 10 kluczy, zarezerwowaną lub źle sformułowaną nazwę klucza albo wartość dłuższą niż 200 znaków, albo sttLanguageHintsStrict nie ma wartości true, false ani null, albo turnDigitTailFloorMs lub turnLongSpeechFloorMs jest poza 0 i 400 do 2300, albo turnLongSpeechSecs jest poza zakresem od 1 do 30. - 409
invalid_transitionProdukt "numer do połączeń i SMS", który podłączają Państwo do agenta, nie jest aktywny (czeka na rejestrację, został odrzucony albo jest zwalniany). Agenta można podłączyć tylko do aktywnego numeru. Agent nie został utworzony i nic nie zostało zmienione. - 409
in_progressInna operacja (aktywacja, rozliczenie lub zwolnienie) przetwarza produkt "numer do połączeń i SMS", który podłączają Państwo do agenta. Agent nie został utworzony i nic się nie zmieniło; prosimy powtórzyć żądanie za minutę. - 500
agent_template_missingSzablonu tworzenia agenta brakuje lub jest uszkodzony po naszej stronie - tymczasowy błąd po naszej stronie, prosimy skontaktować się z pomocą techniczną. - 502
engine_unavailablePodczas podłączania produktu "numer do połączeń i SMS" własny silnik głosowy volai nie odpowiedział. Agent nie został utworzony i nic nie zostało zmienione; prosimy powtórzyć żądanie za chwilę. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji): komunikat to mówi, a żądanie można powtórzyć za kilka minut. - 502
engine_readback_mismatchPodczas podłączania produktu "numer do połączeń i SMS" silnik odpowiedział, ale odczyt zwrotny pokazał inne mapowanie niż to, które wysłaliśmy. Agent nie został utworzony i nic nie zostało zmienione; prosimy powtórzyć żądanie. - 503
engine_not_configuredZażądali Państwo własnego silnika głosowego volai (useForOutboundTasks: true), ale silnik lub ruch wychodzący na nim nie jest teraz skonfigurowany. Należy sprawdzić konfigurację albo pominąć pole - agent zostanie utworzony na ElevenLabs. - 503
engine_setup_incompleteAgent został utworzony, ale konfiguracja zadań wychodzących na własnym silniku volai nie została ukończona. NIE należy ponawiać tworzenia, co utworzyłoby duplikat. Agenta należy znaleźć wGET /v1/agentspo nazwie, sprawdzićprovider, a następnie dokończyć konfigurację w portalu Agenci lub z pomocą techniczną; samoPATCH /v1/agents/{id}nie może wybrać dostawcy.
GET/v1/agents/{id}
Szczegóły pojedynczego agenta. Usunięty agent zwraca 404 agent_not_found. Przy tworzeniu useForOutboundTasks wybiera początkową platformę: true wybiera silnik volai, false ElevenLabs, a pominięcie używa wartości domyślnej wdrożenia. Ponieważ POST zwraca tylko id, wynik należy zweryfikować w tym kolejnym GET i jego polu provider. Włączenie transferTo na agencie ElevenLabs wywołuje próbę przełączenia na silnik, ale może być wstrzymana, niedostępna lub zakończyć się niepowodzeniem; przekazanie nie działa, dopóki provider pozostaje elevenlabs. Udane przełączenie może zmienić głos. Zmiana language przez PATCH z języka, w którym nowi agenci startują na ElevenLabs (dziś en, de, pl), na język, w którym startują na silniku (cs, sk), wywołuje tę samą próbę: po sukcesie agent dostaje domyślny głos silnika dla nowego języka (Milena, Katarina), po niepowodzeniu pozostaje na ElevenLabs. Zmiana między cs a sk niczego nie przenosi. Inne przeniesienia dostawcy obsługuje pomoc techniczna. ElevenLabs akceptuje z katalogu tylko anet lub surowy identyfikator głosu ElevenLabs; inne głosy z katalogu wymagają silnika. Odpowiedź zawsze zawiera też pole laughterEffective ({active, reason, sensitiveCategory} | null) - bieżący wynik polityki śmiechu dla OPUBLIKOWANEJ konfiguracji agenta, liczony na świeżo poza jakimkolwiek połączeniem. Dla agenta ElevenLabs jest to zawsze {active: false, reason: "not_engine", sensitiveCategory: null} bez wywoływania silnika; dla agenta silnika reason ma jedną z kilku wartości (ok, agent_off, sensitive_prompt, voice_not_listed i inne), a silnik może z czasem dodawać nowe - nieznaną wartość należy traktować jako wyłączenie, a nie błąd. Gdy silnik nie odpowie w ciągu 3 sekund, nie działa albo nie zna agenta (ciche 404 na starszym buildzie), laughterEffective ma po prostu wartość null - nigdy domysł.
Gdy silnik nie odpowie w ciągu 3 sekund, nie działa albo nie zna agenta, odpowiedź wygląda tak: "laughterEffective": null - a nie obiekt z odgadniętymi wartościami.
Żądanie
curl https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"agent": {
"id": "ag_kx91fa2b",
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, w czym mogę pomóc?",
"voiceId": "milena",
"provider": "engine",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "Dzwoniący wyraźnie prosi o połączenie z człowiekiem.",
"dataFields": [
{
"key": "name",
"type": "string",
"description": "Imię i nazwisko dzwoniącego, tak jak je podał."
},
{
"key": "urgency",
"type": "string",
"description": "Jak pilna była prośba.",
"enumValues": ["low","normal","high"]
}
],
"recordCalls": true,
"recordingNotice": false,
"goal": null,
"knowledge": null,
"notifyEmail": true,
"silencePromptSecs": 10,
"laughter": "auto",
"firstMessageProtectSecs": null,
"silencePromptText": null,
"sttLanguageHintsStrict": false,
"turnDigitTailFloorMs": null,
"turnLongSpeechFloorMs": null,
"turnLongSpeechSecs": null,
"missedCallSms": { "enabled": false },
"suspended": false,
"laughterEffective": {
"active": true,
"reason": "ok",
"sensitiveCategory": null
}
}
}Stany błędów
- 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta.
GET/v1/agents/{id}/call-hold
Odczytuje stałe wstrzymanie połączeń agenta ustawione przez właściciela. Surowa odpowiedź to {agentId, held, hold}; przed zatrzymaniem held:false, hold:null. Nie opisuje zawieszenia administracyjnego ani innych bramek dopuszczania połączeń.
Żądanie
curl https://volai.cz/v1/agents/ag_example/call-hold -H 'Authorization: Bearer vk_YOUR_KEY'Odpowiedź
{
"agentId": "ag_example",
"held": false,
"hold": null
}Stany błędów
- 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta.
POST/v1/agents/{id}/call-hold
Trwale zatrzymuje nowe połączenia wychodzące i zweryfikowane oddzwonienia kampanii dla agenta silnika należącego do konta. Należy podać dokładnie reason:budget_limit i operationId (1-128 liter, cyfr, kropek, podkreśleń, dwukropków lub myślników, zaczynające się od litery lub cyfry). Nie ma wygaśnięcia ani operacji wznowienia. Identyczny ID i treść odtwarzają pierwotny audyt; inna operacja zwraca 409 revision_conflict. Po niepewnej odpowiedzi należy odczytać GET. hold zawiera agentId, reason, operationId, fingerprint i createdAt. Wstrzymania administracyjne, demo i aktywne połączenia pozostają bez zmian. Niezależne błędy rozwiązywania oddzwonień nadal zachowują agenta domyślnego numeru. Idempotency-Key nie jest tu mechanizmem idempotentności.
Żądanie
curl https://volai.cz/v1/agents/ag_example/call-hold -X POST -H 'Authorization: Bearer vk_YOUR_KEY' -H 'Content-Type: application/json' -d '{"reason":"budget_limit","operationId":"budget-stop-001"}'Odpowiedź
{
"agentId": "ag_example",
"held": true,
"hold": {
"agentId": "ag_example",
"reason": "budget_limit",
"operationId": "budget-stop-001",
"fingerprint": "14e5bfb3f57897b30d172ef4ba861b2a27e7beb1de278a94b47bc0f14c614f4c",
"createdAt": "2026-10-08T12:00:00.000Z"
}
}Stany błędów
- 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 400
validationNieprawidłowa treść lub nieobsługiwany agent. Należy wybrać agenta silnika należącego do konta, spoza wspólnego demo. - 409
revision_conflictIstnieje inne trwałe wstrzymanie. Pierwotny audyt zostaje zachowany; jego stan można odczytać przez GET.
PATCH/v1/agents/{id}
Aktualizuje istniejącego agenta - te same pola co przy tworzeniu, wszystkie opcjonalne. Należy wysłać tylko to, co ma się zmienić. Dwa z nich są jednak zawsze wysyłane W CAŁOŚCI, ponieważ zastępują istniejącą listę: toolIds i dataFields. Pusta tablica wyłącza więc daną funkcję ("toolIds": [] usuwa wszystkie narzędzia agenta), natomiast brakujący klucz oznacza "zostaw bez zmian". Przekazanie połączenia człowiekowi wyłącza się pustym ciągiem: "transferTo": "".
Żądanie
curl -X PATCH https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?"}'Odpowiedź
{
"agent": {
"id": "ag_kx91fa2b",
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"voiceId": "milena",
"provider": "engine",
"numberE164": "+420601234567",
"createdAt": 1756111000000,
"status": "active",
"toolIds": ["tl_5c2a91f4"],
"transferTo": "+420777123456",
"transferCondition": "Dzwoniący wyraźnie prosi o połączenie z człowiekiem.",
"recordCalls": true,
"recordingNotice": false,
"goal": null,
"knowledge": null,
"notifyEmail": true,
"silencePromptSecs": 10,
"laughter": "auto",
"firstMessageProtectSecs": null,
"silencePromptText": null,
"sttLanguageHintsStrict": false,
"turnDigitTailFloorMs": null,
"turnLongSpeechFloorMs": null,
"turnLongSpeechSecs": null,
"missedCallSms": { "enabled": false },
"suspended": false
}
}Stany błędów
- 400
invalid_nameNazwa agenta musi mieć od 1 do 60 znaków. - 400
invalid_system_promptInstrukcje agenta muszą mieć od 10 do 6000 znaków. - 400
invalid_languagelanguage musi być cs, sk, en, de lub pl. - 400
tool_limittoolIds zawiera więcej niż 10 narzędzi. - 400
invalid_data_fieldsdataFields zawiera nieprawidłowy klucz, typ, opis albo więcej niż 10 pozycji. - 400
invalid_goalgoal jest zbyt długie - mieści się w nim 300 znaków. - 400
invalid_knowledgeknowledge jest zbyt długie - mieści się w nim 20000 znaków. - 400
transfer_looptransferTo wskazuje numer volai na tym samym koncie. - 400
blocked_destinationtransferTo jest linią o podwyższonej opłacie ze specjalnym cennikiem. - 400
destination_not_allowedAgent ma produkt "numer do połączeń i SMS" (albo właśnie go Państwo podłączają), atransferTonie jest numerem czeskim (+420): połączenia przełączane z takiego numeru mogą trafiać tylko na numery czeskie. Należy podać czeskitransferToalbo nie podłączać tego numeru do agenta. Dla produktu "numer w innym kraju" numer przełączenia musi znajdować się w kraju numeru (gdy dzwonienie tam jest włączone) lub w Czechach. Nic nie zostało zmienione. - 400
unsupported_for_number_typePrzypisują Państwo (lub podmieniają) produkt "numer do połączeń i SMS", ale agent nie działa na własnym silniku głosowym volai: tego rodzaju numer odbiera wyłącznie agent silnika. Nic nie zostało zmienione. - 400
invalid_voicemail_messagevoicemail.message brakuje lub jest zbyt długie - jest wymagane, gdy action ma wartość "message", mieści się w nim 400 znaków. - 400
voicemail_requires_enginevoicemail jest tylko dla agenta z provider "engine". Agenta ElevenLabs przenosi na silnik pomoc techniczna; do tego czasu pole należy pominąć. - 400
invalid_workflowNieprawidłowy graf workflow (nieosiągalny węzeł, brak węzła wejściowego, zarezerwowana nazwa narzędzia...); nic nie jest zapisywane, konkretny powód zawsze znajduje się w message. - 404
calendar_integration_not_foundcalendar.integrationId nie wskazuje połączenia z kalendarzem na Państwa koncie - należy wybrać je ponownie z GET /v1/integrations. - 400
calendar_invalid_calendarcalendar.calendarId nie należy do kalendarzy tego połączenia albo jest tylko do odczytu (accessRole reader lub freeBusyReader), więc agent nie mógłby zapisać w nim terminu. Należy wybrać kalendarz z możliwością zapisu z list_calendars. - 400
calendar_invalid_timezonecalendar.timezone nie jest prawidłową strefą IANA, na przykład "Europe/Prague". - 400
calendar_invalid_windowsOkna w calendar.windows nie używają formatu HH:MM, nakładają się albo jest ich więcej niż cztery w jednym dniu. - 400
calendar_no_windowsKalendarz jest włączony, ale żaden dzień tygodnia nie ma okna - agent nie miałby nic do zaoferowania. - 400
calendar_invalid_rulesLiczbowe reguły kalendarza są poza zakresem albo minimalne wyprzedzenie jest dłuższe niż horyzont wyszukiwania. - 404
tool_not_foundNarzędzie z toolIds nie istnieje albo nie należy do Państwa konta. - 404
agent_not_foundAgent nie istnieje albo nie należy do Państwa konta. - 400
validationKształt danych wejściowych się nie zgadza - zwykle voiceId nie pasuje do żadnej pozycji z GET /v1/voices i nie wygląda na surowy identyfikator ElevenLabs (komunikat błędu wymienia prawidłowe wartości), albo silencePromptSecs jest poza zakresem od 3 do 25, firstMessageProtectSecs poza zakresem od 0 do 20, albo silencePromptText jest dłuższe niż 80 znaków, albo inboundVariables zawiera więcej niż 10 kluczy, zarezerwowaną lub źle sformułowaną nazwę klucza albo wartość dłuższą niż 200 znaków, albo sttLanguageHintsStrict nie ma wartości true, false ani null, albo turnDigitTailFloorMs lub turnLongSpeechFloorMs jest poza 0 i 400 do 2300, albo turnLongSpeechSecs jest poza zakresem od 1 do 30. - 409
invalid_transitionProdukt "numer do połączeń i SMS", który przypisują Państwo do agenta, nie jest aktywny (czeka na rejestrację, został odrzucony albo jest zwalniany). Agenta można podłączyć tylko do aktywnego numeru. Nic nie zostało zmienione. - 409
in_progressAlbo inna zmiana (wersja robocza lub publikacja) jest już w toku na tym agencie, albo poprzednia zmiana doszła do skutku, ale nie dało się jej bezpiecznie potwierdzić - przed ponowieniem tego PATCH należy odczytaćGET /v1/agents/{id}/draft/operationi zdecydować na podstawieoperation. Ten sam kod jest zwracany, gdy produkt "numer do połączeń i SMS", który podłączają Państwo do agenta lub podmieniają, jest przetwarzany przez inną operację (aktywację, rozliczenie lub zwolnienie): jest to stan przejściowy, ale przy zamianie numerów pierwotny numer mógł już zostać odłączony, więc najpierw należy odczytać agenta (GET /v1/agents/{id}) i powtórzyć PATCH za minutę. - 502
engine_unavailableDla agenta już działającego na własnym silniku volai: silnik teraz nie odpowiada, zmiana nie została zapisana. Prosimy spróbować za chwilę. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji, zwykle po przełączeniu awaryjnym): komunikat to mówi, zmiana na pewno nie została zapisana, agent nie jest blokowany jako niepewny, a to samo żądanie można powtórzyć za kilka minut. - 503
engine_not_configuredDla agenta już działającego na własnym silniku volai: silnik nie jest teraz skonfigurowany po naszej stronie, zmiana nie została zapisana. Prosimy skontaktować się z pomocą techniczną. - 502
engine_readback_mismatchDla agenta już działającego na własnym silniku volai: silnik zapisał inną konfigurację niż ta, którą wysłaliśmy, zmiana nie została zapisana. Prosimy spróbować ponownie.
DELETE/v1/agents/{id}
Usuwa agenta zarówno z volai, jak i z ElevenLabs. Numer, który był do niego podłączony, pozostaje Państwa - wystarczy nadać mu nowe kierowanie: numer głosowy przez PATCH /v1/numbers/{e164}, produkt "numer do połączeń i SMS" przez PATCH /v1/voice-sms-numbers/{e164} (MCP set_voice_sms_number_routing). Odłączenie numeru od agenta jest częścią usunięcia: jeśli się nie powiedzie (dla produktu "numer do połączeń i SMS" 409 in_progress, 502 routing_failed), agent nie jest usuwany, a numer pozostaje taki jak był - prosimy powtórzyć żądanie za chwilę.
Żądanie
curl -X DELETE https://volai.cz/v1/agents/ag_kx91fa2b \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"deleted": true
}Stany błędów
- 404
agent_not_foundAgent nie istnieje albo nie należy do Państwa konta. - 409
in_progressNumer agenta jest przetwarzany przez inną operację (dla produktu "numer do połączeń i SMS": aktywację, rozliczenie lub zwolnienie). Agent nie został usunięty, a numer pozostaje taki jak był; prosimy powtórzyć żądanie za minutę. - 502
routing_failedOdłączenie numeru od agenta się nie powiodło (dla produktu "numer do połączeń i SMS": we własnym silniku głosowym). Agent nie został usunięty, a numer pozostaje skierowany tak jak wcześniej; prosimy powtórzyć żądanie za chwilę.
Odpowiedzi POST /v1/agents, PATCH /v1/agents/{id}, POST /v1/agents/{id}/draft/publish i POST /v1/agents/{id}/draft/rollback mogą zawierać opcjonalne pole warnings. Kilka kodów dotyczy pierwszej kwestii: first_message_no_ai_disclosure, gdy zapisana pierwsza kwestia agenta nie ujawnia, że jest asystentem cyfrowym (EU AI Act, art. 50), oraz first_message_too_long, gdy szacowany czas mówienia tego, co dzwoniący faktycznie słyszy (pierwsza kwestia plus zdanie o nagrywaniu, gdy jest włączone: recordingNotice razem z recordCalls), przekracza 7 sekund (około 2,53 słowa na sekundę; cyfry zawsze, a skróty pisane wielkimi literami do czterech znaków liczą się po znaku). Niezmieniona domyślna pierwsza kwestia nigdy nie wywołuje tego kodu, tylko taka, którą klient edytował. Dzwoniący, którzy przerywają agentowi, robią to najczęściej w ciągu sekundy słyszalnej mowy. Agent z fazami (pole workflow, zob. sekcja Fazy połączenia (workflow) poniżej) dodaje do tego samego pola jeszcze dziesięć kodów - dead_end, empty_node, entry_phrase_never_spoken, missing_enter_phrase, short_condition, condition_about_agent, base_prompt_long, workflow_not_synced_to_provider, workflow_node_tool_not_synced i workflow_node_tool_missing (węzeł odwołuje się do narzędzia, którego nie ma już na koncie - zostało usunięte; fazę trzeba zapisać bez niego) - i zwykle zwraca je nawet wtedy, gdy pierwsza kwestia jest w porządku (szablon Recepcja zwraca trzy). Ostrzeżenie dotyczące jednej konkretnej fazy lub przejścia zawiera także nodeId lub edgeId. Żaden z kodów nie blokuje zapisu - tylko go sygnalizują. Pole jest nieobecne w odpowiedzi tylko wtedy, gdy nie ma nic do zasygnalizowania. Cofnięcie wersji roboczej do wcześniejszej rewizji (draft/rollback) zwraca warnings tak samo jak publikacja, ponieważ również zapisuje pierwszą kwestię w działającym agencie. Tylko publikacja i cofnięcie (nie POST/PATCH /v1/agents) mogą w tym samym polu zwrócić także number_changed_by_publish, z wartościami from/to (E.164 lub null dla braku numeru), gdy ponowna publikacja faktycznie zmienia numer telefonu działającego agenta - to również nie blokuje publikacji. Osobny kod, calendar_pending_engine_switch, pojawia się, gdy agent ma skonfigurowany calendar (terminy w kalendarzu), ale działa na ElevenLabs - ustawienia są zapisane i zaczną działać, gdy agent przejdzie na własny silnik volai, co włączenie kalendarza wywołuje samo. Agent z podłączonym numerem telefonu (numberE164), którego instrukcje (systemPrompt lub firstMessage) używają {{variable}} spoza zestawu, który połączenie przychodzące faktycznie dostarcza (caller_number, called_number, attempt_id, volai_blocked, volai_block_reason, trial_prompt, trial_first_message oraz siedem zmiennych system__*), dostaje prompt_variables_unavailable_inbound - w połączeniu przychodzącym ta zmienna jest zastępowana pustym tekstem. Gdy usunięcie wszystkich bloków {{...}} z niepustej pierwszej kwestii nie zostawia w niej żadnej litery ani cyfry poza zmiennymi, dostaje ona first_message_empty. Osobny kod, agent_misuse_suspected, pojawia się, gdy konfiguracja agenta (dla powyższych odpowiedzi) zawiera sformułowania, które mogą sprawiać wrażenie podszywania się pod organ władzy lub inną instytucję albo prośby o informacje poufne - zob. warunki volai, sekcja 10. Ten sam kod w tym samym kształcie wraca także w opcjonalnym polu warnings w PUT /v1/agents/{id}/draft i POST /v1/agents/{id}/simulate (wersja robocza), POST /v1/calls (prompt połączenia próbnego lub variables), POST /v1/tasks i PATCH /v1/tasks/{id} (nazwa zadania i zmienne odbiorców) oraz POST /v1/tools i PATCH /v1/tools/{id} (opis narzędzia i jego parametry). Nigdy nie blokuje zapisu ani wysyłki - to tylko ostrzeżenie. Tylko POST /v1/agents może zwrócić także elevenlabs_by_explicit_choice, gdy żądanie wysłało useForOutboundTasks: false dla języka, który wdrożenie w innym przypadku uruchomiłoby na własnym silniku głosowym volai (dziś cs i sk), więc agent pozostał na ElevenLabs - bez głosów silnika i bez funkcji działających wyłącznie na silniku. Agent jest nadal tworzony, ostrzeżenie tylko to sygnalizuje; jeśli chcieli Państwo agenta tylko do połączeń przychodzących, pole należy pominąć.
{
"id": "ag_kx91fa2b",
"warnings": [
{
"code": "first_message_no_ai_disclosure",
"message": {
"cs": "Podle evropského AI Act (čl. 50) musí první věta prozradit, že mluví digitální asistentka. Stačí: „Dobrý den, tady digitální asistentka firmy Novák.“ Bez toho za sdělení odpovídáš ty.",
"en": "Under the EU AI Act (Article 50), the first line must reveal that a digital assistant is speaking. For example: \"Hello, this is the digital assistant for Novak Inc.\" Without it, you are responsible for the disclosure."
}
}
]
}Nowe pola agenta
Oprócz nazwy, promptu, głosu i numeru POST /v1/agents i PATCH /v1/agents/{id} przyjmują też te pola. Wszystkie są opcjonalne przy tworzeniu. toolIds, transferTo, transferCondition, dataFields i recordCalls nie występują w odpowiedzi dla starszego agenta, dopóki ktoś ich nie ustawi. goal i knowledge są natomiast zawsze zwracane - nieustawione jako null, a nie jako brakujący klucz - a notifyEmail jest zawsze wartością logiczną (brak oznacza włączone). recordingNotice jest również zawsze wartością logiczną - false, dopóki ktoś go nie włączy. Odpowiedź zawsze zawiera też provider (engine lub elevenlabs) - mówi, na jakiej platformie agent faktycznie działa, obie wartości występują w produkcji. Podobnie zawsze zawiera suspended (wartość logiczna) - true, gdy operator volai ręcznie zawiesił agenta; taki agent nie może odbierać ani wykonywać połączeń (agent_suspended), a pola nie można ustawić przez API ani MCP. useForOutboundTasks jest wyjątkiem - przyjmuje je tylko POST /v1/agents, nie ma go w PATCH (istniejącego agenta między dostawcami przenosi volai, nie klient; samo robi to po włączeniu transferTo lub calendar albo po zmianie language na język, w którym nowi agenci startują na silniku - zob. szczegóły agenta powyżej). voicemail jest dostępne tylko dla agenta z provider: "engine" - dla pozostałych POST/PATCH z tym polem kończy się niepowodzeniem. silencePromptSecs (przypomnienie o ciszy) jest natomiast akceptowane i zwracane dla obu dostawców - dla agenta na ElevenLabs wartość jest tylko zapisywana, a słychać ją dopiero po przejściu agenta na własny silnik głosowy volai. firstMessageProtectSecs (chroni pierwszą kwestię przed przerwaniem, od 0 do 20 sekund) i silencePromptText (własne zdanie przypomnienia o ciszy, do 80 znaków) są akceptowane i zwracane również dla OBU dostawców, ale w przeciwieństwie do silencePromptSecs BEZ wymuszonej wartości domyślnej: brakujące pole w POST nie jest zapisywane, a odpowiedź pokazuje null (ochrona wyłączona, tekst domyślny), zaś PATCH bez klucza pozostawia zapisaną wartość bez zmian. workflow (fazy połączenia) jest dostępne u obu dostawców, ale nie jednakowo w pełni - zob. sekcja Fazy połączenia (workflow) poniżej. laughter (śmiech agenta) jest również zawsze obecne, tak jak goal/knowledge/notifyEmail powyżej - dla starszego agenta bez ustawionej wartości ma wartość auto, nigdy nie jest brakującym kluczem; akceptowane i zapisywane dla OBU dostawców, ale słyszalne tylko dla agenta na silniku z czeskim głosem Cartesia. inboundVariables (wartości domyślne dla zmiennych {{variable}} w połączeniach PRZYCHODZĄCYCH) jest opcjonalne jak toolIds - agent bez nich nie ma tego klucza w odpowiedzi; akceptowane i zwracane dla OBU dostawców, ponieważ silnik i ElevenLabs czytają ten sam webhook inicjujący, i nieużywane w połączeniach wychodzących. sttLanguageHintsStrict (ścisła blokada języka dla rozpoznawania mowy) jest zawsze obecne jako wartość logiczna - false, gdy agent go nie ustawił; akceptowane i zapisywane dla OBU dostawców, ale czytane tylko przez własny silnik volai, ElevenLabs je ignoruje. turnDigitTailFloorMs, turnLongSpeechFloorMs i turnLongSpeechSecs (progi ciszy na końcu wypowiedzi dzwoniącego) są zawsze obecne jako liczby lub null, tak jak firstMessageProtectSecs - BEZ wymuszonej wartości domyślnej: brakujące pole w POST nie jest zapisywane, a odpowiedź pokazuje null (silnik używa swoich domyślnych 0 / 0 / 5), PATCH bez klucza pozostawia zapisaną wartość, a null ją usuwa; akceptowane i zapisywane dla OBU dostawców, czytane tylko przez własny silnik volai.
| Pole | Co robi |
|---|---|
toolIds | Tablica id narzędzi webhookowych z GET /v1/tools, które agent może wywoływać podczas połączenia. Najwyżej 10. Narzędzie należy do konta, więc może je mieć włączone więcej niż jeden agent. |
transferTo | Numer w formacie E.164, na który agent przełącza dzwoniącego, gdy ten poprosi o człowieka. Pusty ciąg wyłącza przekazanie. Nie można tu użyć własnego numeru volai konta (transfer_loop). |
transferCondition | Kiedy przełączyć, zapisane jako instrukcja dla modelu (do 500 znaków). Jeśli nie zostanie wysłane, podstawiamy zdanie domyślne. |
dataFields | Co agent ma zebrać z połączenia: tablica obiektów {key, type, description, enumValues}, najwyżej 10. key to małe litery, cyfry i podkreślenia; type to string, number lub boolean; enumValues można ustawić tylko dla pola tekstowego. Wypełnione wartości trafiają następnie do pola data w połączeniu. |
recordCalls | Czy nagrywać połączenia tego agenta. Domyślnie włączone; przy false nie powstaje nagranie dla połączeń rozpoczętych po zapisaniu zmiany, a GET /v1/calls/{id}/recording zwraca dla nich no_recording (połączenie, które już dzwoni lub trwa, stosuje ustawienie z chwili rozpoczęcia). Samo nagrywanie niczego nie mówi dzwoniącemu - zdanie z informacją włącza recordingNotice. Zadbanie o to, aby informacja spełniała wymogi prawa w Państwa sytuacji, należy do Państwa. |
recordingNotice | Czy agent kończy swoją pierwszą kwestię informacją dla dzwoniącego, że rozmowa jest nagrywana - krótkie zdanie w języku agenta (po angielsku "Please note this call is recorded."). Domyślnie wyłączone: pominięte pole nie zapisuje niczego w POST, a PATCH bez klucza pozostawia je bez zmian. Odtwarzane tylko wtedy, gdy recordCalls jest włączone; odpowiedź zawsze zawiera je jako wartość logiczną. Od 22 października 2026 volai nie dodaje go już samo. |
goal | Cel połączenia w jednym zdaniu - sędzia LLM używa go po połączeniu, aby rozstrzygnąć, czy agent osiągnął to, czego chciał dzwoniący (oceniane tylko dla agenta na własnym silniku volai, zob. sekcja Cel i ocena w dokumentacji Agenta głosowego). Do 300 znaków. PATCH z null lub pustym ciągiem czyści cel. |
knowledge | Tekst, który agent zna oprócz swoich instrukcji - cennik, godziny otwarcia, częste pytania. Dołączany na końcu promptu. Do 20000 znaków. PATCH z null lub pustym ciągiem czyści wiedzę. |
notifyEmail | Czy wysyłać właścicielowi konta e-mailem podsumowanie po każdym połączeniu, które odbierze ten agent - dziś e-mail wychodzi tylko dla agenta na własnym silniku volai (provider: "engine"); dla agenta na ElevenLabs wartość jest zapisywana, ale e-mail nie jest jeszcze wysyłany. Domyślnie włączone. |
useForOutboundTasks | true wybiera przy tworzeniu silnik volai, false zaczyna na ElevenLabs, a pominięcie używa wartości domyślnej wdrożenia, która zależy od dostępności silnika, jego włączenia i skonfigurowanych języków. Sam wybór cs lub sk nie gwarantuje silnika; gdy domyślny silnik jest wyłączony, tworzenie zaczyna się na ElevenLabs. POST zwraca tylko id, więc provider należy odczytać kolejnym GET /v1/agents/{id}. Pole działa tylko przy tworzeniu; inne przeniesienia obsługuje pomoc techniczna. transferTo wywołuje także próbę przeniesienia z ElevenLabs na silnik, której wynik trzeba zweryfikować. |
voicemail | Wykrywanie poczty głosowej w połączeniach WYCHODZĄCYCH. message jest wymagane dla action: "message", do 400 znaków. PATCH akceptuje je tylko z provider: "engine". W POST początkowym wyborem musi być silnik: jawne useForOutboundTasks: true albo pominięcie, gdy silnik jest domyślny. false kończy się voicemail_requires_engine nawet z transferTo, ponieważ walidacja poprzedza próbę przełączenia. PATCH z null je czyści. |
silencePromptSecs | Ile sekund ciszy po stronie dzwoniącego wywołuje jedno przypomnienie ("Czy Pan jest na linii?"), liczba całkowita od 3 do 25. null je wyłącza. Brak w POST zapisuje wartość domyślną 10. Akceptowane i zapisywane dla OBU dostawców - dla agenta na standardowej platformie ElevenLabs (provider: "elevenlabs") wartość jest tylko zapisywana, a słychać ją dopiero po przejściu agenta na własny silnik głosowy volai, jedyny, który potrafi je odtworzyć. Wartość spoza zakresu od 3 do 25 kończy się ogólnym błędem walidacji (validation, 400) - bez dedykowanego kodu. |
firstMessageProtectSecs | Ile sekund pierwszej kwestii chronić przed krótką reakcją osoby, która odebrała ("Halo?", "Tak?"), liczba od 0 do 20 (dozwolone ułamki dziesiętne), tylko połączenia WYCHODZĄCE. Odpowiedź osoby, która odebrała, bez słowa przerywającego (na przykład stop, nie, czekaj), jest odrzucana w chronionym oknie: nie przerywa kwestii, nie trafia do transkrypcji, a agent na nią nie reaguje. Słowo przerywające lub wyraźna prośba o niedzwonienie nadal ją przerywa. Słowa przerywające zależą od języka agenta: agenta angielskiego przerywają stop, no, wait lub hold. null (lub 0) wyłącza ochronę. Nie jest stosowana żadna wartość domyślna: brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje null; PATCH bez klucza pozostawia zapisaną wartość bez zmian. Akceptowane i zapisywane dla OBU dostawców - dla agenta na standardowej platformie ElevenLabs (provider: "elevenlabs") wartość jest tylko zapisywana, a słychać ją dopiero po przejściu agenta na własny silnik głosowy volai. Wartość spoza zakresu od 0 do 20 kończy się ogólnym błędem walidacji (validation, 400) - bez dedykowanego kodu. |
silencePromptText | Własne zdanie przypomnienia o ciszy, do 80 znaków, wypowiadane słowo w słowo (bez podstawiania zmiennych) zamiast domyślnego przypomnienia dla języka agenta ("Are you still there?" po angielsku). Najlepiej zakończyć je znakiem zapytania, aby krótkie "Tak" po nim zostało zachowane jako odpowiedź. Białe znaki wokół są obcinane; pusty ciąg lub null przywraca tekst domyślny dla języka agenta. Nie jest stosowana żadna wartość domyślna: brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje null; PATCH bez klucza pozostawia zapisany tekst bez zmian. Akceptowane i zapisywane dla OBU dostawców, ale słyszalne tylko na własnym silniku volai i tylko wtedy, gdy przypomnienie o ciszy jest włączone (silencePromptSecs nie jest null). Tekst dłuższy niż 80 znaków kończy się ogólnym błędem walidacji (validation, 400). |
inboundVariables | Wartości domyślne dla zmiennych {{variable}} w połączeniach PRZYCHODZĄCYCH, obiekt {"name": "value"} - na przykład {"uvod": "How can I help you?"} dla pierwszej kwestii "... this call is recorded. {{uvod}}". W połączeniu wychodzącym zmienne przekazuje się przy wybieraniu numeru (variables w POST /v1/calls); w połączeniu przychodzącym nikt ich nie dostarcza, a silnik wstawiłby pusty tekst, dlatego te wartości są scalane ze zmiennymi połączenia w połączeniach przychodzących - tylko dla nazw, których samo połączenie nie dostarcza (caller_number, called_number, attempt_id, volai_*, trial_* i system__* zawsze mają pierwszeństwo, a walidacja odrzuca takie nazwy). Nieużywane w połączeniach wychodzących. Najwyżej 10 kluczy z liter, cyfr i podkreśleń (od 1 do 40 znaków), wartości są tekstem do 200 znaków (białe znaki wokół są obcinane). W PATCH obiekt ZASTĘPUJE zapisane wartości w całości (bez scalania klucz po kluczu), null lub {} je usuwa, a pominięty klucz pozostawia je bez zmian; w POST pominięte pole nie zapisuje niczego. Odpowiedź pomija klucz, gdy agent nie ma wartości domyślnych. Działa dla OBU dostawców - silnik i ElevenLabs czytają ten sam webhook. Zmienna z wartością domyślną nie jest już zgłaszana ostrzeżeniem prompt_variables_unavailable_inbound. Kształt spoza tych limitów kończy się ogólnym błędem walidacji (validation, 400). |
sttLanguageHintsStrict | Ścisła blokada języka dla rozpoznawania mowy (Soniox language_hints_strict): model transkrypcji zdecydowanie preferuje język agenta, więc krótka odpowiedź, taka jak "Ano" w połączeniu wychodzącym, nie jest zapisywana jako obce słowo ani innym alfabetem. Dołożenie starań, nie gwarancja. Tylko dla agentów na własnym silniku volai z językiem cs i połączeń wychodzących; NIE należy włączać jej dla sk - czescy rozmówcy otrzymaliby słowacką pisownię. Język agenta nie jest wymuszany. true ją włącza, false lub null wyłącza; brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje false; PATCH bez klucza pozostawia zapisaną wartość bez zmian. Odpowiedź zawsze zawiera to pole jako wartość logiczną. Akceptowane i zapisywane dla OBU dostawców, czytane tylko przez silnik - ElevenLabs je ignoruje. Jest wysyłana do silnika przy każdym zapisie agenta, więc zapis w portalu nie wyłącza włączonej blokady. |
turnDigitTailFloorMs | Próg ciszy w milisekundach dla odpowiedzi kończącej się grupą od 3 do 8 cyfr (dzwoniący dyktujący numer grupa po grupie bierze oddech między grupami): po tym, jak dzwoniący przestanie mówić, silnik czeka co najmniej tyle, zanim zamknie odpowiedź, aby agent nie wskoczył w numer. 0 wyłącza; w przeciwnym razie 400 do 2300 (od 1 do 399 jest odrzucane). Zalecanym pierwszym krokiem dla czeskich agentów przyjmujących dyktowane numery jest 800, drugim 1200, praktycznym limitem 1500. Daty, kody pocztowe i lata też dostają ten próg (akceptowany koszt). Brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje null (silnik używa swojej wartości domyślnej 0); PATCH z null usuwa zapisaną wartość, a bez klucza pozostawia ją bez zmian. Akceptowane i zapisywane dla OBU dostawców, czytane tylko przez własny silnik volai - ElevenLabs je ignoruje. Jest wysyłany do silnika przy każdym zapisie agenta, więc zapis w portalu nie wyłącza włączonego progu. |
turnLongSpeechFloorMs | Próg ciszy w milisekundach po tym, jak dzwoniący mówił dłużej niż turnLongSpeechSecs: po długim fragmencie mowy rozmówcy biorą oddech i kontynuują, a próg daje im na to czas. 0 wyłącza; w przeciwnym razie 400 do 2300, sugerowane 800. Te same zasady przechowywania co turnDigitTailFloorMs: brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje null (domyślna wartość silnika 0), PATCH z null usuwa wartość. Tylko silnik. |
turnLongSpeechSecs | Długość mowy w sekundach dla turnLongSpeechFloorMs, od 1 do 30 (ułamki dziesiętne są dozwolone). Mierzona od pierwszej potwierdzonej mowy odpowiedzi do ostatniego jej końca, wraz z pauzami; próg obowiązuje dla mowy ŚCIŚLE dłuższej niż ta wartość. Nie ma żadnego skutku, dopóki turnLongSpeechFloorMs wynosi zero. Brakujące pole w POST nie zapisuje niczego, a odpowiedź pokazuje null (domyślna wartość silnika 5), PATCH z null usuwa wartość. |
workflow | Dzieli połączenie na fazy (węzły) - kształt, limity i przykład zob. w sekcji Fazy połączenia (workflow) poniżej. PATCH z null czyści fazy i przywraca agenta do jednej fazy, jak wcześniej. |
laughter | Czy agent może się śmiać podczas połączenia, gdy dzwoniący się śmieje lub żartuje: auto zostawia decyzję wykrywaniu tematów wrażliwych (windykacja, ochrona zdrowia, urzędy, usługi pogrzebowe), on zastępuje to wykrywanie, off zawsze je tłumi; laughterEffective pokazuje rzeczywisty wynik dla danej konfiguracji. Pominięty klucz jest zapisywany jako auto w POST, a odpowiedź zawsze zawiera laughter, nigdy brakujący klucz. Akceptowane i zapisywane dla OBU dostawców, ale słyszalne tylko dla agenta na silniku z czeskim głosem Cartesia - na ElevenLabs nie ma żadnego skutku. Zmiana obowiązuje od następnego połączenia. |
Przekazanie człowiekowi: co API faktycznie robi
Na silniku volai przełączenie odbywa się mostem: agent wybiera numer celu jako drugiego uczestnika i milknie po odebraniu. Na standardowej platformie przełączenie nie działa. transferTo wywołuje próbę przełączenia na silnik, ale wstrzymana próba, niedostępny silnik lub niepowodzenie mogą zostawić agenta na ElevenLabs; należy zweryfikować provider. Na żadnej z platform nie możemy przekazać podsumowania rozmowy, więc nie ma "przełączenia z konsultacją".
Druga noga jest zwykłym połączeniem wychodzącym i jest tak naliczana: 0.92 CZK/min (~EUR 0.04) bez dopłaty za agenta. Na liście połączeń można ją rozpoznać po kind: "transfer".
Fazy połączenia (workflow)
Opcjonalne pole workflow dzieli połączenie na fazy (nodes). Węzeł to ZAKRES, a nie scenariusz: dodaje tylko uzupełnienie promptu, wiedzy i narzędzi, które obowiązuje, gdy połączenie w nim jest, oraz opcjonalną kwestię wypowiadaną przez agenta zaraz po wejściu. O tym, kiedy przechodzić między węzłami, decyduje sam model, wywołując narzędzie - nigdy nie zapisują Państwo tego jako sztywnej sekwencji. Bazowy systemPrompt agenta powinien zawierać tylko to, co jest prawdziwe w KAŻDEJ fazie (tożsamość, ton, sposób przyjmowania wiadomości); instrukcje jednej fazy należą do jej własnego węzła, w przeciwnym razie węzeł nie ma żadnego skutku. Bazowy prompt powinien zawierać także dwa zdania, bez których agent najczęściej odpowiada na to, czego nie wie: "Nie podawaj ani nie zgaduj szczegółów operacyjnych, których nie masz w instrukcjach (godziny otwarcia, terminy, ceny, kto jest dostępny) - powiedz, że potwierdzi to kolega lub koleżanka." oraz "Zawsze pytaj tylko o jedną rzecz naraz."
entry to id węzła, w którym zaczyna się połączenie. edges to przejścia: condition (od 5 do 200 znaków) opisuje, KIEDY przejść, i musi być napisane o tym, co powiedział DZWONIĄCY (nigdy o tym, co zrobił agent lub narzędzie) - trafia dosłownie do opisu narzędzia, które model wywołuje, aby przejść. Limity: od 2 do 8 węzłów, od 1 do 24 krawędzi, najwyżej 5 krawędzi wychodzących z jednego węzła, tylko jedna krawędź między dowolnymi dwoma węzłami (w dowolnym kierunku, duplicate_pair); prompt węzła ma do 2000 znaków, knowledge do 4000, prompt plus knowledge razem są ograniczone do 4000 na węzeł, a toolIds to najwyżej 10 narzędzi na węzeł.
enterPhrase to kwestia wypowiadana jednorazowo przy wejściu do węzła (do 160 znaków). enterBehavior: "wait" mówi modelowi, aby po niej w tej samej turze nie mówił już nic więcej i zamiast tego czekał na dzwoniącego - zwykle dla fazy wrażliwej, jak zawiadomienie o śmierci; wymaga enterPhrase, w przeciwnym razie wait_without_phrase. Węzeł bez własnego enterPhrase dostaje krótką domyślną kwestię przy wejściu (po angielsku "I see.") tylko na własnym silniku volai (provider: "engine"); u agenta hostowanego na ElevenLabs przy wejściu nic nie jest odtwarzane - kwestia specyficzna dla fazy brzmi naturalniej i jest jedyną, która odtwarza się na obu, dlatego edytor stale ją zaleca przez ostrzeżenie missing_enter_phrase.
Nie ma węzła końcowego - fazy nigdy same nie kończą połączenia. Połączenie nadal kończy się wyłącznie przez wbudowane zachowanie kończące, dokładnie jak wcześniej.
enterBehavior: "wait" działa także u agentów na ElevenLabs, nie tylko na własnym silniku głosowym volai (provider: "engine"); kwestia wejściowa jest na silniku wypowiadana dosłownie, natomiast na ElevenLabs trafia do promptu fazy jako polecenie jej wypowiedzenia - model zwykle mówi coś zbliżonego, ale niekoniecznie dokładnie te słowa. Węzeł BEZ własnego enterPhrase nie mówi na ElevenLabs przy wejściu nic: krótką domyślną kwestię (po angielsku "I see.") dodaje tylko własny silnik volai. Dwie różnice są większe. Narzędzie przypisane WYŁĄCZNIE do węzła (node.toolIds) nie przenosi się jeszcze na agenta na ElevenLabs - faza działa tam bez niego, a odpowiedź sygnalizuje to kodem workflow_node_tool_not_synced; jeśli ma tam być, należy dodać je także do narzędzi agenta (pole toolIds, karta Zaawansowane w portalu). Na własnym silniku volai narzędzie węzła przenosi się bez zmian. A workflowPath w połączeniu zapisuje wyłącznie post-call silnika, więc połączenie agenta hostowanego na ElevenLabs nigdy go nie zawiera, nawet jeśli przeszło przez fazy.
Nieprawidłowy graf (nieosiągalny węzeł, brak węzła wejściowego, zarezerwowana nazwa narzędzia...) zwraca invalid_workflow i nic nie jest zapisywane; konkretny powód zawsze znajduje się w message.
Przykład - recepcjonista zakładu pogrzebowego
{
"workflow": {
"version": 1,
"entry": "triage",
"nodes": [
{
"id": "triage",
"label": "Triage",
"prompt": "Find out in one question why the caller is calling. You help callers report a death, arrange a grave dig or urn interment, and request headstone work."
},
{
"id": "bereavement",
"label": "Bereavement",
"prompt": "Find out the deceased's name, where the death occurred, whether the body is at a hospital or at home, a contact and a good time to call back. Promise a funeral home staff member will call within 15 minutes. Do not handle questions about a headstone or its price here - just say a colleague will go over that with the family, explain nothing, and return to the bereavement. Never bring up a headstone yourself.",
"enterPhrase": "My condolences.",
"enterBehavior": "wait"
},
{
"id": "burial",
"label": "Burial and interment",
"prompt": "The caller needs a grave dug, an urn interred, or a tomb opened. Find out the date, cemetery and grave number, a contact, and whether a clergy member needs to be arranged."
},
{
"id": "inquiry",
"label": "Work inquiry",
"prompt": "The caller wants new headstone work - an inscription, a repair or a grave restoration. Find out the cemetery, grave number, scope of work and a callback contact for a price."
}
],
"edges": [
{
"id": "triage_bereavement",
"from": "triage",
"to": "bereavement",
"condition": "The caller is reporting the death of a loved one or needs funeral services."
},
{
"id": "triage_burial",
"from": "triage",
"to": "burial",
"condition": "The caller needs a grave dug, an urn interred, or a tomb opened."
},
{
"id": "triage_inquiry",
"from": "triage",
"to": "inquiry",
"condition": "The caller wants new work: a headstone, an inscription, a repair or a grave restoration."
},
{
"id": "burial_bereavement",
"from": "burial",
"to": "bereavement",
"condition": "The caller reports the death of a loved one during the call."
},
{
"id": "inquiry_bereavement",
"from": "inquiry",
"to": "bereavement",
"condition": "The caller reports the death of a loved one during the call."
}
]
}
}Połączenie, które przeszło przez fazy na własnym silniku głosowym volai (provider: "engine"), zawiera także workflowPath w GET /v1/calls/{id} (i na liście GET /v1/calls): tablicę kroków {node, label, viaEdge, turnIndex, at, enterPhraseSpoken}, at jako unix ms. Krok wejściowy ma viaEdge: null. Połączenie agenta hostowanego na ElevenLabs nigdy nie zawiera tego pola, nawet jeśli przeszło przez fazy - zapisuje je wyłącznie post-call silnika. Połączenie bez faz pomija pole w całości.
Wersje robocze agenta
Wersja robocza to osobna, nieukończona migawka agenta - zapisują w niej Państwo edycję, przeglądają ją, opcjonalnie wypróbowują (symulacja poniżej), a dopiero potem wdrażają do działającego agenta jednym wywołaniem. PUT /v1/agents/{id}/draft NIGDY nie dotyka dostawcy głosu (ElevenLabs ani własnego silnika volai) ani kierowania telefonicznego - tylko zapisuje wersję roboczą. Robi to POST /v1/agents/{id}/draft/publish - to jedyny krok wersji roboczej, który faktycznie zmienia działającego agenta.
Natomiast PATCH /v1/agents/{id} (zob. sekcja Agenci powyżej) dotyka dostawcy SAMEGO, NATYCHMIAST, bez wersji roboczej i bez kroku pośredniego.
Publikacja może zwrócić dowolny błąd PATCH /v1/agents/{id} - to ten sam zapis u dostawcy.
Wersja robocza czy PATCH?
PATCH - Państwo są jedynym edytorem agenta, a zmiana ma wejść w życie od razu. Wersja robocza - agenta jednocześnie edytuje człowiek w portalu volai (edytor w portalu działa WYŁĄCZNIE przez wersje robocze, nigdy przez PATCH), zmianę chcą Państwo przejrzeć lub wypróbować (symulacja) przed wdrożeniem albo potrzebna jest optymistyczna współbieżność (expectedRevision chroni przed nadpisaniem cudzej edycji w toku).
Co się dzieje, gdy ominą Państwo wersję roboczą
Blokada agenta jest WSPÓLNA dla PATCH /v1/agents/{id} i POST .../draft/publish - PATCH w trakcie trwającej publikacji zwraca 409 in_progress. Ale PATCH poza trwającą publikacją przechodzi bez problemu: zwiększa revision o 1, nadpisuje published nową działającą wartością, czyści znacznik tested (wcześniejsza symulacja nie dotyczy już nowej konfiguracji) i pozostawia draft nietknięty. Wersja robocza w toku (zwykle człowiek w portalu) trafia wtedy przy następnym zapisie na 409 revision_conflict - to ZAMIERZONE, a nie błąd: kolidujący PATCH nie może zostać po cichu zgubiony.
Edytor agenta w portalu (/agent/{id}) działa WYŁĄCZNIE przez wersje robocze - nawet edycja jednego pola wywołuje wewnętrznie save_agent_draft + publish_agent_draft.
Pola migawki wersji roboczej
draft i published mają w każdej odpowiedzi TEN SAM kształt - AgentDraftSnapshot, kompletną migawkę konfiguracji. Treść PUT/publish jest .strict() - w przeciwieństwie do PATCH /v1/agents, który po cichu odrzuca nieznany klucz, tutaj nieznany klucz zwraca 400 validation. Pole workflow jest przy zapisie wersji roboczej sprawdzane tylko pod kątem kształtu JSON - reguły grafu (osiągalne fazy, znane narzędzia, limity na fazę) są sprawdzane przy publikacji i w PATCH /v1/agents/{id}, więc wersja robocza zapisuje się nawet z grafem, który publikacja odrzuci z invalid_workflow. Niektóre pola są wyjątkami od zasady "za każdym razem kompletna migawka": brak silencePromptSecs w migawce oznacza bez zmian, a nie wyłączone ani nie domyślne 10 - u agenta ElevenLabs niesie zapisaną wartość, słyszalną dopiero po przejściu agenta na silnik volai. Brak firstMessageProtectSecs i silencePromptText również oznacza bez zmian, a nie wyłączone ani nie tekst domyślny (null wyłącza ochronę pierwszej kwestii albo przywraca domyślny tekst przypomnienia). Brak inboundVariables (domyślnych wartości zmiennych dla połączeń przychodzących) również oznacza bez zmian - null lub {} usuwa zapisane wartości, obiekt zastępuje je w całości. Brak recordingNotice (informacji o nagrywaniu na końcu pierwszej kwestii) również oznacza bez zmian, a nie wyłączone - true/false ją ustawia. Brak sttLanguageHintsStrict (ścisłej blokady języka dla rozpoznawania mowy) również oznacza bez zmian - null wyłącza blokadę (usuwa zapisany klucz), true/false ją ustawia. Brak turnDigitTailFloorMs, turnLongSpeechFloorMs i turnLongSpeechSecs (progów ciszy na końcu wypowiedzi dzwoniącego) również oznacza bez zmian - null usuwa zapisaną wartość (silnik wraca do swoich domyślnych 0 / 0 / 5), liczba ją ustawia. To samo dotyczy braku numberE164 - oznacza on również bez zmian, więc publikacja pozostawia numer działającego agenta takim, jaki jest; tylko null go odłącza, a ciąg podłącza ten numer, dokładnie to samo trójstanowe znaczenie, jakie ma już PATCH /v1/agents/{id}. Ciąg zgodny z AKTUALNYM numerem działającego agenta w tej chwili (a nie z zapisaną migawką published) również liczy się jako bez zmian - zachowuje się tak samo jak brak klucza. To sprawdzenie jest powtarzane przy każdym odczycie wersji roboczej względem numeru aktualnie działającego, więc nie jest decyzją trwałą: jeśli numer później przejdzie do innego agenta poza tą wersją roboczą, ta sama zapisana wartość staje się znów jawnym wyborem, a następna publikacja lub cofnięcie podłączy go tutaj ponownie - nie po cichu, lecz oznaczone ostrzeżeniem number_changed_by_publish w warnings.
| Pole | Uwaga |
|---|---|
name, language, systemPrompt, provider | name, language, systemPrompt i provider są WYMAGANE - wersja robocza to kompletna migawka, a nie częściowa łatka. |
provider | provider ("engine" lub "elevenlabs") musi dokładnie odpowiadać aktualnemu provider agenta, w przeciwnym razie otrzymują Państwo invalid_draft - w POST/PATCH /v1/agents w ogóle się go nie wysyła (serwer wyprowadza go sam). To jedyne pole, które wersja robocza niesie ponad zwykłego agenta. |
toolIds, dataFields | toolIds i dataFields są ograniczone do 10 pozycji każde - tak samo jak w POST /v1/agents i PATCH /v1/agents/{id} (zob. tabela Nowe pola agenta powyżej). |
Pozostałe pola (firstMessage, voiceId, transferTo, transferCondition, recordCalls, recordingNotice, goal, knowledge, notifyEmail) mają to samo znaczenie i limity co w POST/PATCH /v1/agents - zob. tabela Nowe pola agenta powyżej.
operation: co aktualnie blokuje agenta
operation (zarówno w GET .../draft, jak i w GET .../draft/operation) ma wartość null, gdy nic nie blokuje agenta, w przeciwnym razie jest obiektem { id, kind, status, expectedRevision, createdAt, updatedAt }. Wartości kind i status są zawężone do tego, co potrzebne do decyzji, co robić - wewnętrzne szczegóły (kto dokładnie wywołał operację, dokładny tekst błędu dostawcy) nigdy nie opuszczają serwera.
| kind | Uwaga |
|---|---|
update | Edycja na żywo przez PATCH /v1/agents/{id} (poza wersją roboczą). |
publish | Publikacja zapisanej wersji roboczej (POST .../draft/publish). |
rollback | Cofnięcie do wcześniejszej opublikowanej rewizji z historii (POST .../draft/rollback) - podobnie jak publish dotyka dostawcy i telefonii. |
outbound_task | Zbiorcze zadanie wychodzące (kampania wychodząca) - inne zadanie zbiorcze aktualnie korzysta z tego agenta; publish/PATCH poczeka na jego zakończenie. |
| status | Uwaga |
|---|---|
running | Operacja jest właśnie wykonywana u dostawcy. |
uncertain | Dostawca mógł zastosować zmianę albo nie - należy sprawdzić działającego agenta i NIE ponawiać operacji ślepo (zob. POST .../draft/operation poniżej). |
drift | Działająca konfiguracja u dostawcy różni się od tej, którą uważamy, że wysłaliśmy - wymaga ręcznej decyzji. |
failed | Operacja na pewno się nie powiodła - wersja robocza jest bez zmian, można śmiało ponowić. |
done | Operacja zakończyła się pomyślnie (publikacja, potwierdzone cofnięcie albo bezpiecznie rozstrzygnięta niepewność). |
tested (zarówno w GET .../draft, jak i w odpowiedzi POST .../simulate) ma postać { revision, testedAt } | null - kiedy DOKŁADNIE ta rewizja wersji roboczej była ostatnio sprawdzona symulacją. Każda dalsza edycja wersji roboczej (PUT) czyści znacznik; symulacja NIE jest wymogiem publikacji, to tylko opcjonalne sprawdzenie sformułowań przed wdrożeniem.
history to lista { revision, publishedAt } dla ostatnich publikacji (bez zapisanych migawek konfiguracji) - pełną historię wraz z treścią przechowuje tylko portal.
GET/v1/agents/{id}/draft
Odczytuje wersję roboczą, ostatnią opublikowaną konfigurację, rewizję (dla expectedRevision w dalszych wywołaniach), wynik ostatniej symulacji (tested) oraz informację, czy cokolwiek aktualnie blokuje agenta (operation).
Żądanie
curl https://volai.cz/v1/agents/ag_kx91fa2b/draft \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"draft": {
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"voiceId": "milena",
"numberE164": "+420601234567",
"provider": "engine"
},
"published": {
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, w czym mogę pomóc?",
"voiceId": "milena",
"numberE164": "+420601234567",
"provider": "engine"
},
"revision": 4,
"tested": {
"revision": 4,
"testedAt": 1756118920000
},
"history": [
{
"revision": 3,
"publishedAt": 1756118840000
},
{
"revision": 2,
"publishedAt": 1756112640000
}
],
"operation": null
}Stany błędów
- 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie.
PUT/v1/agents/{id}/draft
Zapisuje wersję roboczą względem expectedRevision. Treść należy wysłać jako DOKŁADNIE ten obiekt, który odczytano z GET, z jedną zastosowaną zmianą - treść jest .strict(), więc nawet drobny nieznany klucz zwraca 400. Zapis NIGDY nie zmienia działającego agenta. Przykład GET .../draft powyżej pokazuje stan PO tym PUT (rewizja 3 -> 4): dlatego ten PUT wysyła expectedRevision: 3 (rewizję, na której oparto edycję), a GET powyżej zwraca już revision: 4 (nową rewizję po zapisie).
Żądanie
curl -X PUT "https://volai.cz/v1/agents/ag_kx91fa2b/draft" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedRevision":3,"draft":{"name":"Recepcja","language":"pl","systemPrompt":"Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...","firstMessage":"Dzień dobry, kawiarnia Nula, co mogę podać?","voiceId":"milena","numberE164":"+420601234567","provider":"engine"}}'Odpowiedź
{
"draft": {
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"voiceId": "milena",
"numberE164": "+420601234567",
"provider": "engine"
},
"revision": 4
}Stany błędów
- 400
validationNieprawidłowy kształt treści - limity konkretnych pól podano wyżej. - 400
invalid_draftproviderwersji roboczej nie zgadza się zprovideragenta - wersję roboczą można edytować tylko w obrębie tego samego dostawcy. - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 409
publish_in_progressNa tym agencie trwa właśnie inna publikacja lub zadanie zbiorcze - prosimy spróbować za chwilę. - 409
operation_uncertainNiepewna operacja z poprzedniej próby blokuje tego agenta - przed ponowną próbą należy sprawdzić działającego agenta i rozstrzygnąć to przezGET/POST .../draft/operation. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie.
POST/v1/agents/{id}/draft/publish
Wdraża zapisaną wersję roboczą do działającego agenta - jedyny krok wersji roboczej, który dotyka dostawcy (ElevenLabs lub silnika volai) i kierowania telefonicznego. Przyjmuje Idempotency-Key (zob. Idempotentność powyżej) - drugie wywołanie z tym samym kluczem w ciągu 24 godzin zwraca dokładnie tę samą zapisaną odpowiedź, niezależnie od tego, czy zakończyło się sukcesem, czy wróciło jako operation_uncertain.
Błędy wdrożenia (deploy) są przekazywane BEZ ZMIAN - publikacja dziedziczy CAŁĄ tabelę błędów PATCH /v1/agents/{id} powyżej (in_progress, engine_unavailable, engine_not_configured, engine_readback_mismatch i kody walidacji), oprócz własnej tabeli błędów wersji roboczej powyżej.
Żądanie
curl -X POST "https://volai.cz/v1/agents/ag_kx91fa2b/draft/publish" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: REPLACE_WITH_UNIQUE_POST_V1_AGENTS_AG_KX91FA2B_DRAFT_PUBLISH_ID" \
-d '{"expectedRevision":4}'Przed uruchomieniem proszę zastąpić REPLACE_WITH_UNIQUE… unikalnym ID tej akcji, na przykład UUID, i zapisać je. Przy ponowieniu tego samego żądania po przekroczeniu czasu proszę użyć tego samego ID. Dla innej akcji lub zmienionej treści proszę użyć nowego ID, nawet jeśli wywołują Państwo inny endpoint.
Odpowiedź
{
"revision": 5,
"published": {
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"voiceId": "milena",
"numberE164": "+420601234567",
"provider": "engine"
}
}Stany błędów
- 400
validationNieprawidłowy kształt treści - limity konkretnych pól podano wyżej. - 400
invalid_draftproviderwersji roboczej nie zgadza się zprovideragenta - wersję roboczą można edytować tylko w obrębie tego samego dostawcy. - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 409
publish_in_progressNa tym agencie trwa właśnie inna publikacja lub zadanie zbiorcze - prosimy spróbować za chwilę. - 409
operation_uncertainNiepewna operacja z poprzedniej próby blokuje tego agenta - przed ponowną próbą należy sprawdzić działającego agenta i rozstrzygnąć to przezGET/POST .../draft/operation. - 409
idempotency_in_progressRównoległe wywołanie z tym samymIdempotency-Keywciąż trwa - prosimy spróbować za chwilę. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie. - 503
engine_not_configuredDla agenta na własnym silniku volai: silnik nie jest obecnie skonfigurowany po naszej stronie, publikacja nie została zapisana. Prosimy skontaktować się z pomocą techniczną. - 502
engine_unavailableDla agenta na własnym silniku volai: silnik teraz nie odpowiada, publikacja nie została zapisana. Prosimy spróbować za chwilę. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji): komunikat to mówi, publikacja na pewno nie została zapisana, ani wersja robocza, ani agent nie są blokowane jako niepewne, a publikację można powtórzyć za kilka minut. - 502
engine_readback_mismatchDla agenta na własnym silniku volai: silnik zapisał inną konfigurację niż ta, którą wysłaliśmy, publikacja nie została zapisana. Prosimy spróbować ponownie. - 409
in_progressAlbo na agencie trwa właśnie inna zmiana (działający PATCH lub inna publikacja), albo poprzednia zmiana doszła do skutku, ale nie dało się jej bezpiecznie potwierdzić - przed ponowieniem publikacji należy odczytaćGET /v1/agents/{id}/draft/operationi zdecydować na podstawieoperation.
POST/v1/agents/{id}/draft/rollback
Cofa agenta do wcześniejszej OPUBLIKOWANEJ rewizji z historii (targetRevision, zob. pole history w GET .../draft) i od razu wdraża ją u dostawcy - to publikacja, a nie tylko nadpisana wersja robocza. expectedRevision chroni przed równoległą edycją tak samo jak PUT .../draft. W przeciwieństwie do .../draft/publish BEZ Idempotency-Key - powtórzenie tej samej treści jest samo w sobie bezpieczne.
Dzieli dokładnie te same dwa źródła błędów co publikacja powyżej: własną tabelę błędów wersji roboczej (validation, invalid_draft, forbidden, agent_not_found, revision_conflict, publish_in_progress, operation_uncertain, storage_failed) oraz CAŁĄ tabelę błędów PATCH /v1/agents/{id} (wdrożenie dotyka dostawcy tak samo jak PATCH/publish).
Żądanie
curl -X POST "https://volai.cz/v1/agents/ag_kx91fa2b/draft/rollback" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedRevision":4,"targetRevision":2}'Odpowiedź
{
"revision": 5,
"published": {
"name": "Recepcja",
"language": "pl",
"systemPrompt": "Jesteś recepcjonistą kawiarni Nula. Przyjmujesz zamówienia na odbiór osobisty...",
"firstMessage": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"voiceId": "milena",
"numberE164": "+420601234567",
"provider": "engine"
}
}Stany błędów
- 400
validationNieprawidłowy kształt treści - limity konkretnych pól podano wyżej. - 400
invalid_drafttargetRevision nie znajduje się już wśród zachowanych rewizji historycznych (historywGET .../draftjej nie zawiera). - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 409
publish_in_progressNa tym agencie trwa właśnie inna publikacja lub zadanie zbiorcze - prosimy spróbować za chwilę. - 409
operation_uncertainNiepewna operacja z poprzedniej próby blokuje tego agenta - przed ponowną próbą należy sprawdzić działającego agenta i rozstrzygnąć to przezGET/POST .../draft/operation. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie. - 503
engine_not_configuredDla agenta na własnym silniku volai: silnik nie jest obecnie skonfigurowany po naszej stronie, publikacja nie została zapisana. Prosimy skontaktować się z pomocą techniczną. - 502
engine_unavailableDla agenta na własnym silniku volai: silnik teraz nie odpowiada, publikacja nie została zapisana. Prosimy spróbować za chwilę. Ten sam kod jest zwracany dla zapisu odrzuconego przez węzeł zapasowy silnika (silnik działa obecnie na węźle zapasowym, który nie przyjmuje konfiguracji): komunikat to mówi, publikacja na pewno nie została zapisana, ani wersja robocza, ani agent nie są blokowane jako niepewne, a publikację można powtórzyć za kilka minut. - 502
engine_readback_mismatchDla agenta na własnym silniku volai: silnik zapisał inną konfigurację niż ta, którą wysłaliśmy, publikacja nie została zapisana. Prosimy spróbować ponownie. - 409
in_progressAlbo na agencie trwa właśnie inna zmiana (działający PATCH lub inna publikacja), albo poprzednia zmiana doszła do skutku, ale nie dało się jej bezpiecznie potwierdzić - przed ponowieniem publikacji należy odczytaćGET /v1/agents/{id}/draft/operationi zdecydować na podstawieoperation.
GET/v1/agents/{id}/draft/operation
Odczytuje bieżący stan blokującej operacji (null, gdy nic nie blokuje agenta) - w tym samym kształcie co operation w GET .../draft.
Żądanie
curl https://volai.cz/v1/agents/ag_kx91fa2b/draft/operation \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"operation": {
"id": "ado_4f2a91cd",
"kind": "publish",
"status": "uncertain",
"expectedRevision": 4,
"createdAt": 1756118900000,
"updatedAt": 1756118905000
}
}Stany błędów
- 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie.
POST/v1/agents/{id}/draft/operation
Po ręcznym sprawdzeniu działającego agenta bezpiecznie zamyka operację uncertain - potwierdzają Państwo, że przejrzeli stan, a serwer weryfikuje dostawcę (odczyt zwrotny) i zamyka operację.
Żądanie
curl -X POST https://volai.cz/v1/agents/ag_kx91fa2b/draft/operation \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"operationId":"ado_4f2a91cd","decision":"acknowledge_live_state"}'Odpowiedź
{
"reconciled": true,
"revision": 5
}Stany błędów
- 400
validationBrakuje operationId albo decision nie jest"acknowledge_live_state". - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 409
operation_uncertainNiepewna operacja z poprzedniej próby blokuje tego agenta - przed ponowną próbą należy sprawdzić działającego agenta i rozstrzygnąć to przezGET/POST .../draft/operation. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie.
POST/v1/agents/{id}/simulate
Wysyła wiadomość tekstową do ZAPISANEJ wersji roboczej (a nie do działającego agenta) i ustawia znacznik tested. Bezpłatne (0 CZK, płaci volai), NIE jest wymogiem publikacji, ograniczone do 20 na godzinę na konto, działa na stałym modelu Anthropic (jego nazwa nigdy nie jest ujawniana), BEZ narzędzi i BEZ głosu (ASR/TTS) - to sprawdzenie sformułowań promptu, a nie test rzeczywistego połączenia.
messages (opcjonalne) to wcześniejsze tury rozmowy BEZ bieżącej wiadomości - { role: "user" | "assistant", content }. Limity: message od 1 do 4000 znaków, najwyżej 8 wiadomości historii, 2000 znaków na wiadomość historii, 8000 znaków historii łącznie.
Żądanie
curl -X POST https://volai.cz/v1/agents/ag_kx91fa2b/simulate \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedRevision":4,"message":"Dobry den"}'Odpowiedź
{
"revision": 4,
"text": "Dzień dobry, kawiarnia Nula, co mogę podać?",
"usage": {
"inputTokens": 412,
"outputTokens": 58
},
"tested": {
"revision": 4,
"testedAt": 1756118920000
}
}Stany błędów
- 400
validationNieprawidłowy kształt treści - limity konkretnych pól podano wyżej. - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 429
rate_limitedWykorzystano limit 20 symulacji na godzinę na konto. - 502
simulation_failedModel Anthropic zwrócił błąd lub nieprawidłową odpowiedź - prosimy spróbować ponownie. - 503
simulation_not_configuredSymulacja nie jest tymczasowo skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 503
simulation_unavailableNie udało się zweryfikować limitu symulacji (tymczasowa awaria Redis) - prosimy spróbować ponownie. - 409
publish_in_progressSama symulacja się powiodła, ale zapis znacznikatestedtrafił na trwającą publikację wersji roboczej - prosimy spróbować za chwilę. - 409
operation_uncertainSama symulacja się powiodła, ale zapis znacznikatestedtrafił na niepewną operację z poprzedniej próby - przed ponowną próbą należy rozstrzygnąć ją przezGET/POST .../draft/operation. - 500
storage_failedSama symulacja się powiodła, ale zapis znacznikatestednie powiódł się po naszej stronie - prosimy spróbować ponownie.
POST/v1/agents/{id}/draft/knowledge-import
Odczytuje wiedzę agenta z publicznego adresu internetowego (url) albo z tekstu pliku .txt/.md do 1 MB (text + filename) i zapisuje ją w WERSJI ROBOCZEJ (knowledge oraz lista knowledgeSources). Adres musi być publicznym http/https (adresy prywatne i wewnętrzne, w tym przekierowania do nich, są odrzucane), strona co najwyżej 2 MB i odpowiadająca w ciągu 10 s, z najwyżej 3 przekierowaniami. To, co nie mieści się w wolnym miejscu w polu wiedzy (łącznie 20 000 znaków), jest streszczane przez model Anthropic; gdy jest niedostępny, tekst jest uczciwie ucinany (method: truncated). preview: true niczego nie zapisuje. Nigdy nie zmienia działającego agenta, publikacja pozostaje jawna. Ograniczone do 10 importów na godzinę na konto.
Żądanie
curl -X POST https://volai.cz/v1/agents/ag_kx91fa2b/draft/knowledge-import \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/pricing","mode":"append","expectedRevision":4}'Odpowiedź
{
"saved": true,
"revision": 5,
"mode": "append",
"text": "### example.com/pricing\n\nHaircut 25 EUR\nColouring from 60 EUR\nOpen Mon-Fri 9:00-18:00",
"chars": 85,
"originalChars": 412,
"method": "verbatim",
"inputTrimmed": false,
"title": "Pricing",
"source": {
"url": "https://example.com/pricing",
"importedAt": 1793000000000,
"chars": 85
},
"knowledgeChars": 85
}Stany błędów
- 400
validationNieprawidłowy kształt treści - limity konkretnych pól podano wyżej. - 400
invalid_draftproviderwersji roboczej nie zgadza się zprovideragenta - wersję roboczą można edytować tylko w obrębie tego samego dostawcy. - 400
invalid_import_urlAdres nie jest prawidłowym adresemhttp/https, zawiera dane logowania lub używa niestandardowego portu. - 400
private_addressAdres (lub cel przekierowania) wskazuje na sieć wewnętrzną, loopback lub metadane chmury. - 400
invalid_knowledgeW polu wiedzy prawie nie ma już miejsca (łącznie 20 000 znaków) - należy skrócić istniejący tekst albo wysłaćmode: replace. - 403
forbiddenWspólny agent demonstracyjny volai jest tylko do odczytu. - 404
agent_not_foundAgent nie istnieje, został usunięty albo nie należy do Państwa konta. - 409
revision_conflictKtoś inny w międzyczasie zmienił wersję roboczą (lub ominął ją przez PATCH). Błąd zawieracurrentRevision- bieżący stan należy wczytać przezGET .../drafti przejrzeć różnice przed ponowieniem zapisu. - 409
publish_in_progressNa tym agencie trwa właśnie inna publikacja lub zadanie zbiorcze - prosimy spróbować za chwilę. - 409
operation_uncertainNiepewna operacja z poprzedniej próby blokuje tego agenta - przed ponowną próbą należy sprawdzić działającego agenta i rozstrzygnąć to przezGET/POST .../draft/operation. - 413
import_too_largeStrona jest większa niż 2 MB albo plik jest większy niż 1 MB. - 415
import_unsupported_typeAdres nie jest ani HTML, ani plikiem tekstowym (PDF nie jest jeszcze obsługiwany) albo plik nie kończy się na.txt/.md. - 422
import_fetch_failedNie udało się odczytać strony: DNS, połączenie, status inny niż 2xx, przekroczenie 10 s lub więcej niż 3 przekierowania. - 422
import_emptyŹródło nie zawiera użytecznego tekstu. - 429
rate_limitedNajwyżej 10 importów na godzinę na konto. - 500
storage_failedZapis lub odczyt wersji roboczej nie powiódł się po naszej stronie - prosimy spróbować ponownie.
Głosy
Katalog głosów dla voiceId w POST/PATCH /v1/agents. provider mówi, gdzie działa agent; useForOutboundTasks dokonuje początkowego wyboru. U istniejącego agenta silnika PATCH akceptuje każdy głos z katalogu Z WYJĄTKIEM głosów ElevenLabs (katty, starszy anet); katty i surowe identyfikatory ElevenLabs są prawidłowe tylko na ElevenLabs (anet pozostaje tam wyłącznie u agentów, którzy już go mają). POST ma jeden wyjątek: jawne true zastępuje rozpoznany głos niedostępny na silniku, w tym katty, domyślnym głosem silnika. Jeśli pominięcie wybiera silnik przez wartość domyślną wdrożenia, ta sama wartość zwraca 400; surowy identyfikator ElevenLabs jest dla silnika zawsze odrzucany. ?provider=engine zwraca głosy silnika, a ?provider=elevenlabs głosy ElevenLabs. Nierozpoznana wartość zwraca 400 validation i wymienia prawidłowe wartości. Ten sam katalog, z opisem każdego głosu, znajduje się na stronie Agent głosowy.
GET/v1/voices
Katalog głosów dostępnych dla voiceId agenta.
Żądanie
curl https://volai.cz/v1/voices \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"voices": [
{
"id": "milena",
"name": "Milena",
"gender": "female",
"provider": "cartesia",
"tone": "calm, warm",
"description": "The default volai voice - calm and natural, a good fit for most agents.",
"languages": [
"cs"
],
"preview": {
"studio": "https://volai.cz/hlasy/milena-studio.mp3",
"phone": "https://volai.cz/hlasy/milena-telefon.mp3"
},
"default": true
},
{
"id": "jan",
"name": "Jan",
"gender": "male",
"provider": "cartesia",
"tone": "calm, matter-of-fact",
"description": "The male counterpart to Milena - matter-of-fact, easy-to-follow delivery.",
"languages": [
"cs"
],
"preview": {
"studio": "https://volai.cz/hlasy/jan-studio.mp3",
"phone": "https://volai.cz/hlasy/jan-telefon.mp3"
},
"default": false
},
{
"id": "tereza",
"name": "Tereza",
"gender": "female",
"provider": "cartesia",
"tone": "professional, structured",
"description": "The second female voice - professional, well-structured delivery.",
"languages": [
"cs"
],
"preview": {
"studio": "https://volai.cz/hlasy/tereza-studio.mp3",
"phone": "https://volai.cz/hlasy/tereza-telefon.mp3"
},
"default": false
},
{
"id": "marek",
"name": "Marek",
"gender": "male",
"provider": "cartesia",
"tone": "calm, resonant",
"description": "The second male voice - calm, resonant delivery for longer calls.",
"languages": [
"cs"
],
"preview": {
"studio": "https://volai.cz/hlasy/marek-studio.mp3",
"phone": "https://volai.cz/hlasy/marek-telefon.mp3"
},
"default": false
},
{
"id": "katarina",
"name": "Katarina",
"gender": "female",
"provider": "cartesia",
"tone": "calm, natural",
"description": "Slovak female voice - calm, natural delivery.",
"languages": [
"sk"
],
"preview": {
"studio": "https://volai.cz/hlasy/katarina-studio.mp3",
"phone": "https://volai.cz/hlasy/katarina-telefon.mp3"
},
"default": false
},
{
"id": "peter",
"name": "Peter",
"gender": "male",
"provider": "cartesia",
"tone": "matter-of-fact, clear",
"description": "Slovak male voice - matter-of-fact, easy-to-follow delivery.",
"languages": [
"sk"
],
"preview": {
"studio": "https://volai.cz/hlasy/peter-studio.mp3",
"phone": "https://volai.cz/hlasy/peter-telefon.mp3"
},
"default": false
},
{
"id": "skylar",
"name": "Skylar",
"gender": "female",
"provider": "cartesia",
"tone": "calm, friendly",
"description": "English female voice - calm, friendly delivery.",
"languages": [
"en"
],
"preview": {
"studio": "https://volai.cz/hlasy/skylar-studio.mp3",
"phone": "https://volai.cz/hlasy/skylar-telefon.mp3"
},
"default": false
},
{
"id": "daniel",
"name": "Daniel",
"gender": "male",
"provider": "cartesia",
"tone": "matter-of-fact, confident",
"description": "English male voice - matter-of-fact, confident delivery.",
"languages": [
"en"
],
"preview": {
"studio": "https://volai.cz/hlasy/daniel-studio.mp3",
"phone": "https://volai.cz/hlasy/daniel-telefon.mp3"
},
"default": false
},
{
"id": "alina",
"name": "Alina",
"gender": "female",
"provider": "cartesia",
"tone": "calm, warm",
"description": "German female voice - calm, warm delivery.",
"languages": [
"de"
],
"preview": {
"studio": "https://volai.cz/hlasy/alina-studio.mp3",
"phone": "https://volai.cz/hlasy/alina-telefon.mp3"
},
"default": false
},
{
"id": "lukas",
"name": "Lukas",
"gender": "male",
"provider": "cartesia",
"tone": "matter-of-fact, clear",
"description": "German male voice - matter-of-fact, easy-to-follow delivery.",
"languages": [
"de"
],
"preview": {
"studio": "https://volai.cz/hlasy/lukas-studio.mp3",
"phone": "https://volai.cz/hlasy/lukas-telefon.mp3"
},
"default": false
},
{
"id": "ewa",
"name": "Ewa",
"gender": "female",
"provider": "cartesia",
"tone": "calm, natural",
"description": "Polish female voice - calm, natural delivery.",
"languages": [
"pl"
],
"preview": {
"studio": "https://volai.cz/hlasy/ewa-studio.mp3",
"phone": "https://volai.cz/hlasy/ewa-telefon.mp3"
},
"default": false
},
{
"id": "kacper",
"name": "Kacper",
"gender": "male",
"provider": "cartesia",
"tone": "matter-of-fact, confident",
"description": "Polish male voice - matter-of-fact, confident delivery.",
"languages": [
"pl"
],
"preview": {
"studio": "https://volai.cz/hlasy/kacper-studio.mp3",
"phone": "https://volai.cz/hlasy/kacper-telefon.mp3"
},
"default": false
},
{
"id": "ariel",
"name": "Ariel",
"gender": "female",
"provider": "elevenlabs",
"tone": "natural, conversational",
"description": "Dutch female voice - natural conversational delivery.",
"languages": [
"nl"
],
"preview": {
"studio": "https://volai.cz/hlasy/ariel-studio.mp3",
"phone": "https://volai.cz/hlasy/ariel-telefon.mp3"
},
"default": false
},
{
"id": "alexander",
"name": "Alexander",
"gender": "male",
"provider": "elevenlabs",
"tone": "warm, natural",
"description": "Dutch male voice - warm, natural delivery.",
"languages": [
"nl"
],
"preview": {
"studio": "https://volai.cz/hlasy/alexander-studio.mp3",
"phone": "https://volai.cz/hlasy/alexander-telefon.mp3"
},
"default": false
},
{
"id": "christine",
"name": "Christine",
"gender": "female",
"provider": "elevenlabs",
"tone": "conversational, relaxed",
"description": "Portuguese female voice - relaxed conversational delivery.",
"languages": [
"pt"
],
"preview": {
"studio": "https://volai.cz/hlasy/christine-studio.mp3",
"phone": "https://volai.cz/hlasy/christine-telefon.mp3"
},
"default": false
},
{
"id": "filipe",
"name": "Filipe",
"gender": "male",
"provider": "elevenlabs",
"tone": "natural, expressive",
"description": "Portuguese male voice - natural, expressive delivery.",
"languages": [
"pt"
],
"preview": {
"studio": "https://volai.cz/hlasy/filipe-studio.mp3",
"phone": "https://volai.cz/hlasy/filipe-telefon.mp3"
},
"default": false
},
{
"id": "mikidutza",
"name": "Mikidutza",
"gender": "female",
"provider": "elevenlabs",
"tone": "clear, friendly",
"description": "Romanian female voice - clear, friendly delivery.",
"languages": [
"ro"
],
"preview": {
"studio": "https://volai.cz/hlasy/mikidutza-studio.mp3",
"phone": "https://volai.cz/hlasy/mikidutza-telefon.mp3"
},
"default": false
},
{
"id": "oliver",
"name": "Oliver",
"gender": "male",
"provider": "elevenlabs",
"tone": "matter-of-fact, clear",
"description": "Romanian male voice - matter-of-fact, clear delivery.",
"languages": [
"ro"
],
"preview": {
"studio": "https://volai.cz/hlasy/oliver-studio.mp3",
"phone": "https://volai.cz/hlasy/oliver-telefon.mp3"
},
"default": false
},
{
"id": "isabella",
"name": "Isabella",
"gender": "female",
"provider": "elevenlabs",
"tone": "warm, calm",
"description": "Danish female voice - warm, calm delivery.",
"languages": [
"da"
],
"preview": {
"studio": "https://volai.cz/hlasy/isabella-studio.mp3",
"phone": "https://volai.cz/hlasy/isabella-telefon.mp3"
},
"default": false
},
{
"id": "soren",
"name": "Søren",
"gender": "male",
"provider": "elevenlabs",
"tone": "clear, confident",
"description": "Danish male voice - clear, confident delivery.",
"languages": [
"da"
],
"preview": {
"studio": "https://volai.cz/hlasy/soren-studio.mp3",
"phone": "https://volai.cz/hlasy/soren-telefon.mp3"
},
"default": false
},
{
"id": "yoana",
"name": "Yoana",
"gender": "female",
"provider": "elevenlabs",
"tone": "fresh, engaging",
"description": "Bulgarian female voice - fresh, engaging delivery.",
"languages": [
"bg"
],
"preview": {
"studio": "https://volai.cz/hlasy/yoana-studio.mp3",
"phone": "https://volai.cz/hlasy/yoana-telefon.mp3"
},
"default": false
},
{
"id": "peterk",
"name": "Peter K",
"gender": "male",
"provider": "elevenlabs",
"tone": "warm, smooth",
"description": "Bulgarian male voice - warm, smooth delivery.",
"languages": [
"bg"
],
"preview": {
"studio": "https://volai.cz/hlasy/peterk-studio.mp3",
"phone": "https://volai.cz/hlasy/peterk-telefon.mp3"
},
"default": false
},
{
"id": "soili",
"name": "Soili",
"gender": "female",
"provider": "elevenlabs",
"tone": "neutral, light",
"description": "Finnish female voice - neutral, light delivery.",
"languages": [
"fi"
],
"preview": {
"studio": "https://volai.cz/hlasy/soili-studio.mp3",
"phone": "https://volai.cz/hlasy/soili-telefon.mp3"
},
"default": false
},
{
"id": "jussi",
"name": "Jussi",
"gender": "male",
"provider": "elevenlabs",
"tone": "strong, lively",
"description": "Finnish male voice - strong, lively delivery.",
"languages": [
"fi"
],
"preview": {
"studio": "https://volai.cz/hlasy/jussi-studio.mp3",
"phone": "https://volai.cz/hlasy/jussi-telefon.mp3"
},
"default": false
},
{
"id": "crhysa",
"name": "Crhysa",
"gender": "female",
"provider": "elevenlabs",
"tone": "professional, lively",
"description": "Greek female voice - professional, lively delivery.",
"languages": [
"el"
],
"preview": {
"studio": "https://volai.cz/hlasy/crhysa-studio.mp3",
"phone": "https://volai.cz/hlasy/crhysa-telefon.mp3"
},
"default": false
},
{
"id": "iordanis",
"name": "Iordanis",
"gender": "male",
"provider": "elevenlabs",
"tone": "natural, conversational",
"description": "Greek male voice - natural conversational delivery.",
"languages": [
"el"
],
"preview": {
"studio": "https://volai.cz/hlasy/iordanis-studio.mp3",
"phone": "https://volai.cz/hlasy/iordanis-telefon.mp3"
},
"default": false
},
{
"id": "natasha",
"name": "Natasha",
"gender": "female",
"provider": "elevenlabs",
"tone": "soft, conversational",
"description": "French female voice - soft, conversational delivery.",
"languages": [
"fr"
],
"preview": {
"studio": "https://volai.cz/hlasy/natasha-studio.mp3",
"phone": "https://volai.cz/hlasy/natasha-telefon.mp3"
},
"default": false
},
{
"id": "max",
"name": "Max",
"gender": "male",
"provider": "elevenlabs",
"tone": "charismatic, friendly",
"description": "French male voice - charismatic, friendly delivery.",
"languages": [
"fr"
],
"preview": {
"studio": "https://volai.cz/hlasy/max-studio.mp3",
"phone": "https://volai.cz/hlasy/max-telefon.mp3"
},
"default": false
},
{
"id": "rossana",
"name": "Rossana",
"gender": "female",
"provider": "elevenlabs",
"tone": "warm, conversational",
"description": "Italian female voice - warm conversational delivery.",
"languages": [
"it"
],
"preview": {
"studio": "https://volai.cz/hlasy/rossana-studio.mp3",
"phone": "https://volai.cz/hlasy/rossana-telefon.mp3"
},
"default": false
},
{
"id": "marco",
"name": "Marco",
"gender": "male",
"provider": "elevenlabs",
"tone": "charismatic, friendly",
"description": "Italian male voice - charismatic, friendly delivery.",
"languages": [
"it"
],
"preview": {
"studio": "https://volai.cz/hlasy/marco-studio.mp3",
"phone": "https://volai.cz/hlasy/marco-telefon.mp3"
},
"default": false
},
{
"id": "zsofia",
"name": "Zsófia",
"gender": "female",
"provider": "elevenlabs",
"tone": "soft, inviting",
"description": "Hungarian female voice - soft, inviting delivery.",
"languages": [
"hu"
],
"preview": {
"studio": "https://volai.cz/hlasy/zsofia-studio.mp3",
"phone": "https://volai.cz/hlasy/zsofia-telefon.mp3"
},
"default": false
},
{
"id": "gabor",
"name": "Gábor",
"gender": "male",
"provider": "elevenlabs",
"tone": "warm, confident",
"description": "Hungarian male voice - warm, natural, confident delivery.",
"languages": [
"hu"
],
"preview": {
"studio": "https://volai.cz/hlasy/gabor-studio.mp3",
"phone": "https://volai.cz/hlasy/gabor-telefon.mp3"
},
"default": false
},
{
"id": "sanna",
"name": "Sanna",
"gender": "female",
"provider": "elevenlabs",
"tone": "direct, natural",
"description": "Swedish female voice - direct, natural delivery.",
"languages": [
"sv"
],
"preview": {
"studio": "https://volai.cz/hlasy/sanna-studio.mp3",
"phone": "https://volai.cz/hlasy/sanna-telefon.mp3"
},
"default": false
},
{
"id": "filip",
"name": "Filip",
"gender": "male",
"provider": "elevenlabs",
"tone": "energetic, expressive",
"description": "Swedish male voice - energetic, expressive delivery.",
"languages": [
"sv"
],
"preview": {
"studio": "https://volai.cz/hlasy/filip-studio.mp3",
"phone": "https://volai.cz/hlasy/filip-telefon.mp3"
},
"default": false
},
{
"id": "sarah-sl",
"name": "Sarah",
"gender": "female",
"provider": "elevenlabs",
"tone": "calm, confident",
"description": "Slovenian female voice - calm, confident delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"sl"
],
"preview": {
"studio": "https://volai.cz/hlasy/sarah-sl-studio.mp3",
"phone": "https://volai.cz/hlasy/sarah-sl-telefon.mp3"
},
"default": false
},
{
"id": "daniel-sl",
"name": "Daniel",
"gender": "male",
"provider": "elevenlabs",
"tone": "steady, clear",
"description": "Slovenian male voice - steady, broadcaster-style delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"sl"
],
"preview": {
"studio": "https://volai.cz/hlasy/daniel-sl-studio.mp3",
"phone": "https://volai.cz/hlasy/daniel-sl-telefon.mp3"
},
"default": false
},
{
"id": "alice-et",
"name": "Alice",
"gender": "female",
"provider": "elevenlabs",
"tone": "clear, engaging",
"description": "Estonian female voice - clear, engaging delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"et"
],
"preview": {
"studio": "https://volai.cz/hlasy/alice-et-studio.mp3",
"phone": "https://volai.cz/hlasy/alice-et-telefon.mp3"
},
"default": false
},
{
"id": "chris-et",
"name": "Chris",
"gender": "male",
"provider": "elevenlabs",
"tone": "down-to-earth, charming",
"description": "Estonian male voice - down-to-earth, charming delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"et"
],
"preview": {
"studio": "https://volai.cz/hlasy/chris-et-studio.mp3",
"phone": "https://volai.cz/hlasy/chris-et-telefon.mp3"
},
"default": false
},
{
"id": "matilda-lv",
"name": "Matilda",
"gender": "female",
"provider": "elevenlabs",
"tone": "professional, matter-of-fact",
"description": "Latvian female voice - knowledgeable, professional delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"lv"
],
"preview": {
"studio": "https://volai.cz/hlasy/matilda-lv-studio.mp3",
"phone": "https://volai.cz/hlasy/matilda-lv-telefon.mp3"
},
"default": false
},
{
"id": "george-lv",
"name": "George",
"gender": "male",
"provider": "elevenlabs",
"tone": "warm, captivating",
"description": "Latvian male voice - warm, captivating delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"lv"
],
"preview": {
"studio": "https://volai.cz/hlasy/george-lv-studio.mp3",
"phone": "https://volai.cz/hlasy/george-lv-telefon.mp3"
},
"default": false
},
{
"id": "sarah-lt",
"name": "Sarah",
"gender": "female",
"provider": "elevenlabs",
"tone": "calm, confident",
"description": "Lithuanian female voice - calm, confident delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"lt"
],
"preview": {
"studio": "https://volai.cz/hlasy/sarah-lt-studio.mp3",
"phone": "https://volai.cz/hlasy/sarah-lt-telefon.mp3"
},
"default": false
},
{
"id": "daniel-lt",
"name": "Daniel",
"gender": "male",
"provider": "elevenlabs",
"tone": "steady, clear",
"description": "Lithuanian male voice - steady, broadcaster-style delivery (multilingual voice, the model drives the pronunciation).",
"languages": [
"lt"
],
"preview": {
"studio": "https://volai.cz/hlasy/daniel-lt-studio.mp3",
"phone": "https://volai.cz/hlasy/daniel-lt-telefon.mp3"
},
"default": false
}
]
}Stany błędów
- 400
validationprovider nie jest jedną z wartości: elevenlabs, engine.
Pole tone opisuje charakter głosu (spokojny, rzeczowy); languages zawiera kody języków, w których głos mówi - cs: Milena, Jan, Tereza, Marek; sk: Katarina, Peter; en: Skylar, Daniel; de: Alina, Lukas; pl: Ewa, Kacper; nl: Ariel, Alexander; pt: Christine, Filipe; sl: Sarah, Daniel; ro: Mikidutza, Oliver; da: Isabella, Søren; et: Alice, Chris; lv: Matilda, George; bg: Yoana, Peter K; fi: Soili, Jussi; el: Crhysa, Iordanis; fr: Natasha, Max; it: Rossana, Marco; hu: Zsófia, Gábor; lt: Sarah, Daniel; sv: Sanna, Filip; preview podaje adresy URL próbek podglądu studyjnego i telefonicznego; default oznacza rekomendowaną pozycję katalogu - w portalu niesie tylko plakietkę "Domyślny" na karcie i samo niczego nie wybiera z góry. Nowy agent utworzony bez voiceId się na niego nie przełącza - dziedziczy domyślny głos swojego dostawcy (Milena na silniku, głos z szablonu na ElevenLabs; zob. sekcja Agenci powyżej).
Narzędzia agenta
Narzędzie to adres Państwa API, który agent wywołuje w trakcie rozmowy (zweryfikować zamówienie, zapisać rezerwację) i wykorzystuje odpowiedź w mowie. Narzędzie należy do konta i jest przypisywane agentowi przez toolIds - samo utworzenie narzędzia nie zmienia żadnego agenta.
Limity: najwyżej 10 narzędzi na konto, 10 parametrów i 5 nagłówków na narzędzie, timeoutSecs od 5 do 30 (domyślnie 20). Adres musi być https i publiczny - odrzucamy sieci wewnętrzne i loopback (private_address), ponieważ w przeciwnym razie nasz serwer mógłby posłużyć do badania cudzej infrastruktury.
Bramka potwierdzenia (requireConfirmation, domyślnie false): dla narzędzia, które coś wysyła lub tworzy (SMS, zgłoszenie), należy wysłać requireConfirmation: true w POST /v1/tools lub PATCH /v1/tools/{id}. Silnik głosowy volai uruchamia wtedy narzędzie tylko wtedy, gdy ostatnia odpowiedź dzwoniącego wyraźnie brzmi tak; w przeciwnym razie narzędzie się nie uruchamia, agent dostaje polecenie ponownego zapytania, a próba zakończenia połączenia w tej samej rundzie jest wstrzymywana. Dotyczy tylko agenta na silniku volai, którego językiem jest cs lub sk (silnik zna słowa zgody tylko po czesku i słowacku; dla en, de i pl zablokowałby nawet wyraźne "Tak"); dla każdego innego agenta pole jest ignorowane, a mimo to zapis narzędzia nigdy nie jest odrzucany z powodu języka, ponieważ narzędzie jest wspólne dla kilku agentów. ElevenLabs nie używa tego pola. Bramka czyta tylko ostatnią odpowiedź dzwoniącego i nie zna pytania agenta: wychwytuje niejasną zgodę ("Halo?", "Tak?", "Tak, ale później"), a nie każde "Tak" powiedziane w odpowiedzi na inne pytanie. Zmiana nie obowiązuje od razu u agenta już działającego na silniku: narzędzie jest tam przepisywane dopiero przy następnym zapisie lub publikacji tego agenta (POST /v1/agents/{id}/draft/publish, Publikuj w portalu lub PATCH /v1/agents/{id}) - edycja narzędzia nigdy sama nie przepisuje agentów. W trwającej kampanii liczy się sposób: ponowna publikacja przetestowanej wersji roboczej bez zmian zachowuje znacznik przetestowania, więc zadanie działa dalej, teraz z bramką, której test nie obejmował; bezpośrednie PATCH /v1/agents/{id} czyści znacznik przetestowania i zatrzymuje zadanie z agent_version_changed. Włączenie bramki należy planować poza trwającą kampanią i potem ponownie przetestować kampanię.
GET/v1/tools
Narzędzia webhookowe konta. Wartości nagłówków są zawsze maskowane.
Żądanie
curl https://volai.cz/v1/tools \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"tools": [
{
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Weryfikacja zamówienia",
"description": "Wyszukuje status zamówienia po numerze. Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "Numer zamówienia podyktowany przez dzwoniącego.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 20,
"requireConfirmation": false,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
]
}POST/v1/tools
Tworzy narzędzie. label to czytelna nazwa; nazwa widoczna dla modelu jest z niej automatycznie wyprowadzana (Weryfikacja zamówienia -> verify_order) i zwracana w polu name. description to jedyna rzecz, na której model opiera decyzję, KIEDY użyć narzędzia - należy ją napisać jako instrukcję ("Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie."), od 10 do 1000 znaków.
Żądanie
curl -X POST https://volai.cz/v1/tools \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Weryfikacja zamówienia",
"description": "Wyszukuje status zamówienia po numerze. Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bearer your_key" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "Numer zamówienia podyktowany przez dzwoniącego.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 20
}'Odpowiedź
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Weryfikacja zamówienia",
"description": "Wyszukuje status zamówienia po numerze. Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "Numer zamówienia podyktowany przez dzwoniącego.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 20,
"requireConfirmation": false,
"createdAt": 1756111000000,
"updatedAt": 1756111000000
}
}Stany błędów
- 400
tool_limitKonto ma już 10 narzędzi. - 400
invalid_tool_namelabel musi mieć od 1 do 60 znaków albo wyprowadzona z niego nazwa byłaby zarezerwowana (end_call,transfer_to_human,switch_to_*). - 400
invalid_tool_urlurl nie jest prawidłowym adresem albo nie jest https. - 400
private_addressurl wskazuje na sieć wewnętrzną lub loopback. - 400
invalid_tool_paramsParametr ma nieprawidłową nazwę, typ, źródło albo jest ich więcej niż 10. - 400
invalid_tool_headersNagłówek jest zabroniony (host, content-length, x-conversation-id, x-caller-id), zduplikowany, pusty albo jest ich więcej niż 5. - 400
validationdescription jest poza zakresem od 10 do 1000 znaków, timeoutSecs jest poza zakresem od 5 do 30, requireConfirmation nie jest wartością logiczną albo method jest inną wartością niż GET/POST. - 502
eleven_labs_errorNie udało się utworzyć narzędzia na platformie głosowej - lokalnie niczego nie zapisaliśmy, prosimy spróbować ponownie.
GET/v1/tools/{id}
Szczegóły pojedynczego narzędzia - te same pola co na liście, plus agents: agenci, którzy mają je włączone przez toolIds (tylko id i name; pełnego agenta pobiera się przez GET /v1/agents/{id}).
Żądanie
curl https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Weryfikacja zamówienia",
"description": "Wyszukuje status zamówienia po numerze. Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "Numer zamówienia podyktowany przez dzwoniącego.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 20,
"requireConfirmation": false,
"createdAt": 1756111000000,
"updatedAt": 1756111000000,
"agents": [{ "id": "ag_kx91fa2b", "name": "Recepcja" }]
}
}Stany błędów
- 404
tool_not_foundNarzędzie nie istnieje albo nie należy do Państwa konta.
PATCH/v1/tools/{id}
Aktualizuje narzędzie - wszystkie pola opcjonalne, zmieniane są tylko te wysłane. Dwie rzeczy wymagają uwagi: params i headers zastępują w całości istniejącą listę, a zmiana label przepisuje też nazwę, pod którą model zna narzędzie - jeśli odwołują się Państwo do niej w systemPrompt, należy zaktualizować to jednocześnie.
Nagłówki wracają zawsze tylko zamaskowane ("Bear***"). Jeśli odeślą Państwo dokładnie tę zamaskowaną wartość, traktujemy ją jako "zachowaj oryginał" - dzięki temu zwykły przepływ "wczytaj narzędzie, zmień jedno pole, odeślij cały obiekt" nie zniszczy Państwa danych uwierzytelniających. Aby faktycznie je zmienić, należy wysłać prawdziwą wartość w całości.
Żądanie
curl -X PATCH https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"timeoutSecs": 10}'Odpowiedź
{
"tool": {
"id": "tl_5c2a91f4",
"name": "verify_order",
"label": "Weryfikacja zamówienia",
"description": "Wyszukuje status zamówienia po numerze. Użyj, gdy dzwoniący pyta, gdzie jest jego zamówienie.",
"url": "https://api.yourapp.com/orders",
"method": "POST",
"headers": { "Authorization": "Bear***" },
"params": [
{
"name": "order_number",
"type": "string",
"source": "llm",
"description": "Numer zamówienia podyktowany przez dzwoniącego.",
"required": true
},
{ "name": "phone", "type": "string", "source": "caller_number" },
{ "name": "source", "type": "string", "source": "constant", "constantValue": "phone" }
],
"timeoutSecs": 10,
"requireConfirmation": false,
"createdAt": 1756111000000,
"updatedAt": 1756111890000
}
}Stany błędów
- 400
invalid_tool_urlurl nie jest prawidłowym adresem albo nie jest https. - 400
private_addressurl wskazuje na sieć wewnętrzną lub loopback. - 400
invalid_tool_paramsParametr ma nieprawidłową nazwę, typ lub źródło. - 400
invalid_tool_headersNagłówek jest zabroniony, zduplikowany lub pusty. - 400
validationdescription (jeśli wysłane) jest poza zakresem od 10 do 1000 znaków, timeoutSecs jest poza zakresem od 5 do 30, requireConfirmation nie jest wartością logiczną albo method jest inną wartością niż GET/POST. - 404
tool_not_foundNarzędzie nie istnieje albo nie należy do Państwa konta. - 502
eleven_labs_mismatchZmiana nie została zapisana na platformie głosowej dokładnie tak, jak podano - lokalnie niczego nie nadpisaliśmy.
DELETE/v1/tools/{id}
Usuwa narzędzie i zwraca agentów, dla których przestaje działać (tylko id i name; pełnego agenta pobiera się przez GET /v1/agents/{id}). W przeciwieństwie do zwolnienia numeru jest to strata możliwa do odzyskania - to samo narzędzie można utworzyć ponownie, tylko z nowym id, które trzeba następnie ponownie przypisać agentom.
Żądanie
curl -X DELETE https://volai.cz/v1/tools/tl_5c2a91f4 \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"deleted": true,
"agents": [
{ "id": "ag_kx91fa2b", "name": "Recepcja" }
]
}Stany błędów
- 404
tool_not_foundNarzędzie nie istnieje albo nie należy do Państwa konta. - 502
eleven_labs_errorNie udało się usunąć narzędzia z platformy głosowej - prosimy spróbować ponownie.
POST/v1/tools/test
Wysyła PRAWDZIWE żądanie według przesłanej definicji i zwraca to, co wróciło - narzędzie NIE jest zapisywane (w przeciwieństwie do POST /v1/tools, label/description są tu opcjonalne). Przydatne do wypróbowania adresu przed zapisaniem go jako narzędzia agenta. timeoutSecs jest tu tylko walidowane (od 5 do 30) - test zawsze czeka stałe 10 sekund, abyście nie czekali na pełny limit czasu środowiska uruchomieniowego; to wartość, której narzędzie użyłoby po zapisaniu w czasie działania. Dzieli limit zapytań z POST /v1/tools/{id}/test poniżej - łącznie 10 prób na minutę na konto.
Żądanie
curl -X POST https://volai.cz/v1/tools/test \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://api.yourapp.com/orders", "method": "GET", "params": [{"name": "order", "type": "string", "source": "llm", "description": "Order number."}]}'Odpowiedź
{
"status": 200,
"durationMs": 214,
"body": "{\"status\":\"shipped\"}",
"note": null
}Stany błędów
- 400
validationmethod inna niż GET/POST albo timeoutSecs poza zakresem od 5 do 30. - 400
invalid_tool_urlurl nie jest prawidłowym adresem albo podczas wysyłania żądania wystąpił błąd sieci/DNS/TLS (w tej fazie wszystko - w tym przekroczenie czasu - zbiega się w ten sam kod). - 400
private_addressurl wskazuje na sieć wewnętrzną lub loopback (ochrona SSRF) - sprawdzane PRZED wysłaniem. - 400
invalid_tool_paramsParametr ma nieprawidłową nazwę, typ, źródło albo jest ich więcej niż 10. - 400
invalid_tool_headersNagłówek jest zabroniony, zduplikowany, pusty albo jest ich więcej niż 5. - 429
rate_limitedWykorzystano 10 prób na minutę (wspólnie zPOST /v1/tools/{id}/test) - prosimy spróbować za chwilę.
POST/v1/tools/{id}/test
Testuje JUŻ ZAPISANE narzędzie po id - treść jest zawsze pustym {}, endpoint bierze adres, metodę i PRAWDZIWE (niezamaskowane) nagłówki z zapisanego rekordu, a nie z treści żądania. private_address/invalid_tool_params/invalid_tool_headers nie mogą tu wrócić (zapisany rekord nie przechodzi ponownie walidacji danych wejściowych) - błąd sieci nadal zbiega się w invalid_tool_url.
Żądanie
curl -X POST https://volai.cz/v1/tools/tl_5c2a91f4/test \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{}'Odpowiedź
{
"status": 200,
"durationMs": 214,
"body": "{\"status\":\"shipped\"}",
"note": null
}Stany błędów
- 400
validationTreść żądania nie jest pusta - należy wysłać{}lub w ogóle żadnej treści. - 400
invalid_tool_urlBłąd sieci/DNS/TLS (w tym przekroczenie czasu) podczas wysyłania prawdziwego żądania. - 404
tool_not_foundNarzędzie nie istnieje albo nie należy do Państwa konta.
Parametry narzędzia: pole source
Każdy parametr ma name, type (string, number, boolean) i source - skąd pochodzi jego wartość. Dla GET parametry trafiają do ciągu zapytania; dla POST do treści JSON.
| source | Skąd pochodzi wartość | Co jeszcze wypełnić |
|---|---|---|
llm | Model wyciąga ją z rozmowy. | description (dokładnie, co tam wpisać) i opcjonalnie required. |
caller_number | Numer dzwoniącego w formacie E.164, wypełniany automatycznie. | Nic. Model nigdy nie widzi tego pola. |
called_number | Państwa wywoływany numer w formacie E.164, wypełniany automatycznie. | Nic. Model nigdy nie widzi tego pola. |
constant | Stała wartość, którą Państwo podają. | constantValue. |
Połączenie testowe
"Niech agent do Państwa zadzwoni" - prawdziwe połączenie wychodzące przez makeAgentCall, tylko z własnym dziennym limitem 3 połączeń na konto (dla wszystkich agentów), aby połączenie testowe nie mogło służyć do obejścia zwykłego limitu połączeń.
POST/v1/agents/{id}/test-call
Oddzwania z Państwa własnego agenta - ta sama cena i te same zasady co POST /v1/calls, bez zniżki i bez specjalnej stawki. Agent na własnym silniku głosowym volai dzwoni nawet BEZ przypisanego numeru - ze wspólnej linii volai, więc odbiorca widzi numer volai, a nie Państwa. Istniejącemu agentowi można przypisać własny numer przez PATCH /v1/numbers/{e164} (kierowanie agent) lub pole numberE164 agenta (POST/PATCH /v1/agents) albo w sekcji Numery w portalu. Odpowiedź zawiera testCalls - max, remaining i resetAt (kiedy kończy się 24-godzinne okno, które zaczęło się od pierwszej próby, epoch ms) - a GET /v1/account zwraca to samo pole, więc przed połączeniem można sprawdzić, ile zostało. Liczy się każda wybrana próba, także nieodebrana, która nie jest naliczana.
Żądanie
curl -X POST https://volai.cz/v1/agents/ag_kx91fa2b/test-call \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456"}'Odpowiedź
{
"id": "c_9d4e2b7f",
"status": "initiated",
"testCalls": { "max": 3, "remaining": 2, "resetAt": 1755086400000 }
}Stany błędów
- 400
validationBrakuje to albo ma ponad 32 znaki, albo treść nie jest prawidłowym JSON. - 400
destination_not_allowedAgent używa produktu "numer do połączeń i SMS", atonie jest numerem czeskim (+420): produkt "numer do połączeń i SMS" może dzwonić tylko na numery czeskie. Należy podać czeskito. Nic nie zostało naliczone. - 404
agent_not_foundagentId nie istnieje albo nie należy do Państwa konta. - 404
agent_no_numberAgent nie ma przypisanego numeru telefonu - ten błąd jest zwracany tylko dla agenta na ElevenLabs; agent na własnym silniku volai dzwoni nawet bez numeru, ze wspólnej linii volai. - 403
agent_suspendedOperator volai ręcznie zawiesił tego agenta - połączenia testowego nie można teraz wykonać. Zawieszone konto zwraca zamiast tegoaccount_suspended. - 402
insufficient_creditSaldo nie pokrywa minimum potrzebnego do rozpoczęcia połączenia. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 503
capacity_busyWszystkie linie wychodzące są teraz zajęte, prosimy spróbować za minutę. Produkt "numer do połączeń i SMS" może prowadzić naraz tylko jedno połączenie wychodzące. - 429
rate_limitedWyczerpano dzienny limit 3 połączeń testowych na konto - zwykłe połączenie przez POST /v1/calls nie ma tego limitu.
Relay - własny agent
Jednorazowa dzierżawa nazwy SIP z puli relay, do połączenia wychodzącego od własnego agenta głosowego (ElevenLabs lub dowolnej innej platformy) - bez dopłaty za agenta (zwykle wynosi ona 2.50 CZK/min (~EUR 0.10), ale tutaj płacą Państwo za nią na własnej platformie). Pełny przewodnik konfiguracji dla ElevenLabs znajduje się na stronie Własny agent.
POST/v1/relay
Tworzy dzierżawę - to to numer docelowy, from to Państwa własny numer volai, z którego połączenie ma wyglądać na wychodzące. Zwrócone sipName to SAMA nazwa dla to_number w ElevenLabs (nie sipUri - ElevenLabs odrzuca pełny adres SIP URI). Trunk wychodzący Państwa platformy należy ustawić na outboundTrunkAddress z GET /v1/numbers/{e164}/sip - inny adres oznacza, że połączenie nie jest kierowane w sieci, a platforma zgłasza przekroczenie czasu.
ttlSecs (opcjonalne, domyślnie 120) - ile sekund dzierżawa czeka na pierwsze połączenie, zanim niewykorzystana wygaśnie. Dozwolony zakres to od 15 do 300 sekund - poza nim zwraca 400 validation.
Żądanie
curl -X POST https://volai.cz/v1/relay \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+420777123456", "from": "+420601234567"}'Odpowiedź
{
"id": "rl_4f2a91cd",
"sipName": "volai_relay_2",
"sipUri": "sip:volai_relay_2@sip.volai.cz",
"expiresAt": 1756111760000,
"callId": "c_9d4e2b7f"
}Stany błędów
- 400
invalid_numberto lub from nie jest prawidłowym numerem telefonu. - 400
validationttlSecs jest poza dozwolonym zakresem od 15 do 300 sekund. - 404
from_number_not_ownedfrom nie należy do Państwa konta. - 400
unsupported_for_number_typefromto Państwa własny produkt "numer do połączeń i SMS", a połączenia z niego może wykonywać wyłącznie agent volai na wbudowanym silniku głosowym, więc relay przez agenta zewnętrznego nie jest z niego dostępny. Do relay należy użyć numeru głosowego. Nic nie zostało naliczone. - 400
on_dncto znajduje się na Państwa liście zakazu dzwonienia. - 400
relay_lease_limitMają już Państwo aktywną dzierżawę na tym numerze albo dwie na całym koncie. - 402
insufficient_creditSaldo nie pokrywa minimum potrzebnego do rozpoczęcia połączenia. - 402
subscription_cap_reachedKonto ma abonament miesięczny z limitem wydatków, miesięczne saldo jest wyczerpane, a saldo z doładowań nie pokrywa działania. Dla takiego konta zastępujeinsufficient_credit(ten sam status HTTP). Nic nie zostało naliczone. - 402
spend_cap_reachedUstawiony przez Państwa miesięczny limit wydatków został osiągnięty. Nic nie zostało zrealizowane ani wysłane i nic nie zostało naliczone. Limit można podnieść lub usunąć przezPUTalboDELETE /v1/account/spend-caplub poczekać do następnego miesiąca kalendarzowego. - 409
destination_busyNa ten numer trwa już inne połączenie. - 503
capacity_busyPula relay jest teraz pełna, prosimy spróbować za chwilę - nie naliczamy za to opłaty.
GET/v1/relay
Lista Państwa dzierżaw relay - pending czeka na pierwsze połączenie, active oznacza, że właśnie trwa połączenie.
Żądanie
curl https://volai.cz/v1/relay \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"leases": [
{
"id": "rl_4f2a91cd",
"sipName": "volai_relay_2",
"sipUri": "sip:volai_relay_2@sip.volai.cz",
"from": "+420601234567",
"to": "+420777123456",
"callId": "c_9d4e2b7f",
"status": "active",
"createdAt": 1756111640000,
"expiresAt": 1756111760000
}
]
}DELETE/v1/relay/{id}
Zwalnia dzierżawę i zwraca slot do puli, o ile wciąż czeka na pierwsze połączenie (pending). Nie kończy trwającego połączenia - żadne API tego dziś nie potrafi (ani sieć telefoniczna, ani ElevenLabs) - taka dzierżawa (active) po prostu dobiega końca; to tylko uniemożliwia ponowne użycie tej samej dzierżawy.
Żądanie
curl -X DELETE https://volai.cz/v1/relay/rl_4f2a91cd \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"cancelled": true
}Stany błędów
- 404
lease_not_foundDzierżawa nie istnieje, wygasła albo nie należy do Państwa konta. - 409
lease_already_activeNa dzierżawie trwa już połączenie - takiej dzierżawy nie można anulować (dobiega końca sama). Dotyczy tylko DELETE /v1/relay/{id}, a nie tworzenia nowej dzierżawy.
Lista zakazu dzwonienia (DNC)
Numery, na które Państwa agent (własny lub wbudowany) nie może dzwonić - czy to przez POST /v1/calls, połączenie testowe, czy relay. DNC dotyczy tylko połączeń - wysyłanie SMS na numer z listy nie jest ograniczone.
Oprócz listy ręcznej volai automatycznie (i tymczasowo) blokuje cel samodzielnie - po 3 nieudanych próbach w ciągu 24 godzin (zajęte, nieodebrane, odrzucone) przestaje dzwonić na ten numer na 30 dni. Blokadę można zdjąć w portalu (Ustawienia) lub przez POST /v1/dnc/{e164}/unblock - w przeciwnym razie wygasa sama.
GET/v1/dnc
Lista ręczna (numbers) plus automatycznie zablokowane cele (blocked) - blockedAt i expiresAt to milisekundy Unix. Automatycznej blokady, która rozpoczęła się przed wersją 1.8.0, może nie być w blocked; gdy połączenie zwraca destination_auto_blocked, należy wywołać endpoint odblokowania bezpośrednio z tym numerem, nawet bez wiersza na liście.
Żądanie
curl https://volai.cz/v1/dnc \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"numbers": ["+420777998877"],
"blocked": [
{
"e164": "+420777123456",
"blockedAt": 1757600000000,
"expiresAt": 1760192000000
}
]
}POST/v1/dnc
Dodaje numery do listy zakazu dzwonienia. Należy wysłać dokładnie jedno z e164 (jeden numer) i e164s (lista od 1 do 500 numerów). Odpowiedź na e164 to { "added": true, "e164": "..." } i jest niezmieniona. Odpowiedź na e164s zawiera addedCount (numery, których nie było na liście, a teraz są), alreadyOnListCount (prawidłowe pozycje, które niczego nie zmieniły, bo numer był już na liście, w tym numer powtórzony w tym samym żądaniu), rejectedCount i rejected (pozycje, które nie są prawidłowym numerem telefonu, każda z index na liście, input w postaci wysłanej i reason: "invalid_number"). Te trzy liczby sumują się do liczby wysłanych pozycji. Odrzucona pozycja nie powoduje niepowodzenia żądania: prawidłowe pozycje są zawsze dodawane, więc import 500 kontaktów nie zatrzymuje się na jednym błędnym numerze. Żądanie jest idempotentne: ponowne wysłanie tej samej listy niczego nie dodaje i zwraca addedCount: 0.
Żądanie
curl -X POST https://volai.cz/v1/dnc \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"e164": "+420777998877"}'Odpowiedź
{
"added": true,
"e164": "+420777998877"
}Stany błędów
- 400
validationNie wysłano anie164, anie164s, wysłano oba,e164nie jest prawidłowym numerem telefonu,e164sjest puste lub ma więcej niż 500 pozycji, albo pozycja nie jest ciągiem znaków lub ma więcej niż 256 znaków.
Import wielu numerów naraz
Zamiast e164 należy wysłać e164s, aby dodać do 500 numerów w jednym żądaniu. API dopuszcza 60 żądań na minutę na klucz dla wszystkich wywołań łącznie, więc 5000 numerów to 10 żądań zamiast 5000. Pozycje, które nie są prawidłowymi numerami telefonu (w tym dłuższy tekst, na przykład dwa numery w jednej komórce), wracają w rejected z danymi wejściowymi ściętymi do 32 znaków; prawidłowe są mimo to dodawane. Sama odpowiedź 200 nie oznacza, że cokolwiek dodano: należy sprawdzić rejectedCount. alreadyOnListCount sprawia, że po przekroczeniu czasu można bezpiecznie powtórzyć wywołanie. Numery usuwa się pojedynczo przez DELETE /v1/dnc/{e164}.
curl -X POST https://volai.cz/v1/dnc \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"e164s": ["+420777998877", "00420777998878", "+420777998879", "777 99"]}'{
"addedCount": 2,
"alreadyOnListCount": 1,
"rejectedCount": 1,
"rejected": [
{ "index": 3, "input": "777 99", "reason": "invalid_number" }
]
}DELETE/v1/dnc/{e164}
Usuwa numer z listy zakazu dzwonienia.
Żądanie
curl -X DELETE "https://volai.cz/v1/dnc/+420777998877" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"removed": true,
"e164": "+420777998877"
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu.
POST/v1/dnc/{e164}/unblock
Zdejmuje automatyczną blokadę celu, jeśli istnieje, w tym blokadę sprzed wersji 1.8.0, której nie ma na liście blocked. unblocked wraca jako false, a nie 404, gdy żadnej nie było.
Żądanie
curl -X POST "https://volai.cz/v1/dnc/+420777998877/unblock" \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"unblocked": true,
"e164": "+420777998877"
}Stany błędów
- 400
validatione164 w URL nie jest prawidłowym numerem telefonu.
Webhook
GET/v1/webhook
Bieżące ustawienia wychodzącego webhooka - bez sekretu podpisu, który wraca tylko z PUT.
Żądanie
curl https://volai.cz/v1/webhook \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"url": "https://tvoje-appka.cz/webhooks/volai",
"events": ["call.completed", "call.failed", "message.sent"],
"headers": { "Authorization": "Bear***" },
"recentDeliveries": [
{
"id": "whd_9f2b7a1c4e",
"event": "call.completed",
"url": "https://tvoje-appka.cz/webhooks/volai",
"status": 200,
"attempts": 1,
"ok": true,
"durationMs": 184,
"createdAt": 1756111641000
}
]
}PUT/v1/webhook
Ustawia (lub zastępuje) docelowy adres URL i zdarzenia, które Państwo subskrybują. Odpowiedź zawiera także secret podpisu - należy go od razu zapisać, GET później już go nie zwróci. Puste events: [] oznacza subskrypcję wszystkich zdarzeń, a nie żadnego - w tym dodanych później. Jeśli chcą Państwo tylko niektóre zdarzenia, należy je wymienić. Natomiast puste url: "" anuluje webhook - volai nie wysyła wtedy niczego, a odpowiedź zwraca url: null. Jak zweryfikować podpis, opisano na stronie Webhooki. Opcjonalne headers to własne nagłówki HTTP wysyłane z każdym dostarczeniem, na przykład Authorization: Bearer <key> dla celu przyjmującego tylko uwierzytelnione żądania. Wracają skrócone (Bear***); odesłana maska zachowuje zapisaną wartość, pominięcie pola zachowuje zapisane nagłówki, a {} je usuwa. Zmiana adresu URL na inną domenę usuwa zapisane nagłówki.
Żądanie
curl -X PUT "https://volai.cz/v1/webhook" \
-H "Authorization: Bearer vk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://tvoje-appka.cz/webhooks/volai","events":["call.completed","call.failed","message.sent"],"headers":{"Authorization":"Bearer tvuj-klic"}}'Odpowiedź
{
"url": "https://tvoje-appka.cz/webhooks/volai",
"secret": "whsec_9f2b7a1c4e6d8f0a",
"events": ["call.completed", "call.failed", "message.sent"],
"headers": { "Authorization": "Bear***" }
}Stany błędów
- 400
validationBrakuje url lub ma ponad 2000 znaków, events ma więcej niż 20 pozycji albo pozycja ma ponad 100 znaków, alboheadersnie jest obiektem wartości tekstowych. - 400
invalid_urlAdres URL musi zaczynać się od https:// i wskazywać publiczny adres (localhost, sieci wewnętrzne i adresy z danymi logowania są odrzucane; uwierzytelnianie należy wysyłać we własnym nagłówku przezheaders). - 400
invalid_eventsJedno ze zdarzeń nie jest rozpoznane (call.completed, call.failed, call.missed, call.no_answer, message.sent, message.received). - 400
invalid_webhook_headersNagłówka nie można wysłać: zabroniona, nieprawidłowa lub zduplikowana nazwa, pusta lub zbyt długa wartość (ponad 1000 znaków), znak spoza drukowalnego ASCII, w tym podział wiersza, więcej niż 5 nagłówków albo maska (***) zamiast wartości, czy to dla nowego nagłówka, czy po zmianie adresu URL na inną domenę.
DELETE/v1/webhook
Usuwa webhook - ten sam skutek co PUT /v1/webhook z pustym url, tylko nazwany wprost. Nowy webhook można ustawić ponownie w dowolnym momencie przez PUT. Odpowiednikiem MCP jest remove_webhook; list_webhook_deliveries to odpowiednik MCP dla GET /v1/webhook/deliveries powyżej.
Żądanie
curl -X DELETE https://volai.cz/v1/webhook \
-H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"removed":true}POST/v1/webhook/test
Wysyła jedną próbę testową na skonfigurowany adres URL, BEZ WZGLĘDU na filtr events z PUT /v1/webhook - testuje możliwość dostarczenia, a nie subskrypcję. Wysyła także Państwa własne headers, więc sprawdza również klucz celu. Bez ponowienia; nawet niepowodzenie jest zapisywane w historii dostarczeń. Najwyżej 10 na godzinę.
Żądanie
curl -X POST https://volai.cz/v1/webhook/test \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"delivered": true,
"status": 200,
"attempts": 1,
"durationMs": 184
}Stany błędów
- 404
not_foundNie ustawiono webhooka - najpierw należy go ustawić przezPUT /v1/webhook. - 429
rate_limitedOsiągnięto limit 10 zdarzeń testowych na godzinę.
GET/v1/webhook/deliveries
Ostatnie 50 dostarczeń (udanych, nieudanych i testowych), od najnowszych - w tym samym kształcie co recentDeliveries w GET /v1/webhook.
Żądanie
curl https://volai.cz/v1/webhook/deliveries \
-H "Authorization: Bearer vk_YOUR_KEY"Odpowiedź
{
"deliveries": [
{
"id": "whd_9f2b7a1c4e",
"event": "webhook.test",
"url": "https://tvoje-appka.cz/webhooks/volai",
"status": 200,
"attempts": 1,
"ok": true,
"durationMs": 184,
"createdAt": 1756111641000
}
]
}Google & Apple Calendar
Należy otworzyć Połączenia w portalu i połączyć konto: Google za pomocą zgody w przeglądarce (udzielenie dostępu do listy kalendarzy i wydarzeń) albo Apple za pomocą adresu e-mail konta Apple i hasła do aplikacji (wymaga uwierzytelniania dwuskładnikowego); volai automatycznie wykrywa kalendarze iCloud. REST i MCP używają następnie zwykłego klucza API volai należącego do tego konta, a dane logowania dostawcy nigdy nie są zwracane. Link połączenia należy otworzyć w przeglądarce, będąc zalogowanym do volai. GET /v1/integrations zawiera gotowość konfiguracji dostawców i adresy URL połączenia w przeglądarce; ready oznacza konfigurację serwera, a nie zweryfikowane połączenie konta.
Ten interfejs odczytuje i aktualizuje istniejące wydarzenia. Przed zapisem należy odczytać wydarzenie, pokazać użytkownikowi jego pierwotne wartości i propozycję, a po zatwierdzeniu wysłać sourceRevision, patch i confirm: true. Rewizja jest nieprzezroczysta i zawiera ewentualne znaki cudzysłowu. Błędy kalendarza mają prefiks calendar_: stale_revision (409) wymaga nowego odczytu i zatwierdzenia; oauth_failed/unauthorized (409) wymaga ponownego połączenia; provider_error (502) oznacza ponowienie później. Po niepewnym zapisie lub verification_failed (502) należy najpierw odczytać wydarzenie i sprawdzić wynik.
GET/v1/integrations
Lista należących do Państwa połączeń, gotowość konfiguracji dostawców i adresy URL połączenia w przeglądarce.
Żądanie
curl "https://volai.cz/v1/integrations" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"integrations":[],"providers":{"google-calendar":{"ready":true,"connectUrl":"https://volai.cz/api/integrations/google-calendar/oauth/start"},"apple-calendar":{"ready":true,"connectUrl":"https://volai.cz/en/connections"},"fakturoid":{"ready":true,"connectUrl":"https://volai.cz/api/integrations/fakturoid/oauth/start"},"abra-flexi":{"ready":true,"connectUrl":"https://volai.cz/en/connections"}},"googleCalendar":{"ready":true,"connectUrl":"https://volai.cz/api/integrations/google-calendar/oauth/start"}}POST/v1/integrations
Łączy Kalendarz Apple lub ABRA FlexiBee za pomocą danych logowania w treści. Google Calendar i Fakturoid mają tylko przepływ OAuth w przeglądarce (link connectUrl z GET /v1/integrations) - wysłanie tu ich provider zwraca integration_unsupported. Przyjmuje Idempotency-Key; nawet bez niego powtórne wywołanie z tymi samymi danymi logowania jest w praktyce idempotentne - istniejący rekord jest odnajdywany po dostawcy i koncie, do którego należą dane logowania (w Apple po nazwie użytkownika, w ABRA po adresie serwera i firmie); label to tylko nazwa wyświetlana i przy powtórnym wywołaniu jest nadpisywana.
Dane logowania należy przekazać tylko wtedy, gdy użytkownik umieścił je już w miejscu, które można odczytać (zmienna środowiskowa lub plik, który wskazał). Nigdy nie należy o nie prosić w rozmowie ani ich powtarzać.
Żądanie
curl -X POST https://volai.cz/v1/integrations \
-H "Authorization: Bearer $VOLAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"apple-calendar","username":"you@icloud.com","appPassword":"abcd-efgh-ijkl-mnop"}'Odpowiedź
{"integration":{"id":"int_7f3a91cd","provider":"apple-calendar","kind":"calendar","label":"apple-calendar","createdAt":1756111640000,"updatedAt":1756111640000}}Stany błędów
- 409
idempotency_in_progressRównoległe wywołanie z tym samymIdempotency-Keywciąż trwa - prosimy spróbować za chwilę. - 400
validationNieprawidłowy kształt treści - w Apple brakujeusername/appPassword, w ABRA brakujebaseUrl/company/username/password, albo treść zawiera nieznane pole (schemat jest.strict()). - 400
integration_unsupportedprovidertogoogle-calendarlubfakturoid- te łączą się wyłącznie przez przepływ OAuth w przeglądarce. - 400
integration_invalid_providerHost serwera ABRA FlexiBee (baseUrl) nie znajduje się na liście dozwolonych operatora - prosimy poprosić pomoc techniczną volai o włączenie domeny. - 400
integration_invalid_calendarAlbo konto Apple nie ma żadnego dostępnego kalendarza do połączenia, albo calendarUrl wysłany w żądaniu nie należy do kalendarzy zwróconych przez wykrywanie iCloud. - 409
integration_unauthorizedDostawca odrzucił dane logowania (błędna nazwa użytkownika lub hasło). - 409
integration_read_onlyApple CalDAV odrzucił zapis (403) - konto ma do kalendarza tylko dostęp do odczytu. - 409
integration_busyNa koncie trwa już inna operacja na integracjach - prosimy spróbować za chwilę. - 404
integration_not_foundIntegracja, którą to żądanie miało zaktualizować, w międzyczasie zniknęła - prosimy połączyć ponownie. - 502
integration_provider_errorBłąd po stronie dostawcy - prosimy spróbować za chwilę. - 502
integration_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 500
integration_storage_errorZapis danych logowania nie powiódł się po naszej stronie - prosimy spróbować ponownie.
DELETE/v1/integrations/{id}
Rozłącza dowolną integrację (Google, Apple, Fakturoid, ABRA) - nieodwracalnie, bez potwierdzenia i bez narzędzia MCP, ponieważ rozłączenia nie da się cofnąć. Ponowne połączenie przez REST działa tylko dla Apple i ABRA (zob. POST /v1/integrations wyżej) - Google i Fakturoid znów przechodzą przez przepływ OAuth w przeglądarce.
Żądanie
curl -X DELETE https://volai.cz/v1/integrations/int_7f3a91cd \
-H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"disconnected":true}Stany błędów
- 404
integration_not_foundIntegracja nie istnieje lub nie należy do Państwa konta. - 409
integration_busyNa integracji trwa już inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę.
GET/v1/integrations/{id}/calendars
Kalendarze należącego do Państwa połączenia. Id pochodzi z listy połączeń.
Żądanie
curl "https://volai.cz/v1/integrations/int_CONNECTION/calendars" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"calendars":[{"id":"primary","label":"Work","timezone":"Europe/Prague","accessRole":"owner"}]}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną.
POST/v1/integrations/{id}/availability
Zwraca wolne terminy w połączonym kalendarzu BEZ jakiegokolwiek zapisanego agenta - to samo obliczenie co check_availability podczas połączenia, tylko bez reguł agenta. Pominięte windows oznacza 24 godziny każdego dnia; jeśli zostanie wysłane, dzień tygodnia pominięty nie ma okna. to musi być późniejsze niż from i oddalone najwyżej o 31 dni. Odpowiedź zawiera tylko start/end i timezone - bez tytułów ani opisów wydarzeń.
Żądanie
curl -X POST "https://volai.cz/v1/integrations/int_CONNECTION/availability" -H "Authorization: Bearer $VOLAI_API_KEY" -H "Content-Type: application/json" -d '{"calendarId":"primary","from":"2026-09-22T00:00:00+02:00","to":"2026-09-23T00:00:00+02:00","slotMinutes":30,"windows":{"tue":[{"from":"09:00","to":"17:00"}]}}'Odpowiedź
{"slots":[{"start":"2026-09-22T09:00:00+02:00","end":"2026-09-22T09:30:00+02:00"}],"timezone":"Europe/Prague"}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 400
validationNieprawidłowy kształt treści (calendarId, from, to, ...) - treść jest.strict(), nieznane pole jest odrzucane; from/to wymagają RFC3339 z przesunięciem. - 400
calendar_invalid_calendarcalendarId nie odpowiada żadnemu kalendarzowi z GET .../calendars dla tego połączenia. - 400
calendar_too_many_eventsŻądany zakres zawiera więcej wydarzeń, niż może odczytać jedno wyszukiwanie - prosimy poprosić o krótszy zakres (dni zamiast tygodni) i spróbować ponownie. - 400
calendar_invalid_rangeto musi być późniejsze niż from i oddalone od niego najwyżej o 31 dni. - 400
calendar_invalid_timezonetimezone nie jest prawidłową strefą IANA, na przykład Europe/Prague. - 400
calendar_invalid_windowsOkno wymaga from wcześniejszego niż to w formacie HH:MM, najwyżej 4 okien dziennie, a okna tego samego dnia nie mogą się nakładać. - 400
calendar_no_windowsŻaden dzień tygodnia nie ma okna dostępności - prosimy dodać co najmniej jedno. - 400
calendar_invalid_rulesJedna z liczb jest poza zakresem albo minNoticeMinutes jest dłuższe niż cały horyzont wyszukiwania.
GET/v1/integrations/{id}/events
Wymagane zapytanie calendarId. Do 50 wydarzeń Google lub wydarzeń i serii cyklicznych Apple, domyślnie nadchodzące. Opcjonalne search, timeMin, timeMax i pageToken. Granice czasu wymagają przesunięć. Kontynuację zapewnia nextPageToken z identycznymi filtrami, w tym ze stałym timeMin. W Apple recurring:true oznacza całą serię; jej start/end opisują pierwotne wystąpienie, a aktualizacje zmieniają wzorzec serii, zachowując poszczególne wyjątki. Nieznany parametr zapytania jest tu odrzucany błędem 400 validation - w przeciwieństwie do reszty v1 dodatkowe parametry nie są ignorowane.
Żądanie
curl "https://volai.cz/v1/integrations/int_CONNECTION/events?calendarId=primary" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"events":[{"calendarId":"primary","externalId":"event1","title":"Meeting","start":"2026-10-05T10:00:00+02:00","end":"2026-10-05T11:00:00+02:00","status":"confirmed","sourceRevision":"revision1"}]}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 400
validationNieprawidłowy kształt zapytania (np. timeMax nie jest późniejszy niż timeMin albo nieznany parametr) - limity poszczególnych parametrów podano wyżej. - 400
calendar_invalid_calendarcalendarId nie odpowiada żadnemu kalendarzowi z GET .../calendars dla tego połączenia. - 400
calendar_invalid_queryNieprawidłowy pageToken lub eventId Apple. Należy użyć externalId ze świeżej listy wydarzeń i zachować niezmienione filtry stronicowania. - 409
calendar_stale_revisionsourceRevision nie zgadza się z bieżącą rewizją wydarzenia - ktoś zmienił je w międzyczasie. Należy odczytać je ponownie, pokazać użytkownikowi nowy stan i uzyskać zatwierdzenie nowej propozycji. W liście wydarzeń Apple ten sam kod oznacza, że lista zmieniła się między stronami: należy porzucić pageToken i odczytać pierwszą stronę od nowa.
POST/v1/integrations/{id}/events
Tworzy termin w połączonym kalendarzu po jawnym potwierdzeniu użytkownika (confirm: true). Należy podać dokładnie jedno z end lub durationMinutes. idempotencyKey jest opcjonalny - ta sama wartość wysłana ponownie zwraca już utworzone wydarzenie zamiast drugiego zapisu. Tuż przed zapisem sprawdzane jest, czy termin nadal jest wolny; odpowiedzią jest świeżo odczytane wydarzenie w tym samym kształcie co GET .../events/{eventId}.
Żądanie
curl -X POST "https://volai.cz/v1/integrations/int_CONNECTION/events" -H "Authorization: Bearer $VOLAI_API_KEY" -H "Content-Type: application/json" -d '{"calendarId":"primary","start":"2026-09-22T09:00:00+02:00","durationMinutes":30,"title":"Meeting","confirm":true}'Odpowiedź
{
"event": {
"calendarId": "primary",
"externalId": "event1",
"title": "Meeting",
"start": "2026-10-05T10:00:00+02:00",
"end": "2026-10-05T11:00:00+02:00",
"timezone": "Europe/Prague",
"status": "confirmed",
"sourceRevision": "revision1",
"provider": "google-calendar",
"sourceUrl": "https://calendar.google.com/calendar/event?eid=ZXZlbnQx",
"fetchedAt": "2026-09-08T09:00:00.000Z"
}
}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 400
validationNieprawidłowy kształt treści - brakuje dokładnie jednego z end/durationMinutes, tytuł jest pusty lub zbyt długi, albo występuje nieznane pole (.strict()). - 400
calendar_invalid_calendarcalendarId nie odpowiada żadnemu kalendarzowi z GET .../calendars dla tego połączenia. - 400
calendar_invalid_rangeto musi być późniejsze niż from i oddalone od niego najwyżej o 31 dni. - 400
calendar_confirmation_requiredconfirm musi mieć wartość true - bez jawnej zgody użytkownika termin nie zostanie utworzony. - 409
calendar_slot_takenŻądany czas nie jest teraz wolny - prosimy ponownie odczytać dostępność (POST .../availability) i wybrać inny czas. - 503
calendar_unavailableKalendarz jest teraz nieosiągalny do odczytu lub zapisu - prosimy spróbować za chwilę.
GET/v1/integrations/{id}/events/{eventId}
Wymagane zapytanie calendarId. Świeże wydarzenie z sourceRevision i fetchedAt. Identyfikatory należy zakodować jako URL w obu ścieżkach i w wartościach zapytania. W przypadku Apple nieprzezroczysty externalId zwrócony przez listę wydarzeń należy przekazać bez zmian jako eventId. Wskazuje on zasób CalDAV, a nie UID iCalendar; nie należy go konstruować ani dekodować.
Żądanie
curl "https://volai.cz/v1/integrations/int_CONNECTION/events/EVENT_ID?calendarId=primary" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{
"event": {
"calendarId": "primary",
"externalId": "event1",
"title": "Meeting",
"start": "2026-10-05T10:00:00+02:00",
"end": "2026-10-05T11:00:00+02:00",
"timezone": "Europe/Prague",
"status": "confirmed",
"sourceRevision": "revision1",
"provider": "google-calendar",
"sourceUrl": "https://calendar.google.com/calendar/event?eid=ZXZlbnQx",
"fetchedAt": "2026-09-08T09:00:00.000Z"
}
}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 400
validationcalendarId w zapytaniu jest wymagane i może mieć co najwyżej 500 znaków. - 400
calendar_invalid_calendarcalendarId nie odpowiada żadnemu kalendarzowi z GET .../calendars dla tego połączenia. - 400
calendar_invalid_queryNieprawidłowy pageToken lub eventId Apple. Należy użyć externalId ze świeżej listy wydarzeń i zachować niezmienione filtry stronicowania.
PATCH/v1/integrations/{id}/events/{eventId}
Zmiana tytułu, początku lub końca. Należy użyć znaczników czasu RFC3339 z przesunięciami albo dat całodniowych RRRR-MM-DD z wyłączonym końcem. Pływające czasy Apple mogą pomijać przesunięcie tylko przy zachowaniu istniejącego wydarzenia o czasie pływającym. Wymaga potwierdzenia i aktualnej rewizji; zwraca zweryfikowane wydarzenie.
Żądanie
curl -X PATCH "https://volai.cz/v1/integrations/int_CONNECTION/events/EVENT_ID" -H "Authorization: Bearer $VOLAI_API_KEY" -H "Content-Type: application/json" -d '{"calendarId":"primary","sourceRevision":"revision1","patch":{"title":"Updated meeting"},"confirm":true}'Odpowiedź
{
"event": {
"calendarId": "primary",
"externalId": "event1",
"title": "Updated meeting",
"start": "2026-10-05T10:00:00+02:00",
"end": "2026-10-05T11:00:00+02:00",
"timezone": "Europe/Prague",
"status": "confirmed",
"sourceRevision": "revision1",
"provider": "google-calendar",
"sourceUrl": "https://calendar.google.com/calendar/event?eid=ZXZlbnQx",
"fetchedAt": "2026-09-08T09:00:00.000Z"
}
}Stany błędów
- 404
calendar_not_foundNie istnieje ani integracja, ani kalendarz o tym id, albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe), więc ten endpoint go nie dotyczy. - 409
calendar_read_onlyKalendarz jest połączony tylko do odczytu (Apple CalDAV odrzucił zapis) - prosimy sprawdzić swoje uprawnienia do udostępnionego kalendarza w Kalendarzu Apple. - 409
calendar_busyNa tym połączeniu trwa właśnie inna operacja (zwykle odświeżanie tokenu) - prosimy spróbować za chwilę. - 409
calendar_refresh_in_progressDostęp do konta jest odświeżany w tle - prosimy spróbować za chwilę. - 409
calendar_unauthorizedPołączenie utraciło dostęp u dostawcy - prosimy ponownie połączyć konto w portalu. - 409
calendar_oauth_failedZalogowanie do konta dostawcy nie powiodło się - prosimy ponownie połączyć konto w portalu. - 409
calendar_credentials_unavailableZapisanych danych logowania połączenia nie udało się odszyfrować - prosimy ponownie połączyć konto w portalu; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 409
calendar_invalid_credentialsDostawca odrzucił zapisane dane logowania połączenia - prosimy ponownie połączyć konto w portalu. - 503
calendar_not_configuredIntegracja kalendarza nie jest obecnie skonfigurowana po naszej stronie - prosimy skontaktować się z pomocą techniczną. - 502
calendar_provider_errorBłąd po stronie dostawcy (Google, Apple) - prosimy spróbować za chwilę. - 502
calendar_invalid_provider_responseDostawca zwrócił nieoczekiwaną odpowiedź - prosimy spróbować ponownie, a jeśli problem się powtarza, skontaktować się z pomocą techniczną. - 400
validationNieprawidłowy kształt treści (calendarId, sourceRevision, patch, confirm) - treść jest.strict(), nieznane pole jest odrzucane. - 400
calendar_invalid_calendarcalendarId nie odpowiada żadnemu kalendarzowi z GET .../calendars dla tego połączenia. - 400
calendar_invalid_queryNieprawidłowy pageToken lub eventId Apple. Należy użyć externalId ze świeżej listy wydarzeń i zachować niezmienione filtry stronicowania. - 400
calendar_provider_mismatchPołączenie w międzyczasie zmieniło dostawcę, więc propozycja z wcześniejszego odczytu już do niego nie należy - wydarzenie należy odczytać ponownie i uzyskać zatwierdzenie nowej propozycji. - 400
calendar_invalid_patchpatch ma nieprawidłowy kształt lub nieznane pole - dozwolone są tylko title, start i end; start/end wymagają przesunięcia (albo całodniowego RRRR-MM-DD z późniejszym końcem). - 400
calendar_confirmation_requiredconfirm musi mieć wartość true - bez potwierdzenia użytkownika wydarzenie się nie zmieni. - 409
calendar_stale_revisionsourceRevision nie zgadza się z bieżącą rewizją wydarzenia - ktoś zmienił je w międzyczasie. Należy odczytać je ponownie, pokazać użytkownikowi nowy stan i uzyskać zatwierdzenie nowej propozycji. W liście wydarzeń Apple ten sam kod oznacza, że lista zmieniła się między stronami: należy porzucić pageToken i odczytać pierwszą stronę od nowa. - 502
calendar_verification_failedZapis się powiódł, ale nie udało się zweryfikować jego odczytu zwrotnego od dostawcy - należy odczytać wydarzenie ponownie i sprawdzić wynik.
POST/v1/integrations/{id}/events/{eventId}/propose
Przygotowuje proponowaną zmianę tytułu, początku lub końca wydarzenia BEZ zapisu u dostawcy (patch i sourceRevision jak w PATCH .../events/{eventId}, ale bez confirm) - czysta funkcja na zapisanym typie integracji, nigdy nie wywołuje dostawcy. Propozycję (proposal.patch) należy pokazać użytkownikowi do jawnego zatwierdzenia, a następnie wysłać PATCH .../events/{eventId} z tymi samymi wartościami i confirm: true.
Żądanie
curl -X POST "https://volai.cz/v1/integrations/int_CONNECTION/events/EVENT_ID/propose" -H "Authorization: Bearer $VOLAI_API_KEY" -H "Content-Type: application/json" -d '{"calendarId":"primary","sourceRevision":"revision1","patch":{"title":"Updated meeting"}}'Odpowiedź
{"proposal":{"calendarId":"primary","externalId":"EVENT_ID","sourceRevision":"revision1","patch":{"title":"Updated meeting"},"provider":"google-calendar","proposedAt":"2026-09-08T09:00:00.000Z","requiresExplicitConfirmation":true}}Stany błędów
- 400
validationNieprawidłowy kształt treści - brakujecalendarId/sourceRevisionalbopatchzawiera pole inne niżtitle/start/end. - 404
calendar_not_foundIntegracja lub kalendarz o tym ID nie istnieje albo nie należy do Państwa konta. - 400
calendar_unsupportedPołączenie nie jest kalendarzem (jest fakturowe) - ten endpoint go nie dotyczy. - 400
calendar_invalid_patchpatchma nieprawidłowy kształt -start/endmuszą zawierać strefę czasową (albo być całodniowymYYYY-MM-DDz późniejszym końcem).
Faktury Fakturoid i ABRA Flexi
Najpierw należy połączyć konto w sekcji Połączenia. Fakturoid używa OAuth w przeglądarce; gdy dostępnych jest wiele firm z włączonym fakturowaniem, należy wybrać jedną jawnie. ABRA Flexi wymaga serwera HTTPS, identyfikatora firmy i danych logowania użytkownika API; nazwa hosta serwera musi być włączona przez operatora volai. Dane logowania pozostają w portalu. Te endpointy odczytują wystawione faktury i nigdy nie tworzą faktur ani nie oznaczają ich jako opłaconych.
Błędy endpointów faktur mają prefiks integration_ oraz cause i action jak każdy błąd API. Według przyczyny: account oznacza ponowne połączenie konta w sekcji Połączenia, ponawianie bez tego nie ma sensu; busy (409) należy ponowić za kilka sekund, dane są w porządku; service (400, 502 lub 503) leży po naszej stronie albo po stronie dostawcy - należy ponowić później i do tego czasu traktować stan faktury jako NIEZNANY; request (400) wymaga poprawienia zapytania. Nigdy nie należy zastępować nieudanego odczytu wcześniejszym lub podanym przez wywołującego twierdzeniem o płatności.
GET/v1/integrations/{id}/invoices
Opcjonalne zapytanie wyszukiwania (do 100 znaków) przeszukuje dostawcę. Zwraca pierwszych 40 wyników z Fakturoid lub 50 z ABRA Flexi. Aby znaleźć starsze faktury, należy doprecyzować zapytanie; ta odpowiedź nie ma tokenu stronicowania.
Żądanie
curl "https://volai.cz/v1/integrations/int_CONNECTION/invoices?search=2026-0001" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"invoices":[{"externalId":"123","label":"2026-0001","status":"unpaid","amountDueMinor":1210000,"currency":"CZK","dueDate":"2026-09-30","sourceRevision":"v3"}]}Stany błędów
- 400
validationsearch ma więcej niż 100 znaków albo zapytanie zawiera nieznany parametr (na przykład limit) - endpointy kalendarza i faktur odrzucają nieznane parametry, reszta v1 je ignoruje. - 404
integration_not_foundNa koncie nie ma połączenia o tym id; w szczegółach także wtedy, gdy dostawca nie zna faktury o tym invoiceId (na liście 404 dostawcy przychodzi jako provider_error). - 400
integration_unsupportedPołączenie nie jest fakturowe (na przykład kalendarz) - faktury udostępniają tylko Fakturoid i ABRA Flexi. - 400
integration_invalid_providerSerwer ABRA Flexi zapisany w połączeniu nie znajduje się na liście hostów dozwolonych przez operatora. Prosimy skontaktować się z pomocą techniczną. - 409
integration_busyDane logowania połączenia są właśnie odświeżane, a rekord w międzyczasie się zmienił - żądanie należy ponowić za kilka sekund. - 409
integration_refresh_in_progressInne żądanie już odświeża token połączenia - żądanie należy ponowić za kilka sekund. - 409
integration_unauthorizedDostawca odrzucił zapisane dane logowania (HTTP 401) - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_oauth_failedOdświeżenie tokenu Fakturoid nie powiodło się - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_credentials_unavailableZapisanych danych logowania nie udało się odszyfrować - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_invalid_credentialsZapisane dane logowania nie mają oczekiwanego kształtu (brakuje tokenu, slugu lub użytkownika API) - prosimy ponownie połączyć konto w sekcji Połączenia. - 503
integration_not_configuredOperator nie skonfigurował kluczy dostawcy (na przykład OAuth Fakturoid) - nic Państwu za to nie naliczamy, prosimy skontaktować się z pomocą techniczną. - 502
integration_provider_errorDostawca odpowiedział błędem (w tym HTTP 403) albo nie odpowiedział - żądanie należy ponowić później; do tego czasu stan faktury traktować jako nieznany. - 502
integration_invalid_provider_responseDostawca zwrócił odpowiedź o nieoczekiwanym kształcie - żądanie należy ponowić później; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną. - 400
integration_invalid_queryParametr search zawiera ukośniki odwrotne, znaki sterujące lub mieszane cudzysłowy, które wyszukiwarka ABRA Flexi odrzuca - zapytanie należy uprościć.
GET/v1/integrations/{id}/invoices/{invoiceId}
Przed zgłoszeniem, czy faktura jest opłacona, należy odczytać świeży dowód płatności. amountDueMinor to liczba całkowita w jednostkach podrzędnych waluty; null oznacza, że dostawca nie ustalił salda. status może mieć wartość unknown. Ani unknown, ani brak kwoty nie dowodzi zapłaty. Szczegóły zawierają provider, sourceUrl, fetchedAt i evidence: provider_response. invoiceId należy zakodować jako URL.
Żądanie
curl "https://volai.cz/v1/integrations/int_CONNECTION/invoices/123" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"invoice":{"externalId":"123","status":"unpaid","amountDueMinor":1210000,"currency":"CZK","dueDate":"2026-09-30","sourceRevision":"v3","provider":"fakturoid","fetchedAt":"2026-09-08T09:00:00.000Z","sourceUrl":"https://app.fakturoid.cz/api/v3/accounts/firma/invoices/123.json","evidence":"provider_response"}}Stany błędów
- 400
validationinvoiceId w ścieżce brakuje lub ma więcej niż 500 znaków. - 404
integration_not_foundNa koncie nie ma połączenia o tym id; w szczegółach także wtedy, gdy dostawca nie zna faktury o tym invoiceId (na liście 404 dostawcy przychodzi jako provider_error). - 400
integration_unsupportedPołączenie nie jest fakturowe (na przykład kalendarz) - faktury udostępniają tylko Fakturoid i ABRA Flexi. - 400
integration_invalid_providerSerwer ABRA Flexi zapisany w połączeniu nie znajduje się na liście hostów dozwolonych przez operatora. Prosimy skontaktować się z pomocą techniczną. - 409
integration_busyDane logowania połączenia są właśnie odświeżane, a rekord w międzyczasie się zmienił - żądanie należy ponowić za kilka sekund. - 409
integration_refresh_in_progressInne żądanie już odświeża token połączenia - żądanie należy ponowić za kilka sekund. - 409
integration_unauthorizedDostawca odrzucił zapisane dane logowania (HTTP 401) - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_oauth_failedOdświeżenie tokenu Fakturoid nie powiodło się - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_credentials_unavailableZapisanych danych logowania nie udało się odszyfrować - prosimy ponownie połączyć konto w sekcji Połączenia. - 409
integration_invalid_credentialsZapisane dane logowania nie mają oczekiwanego kształtu (brakuje tokenu, slugu lub użytkownika API) - prosimy ponownie połączyć konto w sekcji Połączenia. - 503
integration_not_configuredOperator nie skonfigurował kluczy dostawcy (na przykład OAuth Fakturoid) - nic Państwu za to nie naliczamy, prosimy skontaktować się z pomocą techniczną. - 502
integration_provider_errorDostawca odpowiedział błędem (w tym HTTP 403) albo nie odpowiedział - żądanie należy ponowić później; do tego czasu stan faktury traktować jako nieznany. - 502
integration_invalid_provider_responseDostawca zwrócił odpowiedź o nieoczekiwanym kształcie - żądanie należy ponowić później; jeśli problem się powtarza, prosimy skontaktować się z pomocą techniczną.
Zamówienia konfiguracji (Na klucz)
Usługa Na klucz konfiguruje działającego agenta na podstawie opisu zwykłym językiem zamiast ręcznej konfiguracji w portalu. POST /v1/setup-orders wykonuje jednorazową turę rozmowy: opisują Państwo agenta, a doradca zwraca wycenę (quoted), prosi o potwierdzenie pozycji niestandardowych powyżej progu (awaiting_confirmation) albo odmawia automatycznej konfiguracji dla przypadku użycia, do którego nie pasuje (declined). Samą cenę liczy kod ze stałego katalogu, nigdy model - doradca tylko wybiera pakiet, dodatki i pozycje niestandardowe. Opłacenie wyceny (POST .../checkout, sesja Stripe Checkout obejmująca opłatę za konfigurację plus pierwsze doładowanie salda) rozpoczyna budowę; volai buduje agenta ręcznie i przeprowadza zamówienie przez paid -> building -> ready. Gdy jest ready, można zadzwonić na dołączony numer, aby go wypróbować, wysłać wiadomość z prośbą o zmiany (revision), a następnie POST .../launch, aby uruchomić produkcyjnie. GET .../{id} zawsze zawiera nextStep, zdanie prostym językiem opisujące, co zrobić dalej w bieżącym statusie.
POST/v1/setup-orders
Jednorazowo: w brief (co najmniej 20 znaków) opisują Państwo potrzebnego agenta i otrzymują wycenę albo prośbę o potwierdzenie pozycji niestandardowych. locale ustala język rozmowy z doradcą i wszelkich e-maili dotyczących tego zamówienia (domyślnie en); nie zmienia języka tej odpowiedzi - REST zawsze odpowiada po angielsku. contactPhone jest dołączany do briefu jako notatka dla volai, bez walidacji. Ograniczone do 5 nowych zamówień konfiguracji na konto na dobę. Jeśli tura doradcy się nie powiedzie (advisor_unavailable, advisor_no_quote), zamówienie zostało już utworzone i wliczone do tego limitu, więc ponowienie polega na odnalezieniu go przez GET /v1/setup-orders i wysłaniu kolejnej próby przez POST .../messages, a nie na ponownym wywołaniu tego endpointu. Bez Idempotency-Key: każde wywołanie tworzy nowe zamówienie, jedynym hamulcem jest limit 5 na dobę - po przekroczeniu czasu najpierw należy sprawdzić GET /v1/setup-orders.
Żądanie
curl -X POST https://volai.cz/v1/setup-orders \
-H "Authorization: Bearer $VOLAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"brief":"Reception for a Prague fitness studio, Czech and English, takes bookings and answers pricing questions.","locale":"en"}'201 Created
{"order":{"id":"so_AbCdEf123456","status":"quoted","nextStep":"Start the checkout to pay the setup fee and begin the build.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1757980810000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"revisionsIncluded":2,"quote":{"lines":[{"kind":"package","id":"provozovna","label":"Business","quantity":1,"unitHal":299000,"totalHal":299000},{"kind":"addon","id":"calendar","label":"Calendar booking","quantity":1,"unitHal":99000,"totalHal":99000}],"setupNetHal":398000,"needsConfirmation":false,"revisionsIncluded":2,"monthly":{"callsPerDay":20,"avgMinutes":3,"totalMonthlyHal":542500,"callMinutes":1800}},"selection":{"segment":"provozovna","packageId":"provozovna","addons":[{"id":"calendar","quantity":1}],"customItems":[],"industry":"fitness studio","agentLanguages":["cs","en"],"callsPerDay":20,"avgMinutes":3,"cannotDo":[],"assumptions":["Business hours 9am-6pm"]},"messages":[{"role":"user","content":"Reception for a Prague fitness studio...","at":1757980800000},{"role":"assistant","content":"Here is your quote for a location agent...","at":1757980810000}]}}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (briefbrakuje lub ma mniej niż 20 znaków,contactPhonejest zbyt długi albolocalenie jestcs/en). - 422
advisor_no_quoteDoradca uznał, że musi zadać pytanie, ale tura jednorazowa wymaga wyceny lub odmowy - prosimy spróbować ponownie albo sformułować brief z większą ilością szczegółów. - 503
advisor_unavailableDoradca jest tymczasowo niedostępny (nie skonfigurowano klucza API, błąd sieci lub model odmówił) - prosimy spróbować za chwilę. - 503
setup_orders_disabledUsługa Na klucz jest tymczasowo wyłączona dla wszystkich kont; istniejące opłacone zamówienia przebiegają normalnie.
GET/v1/setup-orders
Zamówienia konfiguracji konta, bez stronicowania.
Żądanie
curl "https://volai.cz/v1/setup-orders" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"orders":[{"id":"so_AbCdEf123456","status":"quoted","nextStep":"Start the checkout to pay the setup fee and begin the build.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1757980810000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"messages":[]}]}GET/v1/setup-orders/{id}
Szczegóły zamówienia konfiguracji, wraz z każdą wiadomością w rozmowie (w tym odpowiedziami właściciela od zespołu volai) i nextStep.
Żądanie
curl "https://volai.cz/v1/setup-orders/so_AbCdEf123456" -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"order":{"id":"so_AbCdEf123456","status":"ready","nextStep":"Call the number, then send a message for changes or launch.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1758153600000,"readyAt":1758153600000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"revisionsIncluded":2,"agentId":"ag_XyZ987","numberE164":"+420999123456","messages":[{"role":"user","content":"Reception for a Prague fitness studio...","at":1757980800000},{"role":"owner","content":"Your agent is ready - give it a call.","at":1758153600000}]}}Stany błędów
- 404
setup_order_not_foundNa koncie nie ma zamówienia konfiguracji o tymid.
POST/v1/setup-orders/{id}/messages
Wysyła wiadomość do doradcy konfiguracji. Dopóki zamówienie jest quoted, nowa wiadomość może wywołać kolejną turę doradcy, która zastępuje wycenę - chyba że jest dla niej aktualnie otwarty checkout (checkout_open), który zamraża wycenę do czasu wygaśnięcia lub wykorzystania tej sesji Checkout. W pozostałych statusach (ready, paid, building, revision) wiadomość jest po prostu zapisywana dla zespołu volai; wysłanie jej w statusie ready przenosi zamówienie do revision. Rundy wliczone w pakiet (order.quote.revisionsIncluded, już wykorzystane order.revisionsUsed) są bezpłatne; po ich wyczerpaniu następna runda, którą zakończy założyciel, jest naliczana 490 CZK bez VAT z salda jako wpis księgi setup_revision.
Żądanie
curl -X POST https://volai.cz/v1/setup-orders/so_AbCdEf123456/messages \
-H "Authorization: Bearer $VOLAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message":"Can it also speak German?"}'Odpowiedź
{"order":{"id":"so_AbCdEf123456","status":"quoted","nextStep":"Start the checkout to pay the setup fee and begin the build.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1757981200000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"messages":[{"role":"user","content":"Reception for a Prague fitness studio...","at":1757980800000},{"role":"assistant","content":"Here is your quote...","at":1757980810000},{"role":"user","content":"Can it also speak German?","at":1757981190000},{"role":"assistant","content":"Updated the quote to add German.","at":1757981200000}]}}Stany błędów
- 400
validationBrakujemessage, jest puste albo przekracza 2000 znaków. - 404
setup_order_not_foundNa koncie nie ma zamówienia konfiguracji o tymid. - 409
order_closedZamówienie jestlaunchedlubcancelled- nie przyjmuje już wiadomości. - 409
turn_in_progressInne żądanie dla tego samego zamówienia jest już w toku - należy poczekać na jego zakończenie i ponowić. - 409
checkout_openDla wyceny tego zamówienia jest aktualnie otwarta sesja Checkout - przed wysłaniem wiadomości, która mogłaby zmienić cenę, należy poczekać na jej wygaśnięcie lub zakończenie. - 422
advisor_no_quoteDoradca uznał, że musi zadać pytanie, ale tura jednorazowa wymaga wyceny lub odmowy - prosimy spróbować ponownie albo sformułować brief z większą ilością szczegółów. - 503
advisor_unavailableDoradca jest tymczasowo niedostępny (nie skonfigurowano klucza API, błąd sieci lub model odmówił) - prosimy spróbować za chwilę.
POST/v1/setup-orders/{id}/checkout
Tworzy sesję Stripe Checkout dla wycenionej opłaty za konfigurację plus pierwszego doładowania salda (creditCzk, jedna z obsługiwanych kwot doładowania konta). Tylko ze statusu quoted i tylko przez właściciela zamówienia. Przekierowanie po sukcesie i po anulowaniu wskazuje z powrotem stronę zamówienia w języku rozmowy (order.locale), niekoniecznie tego żądania.
Żądanie
curl -X POST https://volai.cz/v1/setup-orders/so_AbCdEf123456/checkout \
-H "Authorization: Bearer $VOLAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"creditCzk":500}'Odpowiedź
{"url":"https://checkout.stripe.com/c/pay/cs_test_...","sessionId":"cs_test_a1B2c3D4"}Stany błędów
- 400
validationTreść nie zgadza się ze schematem (creditCzknie jest liczbą całkowitą). - 400
invalid_amountcreditCzknie jest jedną z obsługiwanych kwot doładowania konta. - 400
billing_address_requiredKonto nie ma jeszcze zapisanego kompletnego adresu do faktury - najpierw należy go uzupełnić (PUT /v1/account/billing). - 404
setup_order_not_foundNa koncie nie ma zamówienia konfiguracji o tymid. - 409
checkout_openDla wyceny tego zamówienia jest aktualnie otwarta sesja Checkout - przed wysłaniem wiadomości, która mogłaby zmienić cenę, należy poczekać na jej wygaśnięcie lub zakończenie. - 409
invalid_transitionZamówienie nie jestquotedalbo brakuje jego wyceny - checkout otwiera się tylko z zamówienia wycenionego. - 409
quote_pending_confirmationWycena wymaga jeszcze potwierdzenia przez volai pozycji niestandardowych (awaiting_confirmation), zanim będzie można otworzyć checkout. - 409
turn_in_progressInne żądanie dla tego samego zamówienia jest już w toku - należy poczekać na jego zakończenie i ponowić. - 503
setup_orders_disabledUsługa Na klucz jest tymczasowo wyłączona dla wszystkich kont; istniejące opłacone zamówienia przebiegają normalnie.
POST/v1/setup-orders/{id}/launch
Uruchamia gotowego agenta (ready -> launched) - jedyne przejście, które klient wykonuje sam; każde inne przejście (rozpoczęcie budowy, gotowość, anulowanie) należy do zespołu volai.
Żądanie
curl -X POST https://volai.cz/v1/setup-orders/so_AbCdEf123456/launch -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"order":{"id":"so_AbCdEf123456","status":"launched","nextStep":"Your agent is live.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1758160000000,"readyAt":1758153600000,"launchedAt":1758160000000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"agentId":"ag_XyZ987","numberE164":"+420999123456","messages":[]}}Stany błędów
- 404
setup_order_not_foundNa koncie nie ma zamówienia konfiguracji o tymid. - 409
invalid_transitionZamówienie nie jestready- nic nie jest jeszcze gotowe do uruchomienia.
POST/v1/setup-orders/{id}/cancel
Anuluje nieopłacone zamówienie konfiguracji (draft, quoted, awaiting_confirmation lub declined -> cancelled). Opłaconego zamówienia nie można anulować przez ten endpoint - prosimy skontaktować się z volai.
Żądanie
curl -X POST https://volai.cz/v1/setup-orders/so_AbCdEf123456/cancel -H "Authorization: Bearer $VOLAI_API_KEY"Odpowiedź
{"order":{"id":"so_AbCdEf123456","status":"cancelled","nextStep":"This order was cancelled.","locale":"en","source":"api","createdAt":1757980800000,"updatedAt":1757981500000,"revisionsUsed":0,"extraRevisionHal":49000,"extraRevisionsCharged":0,"messages":[]}}Stany błędów
- 404
setup_order_not_foundNa koncie nie ma zamówienia konfiguracji o tymid. - 409
invalid_transitionZamówienia nie można anulować w jego bieżącym statusie (jest już opłacone albo już zamknięte).
Dziennik zmian
Maszynowo czytelny dziennik zmian publicznego interfejsu - zarówno REST v1, jak i MCP. Jeden dokument pod jednym adresem URL (bez parametrów zapytania - zwróconą tablicę entries filtrują Państwo samodzielnie), dzięki czemu można go bezpiecznie publicznie buforować. Te same dane co /en/changelog w portalu i narzędzie MCP get_changelog. Kanonicznym źródłem prawdy jest jeden plik (content/changelog.json) - każdy widok jest z niego generowany.
GET/v1/changelog
Zwraca bieżącą wersję interfejsu i WSZYSTKIE wpisy dziennika zmian (od najnowszych), po angielsku niezależnie od języka konta. version/technical mają wartość null, gdy wpis ich nie miał (stare wpisy bez wersji lub bez szczegółów technicznych). url to kanoniczna angielska ścieżka z kotwicą do id.
Żądanie
curl https://volai.cz/v1/changelogOdpowiedź
{
"version": "1.57.0",
"entries": [
{
"id": "parita-vlna-a",
"date": "2026-09-08",
"version": "1.5.0",
"areas": ["api", "mcp"],
"audience": "developer",
"breaking": false,
"title": "Account, call annotations, tool test, draft rollback, numbers waitlist and more",
"body": "See /en/docs/api and /en/docs/mcp for the new endpoints and tools.",
"technical": null,
"url": "https://volai.cz/en/changelog#parita-vlna-a"
}
]
}Stany błędów
- 429
rate_limitedOsiągnięto limit 30 żądań na minutę z jednego adresu IP (bez klucza API limit dotyczy adresu IP, a nie konta).
Nagłówek x-volai-version
Absolutnie KAŻDA odpowiedź v1 (udana lub z błędem, w tym 401/429, ścieżki binarne takie jak nagranie połączenia oraz GET /openapi.json) zawiera nagłówek x-volai-version z aktualną wersją interfejsu. Jeśli różni się od ostatniej widzianej wersji, należy wywołać GET /v1/changelog (lub MCP get_changelog), aby zobaczyć, co nowego - klient MCP dostaje tę samą rekomendację bezpośrednio w SERVER_INSTRUCTIONS serwera.
x-volai-version: 1.57.0Odpowiednikiem MCP jest narzędzie get_changelog (READ_ONLY) - te same dane, a dodatkowo potrafi filtrować po since (data lub wersja - tylko nowsze wpisy), area oraz ograniczać liczbę przez limit wprost na serwerze.
Powiązane
Serwer MCP
Podłącz Claude Code, Codex CLI, Codex desktop, Cursor i inne edytory AI.
Agent głosowy
Prompt systemowy, narzędzia, przekazanie człowiekowi, nagrania i ustrukturyzowane dane z połączeń.
Webhooki
Zdarzenia, dane strukturalne w treści, weryfikacja podpisu, ponowienia po błędzie.
SIP
Własny softphone, dane logowania SIP, połączenia wychodzące przez klienta SIP.
Własny agent
Podłączenie platformy zewnętrznej (ElevenLabs, Asterisk...) - bez dopłaty za agenta.