Skip to navigation

API changelog

Integration-relevant changes to the Vorbal API

View as Markdown

Check this page for public API changes that can affect your integration and the actions you need to take.

How changes are published

  • Added identifies a new, backward-compatible capability.
  • Changed identifies a contract or behavior change and includes migration guidance when needed.
  • Fixed identifies an implementation or documentation correction.
  • Deprecated identifies a supported capability that you should no longer use and includes its replacement and planned removal timeline.

Use the endpoint reference for the current contract. Use this changelog to understand how that contract differs from an earlier integration.

October 21, 2026

Added

  • Outbound trunk selection. POST /v1/sessions:dial accepts optional trunk_id to pin an eligible route and caller_id_session_uuid to present the matching caller ID from a live inbound phone Session owned by the same application. Without a pin, the platform selects the lowest-priced route with available capacity. If an eligible route is saturated, the platform tries the next eligible route. A pinned request never falls back. A missing caller ID or price match returns 503 unavailable; saturation across all eligible routes returns HTTP 529 with code unavailable, message overloaded, and Retry-After: 5. Both are rejected before monthly dial quota admission.

Changed

  • Outbound trunk catalog requires caller ID prefixes, prices, and capacity limits. When no catalog exists, the platform initializes an initial HOT route without a manual catalog write. The route allows only its configured caller ID prefixes; calls with other caller IDs remain unavailable until an operator adds an eligible route. Additional providers and their rates remain operator-configured. An existing catalog, including an empty one, is preserved. Migrate catalog fields as follows: replace caller_id_mode: "transparent" with caller_id_prefixes: ["+"]; replace a fixed caller_ids list with explicit allowed caller_id_prefixes; replace price_micros_per_minute with price_agorot_per_minute by dividing the old integer value by 10,000. The new price accepts up to four decimal places and has no micros-field alias. Add the required limits object with max_concurrent_calls, max_concurrent_setups, and max_setups_per_second from 0 through 10,000; zero disables the per-trunk rolling-second limit. Replace min_setup_interval_ms with the desired maximum number of new setup attempts in any rolling second; zero means no rate limit. The platform no longer uses the previous default outbound route. Calls without an enabled route that matches the caller ID and destination price are unavailable.

  • Application limits can cap concurrent outbound setup. Set limits.max_concurrent_dial_setups in Admin application settings from 0 through 10,000. GET /api/v1/app returns the effective value in its limits object. Clients that validate an exact set of limits fields must accept max_concurrent_dial_setups. API-key callers cannot change it through PATCH /api/v1/app. The default is 1; zero means unlimited. Pending and ringing Sessions count across all outbound routes until answer or confirmed termination. If the cap is full, a new dial returns HTTP 529 with Retry-After: 5 before monthly dial quota admission. The platform does not wait for an application setup slot.

  • Outbound call starts are asynchronous and paced. 202 Accepted confirms Session admission, but it does not confirm that the destination is ringing. The platform starts call attempts asynchronously and enforces each route’s maximum starts in a rolling one-second window. Each new initial attempt counts even if the provider does not respond. A new attempt can start while an earlier one is ringing when trunk and application setup capacity remain. Use Session events to follow call progress and completion.

October 19, 2026

Fixed

  • WebSocket Session terminal events reflect connection readiness. A standalone WebSocket Session that fails before connecting now ends with answered: false and duration_seconds: 0. After a successful connection, duration starts when your server accepts the WebSocket connection. Previously, terminal events could report an answered Session and count setup time even when the connection failed. Phone Session behavior is unchanged. Use websocket.connected to observe readiness; event names and fields are unchanged.

  • WebSocket connection events wait for your server. websocket.connected and the Session’s transition from connecting to answered now wait until your server accepts the WebSocket handshake and the connection is ready to exchange audio. Previously, they could occur before your server accepted the connection. Setup failures in DNS, TCP, TLS, or the WebSocket handshake emit websocket.failed with the existing sanitized reason and handshake status, without a preceding websocket.connected for that attempt. Use websocket.connected to confirm connection readiness; it does not prove that audio has been sent, received, or heard. Event names and payload fields are unchanged. Integrations that previously treated a transient connected followed by failure as a successful connection should handle setup failure directly.

October 18, 2026

Fixed

  • Fresh Room join requests wait for confirmed participation. While a Session is still joining a Room, a new participant-add request returns HTTP 409 with code conflict, including requests with a new idempotency key. Previously, a fresh request for the same Room could incorrectly return 200 already_joined before participation was confirmed. Retry the original accepted request with its original key to replay 202 joining; use room.participant.joined to confirm participation before sending a fresh request. Confirmed same-Room requests still return 200 already_joined.

  • Per-file events for finite Session Playback. Each URL file that starts playing now emits playback.file.ended when it completes or is stopped, including a play request with one file. The event carries session_uuid, the original play operation_uuid, the zero-based file_index in the request’s urls array, the original url, and a reason of completed or stopped. It precedes the operation’s terminal playback.stopped or playback.failed event. Files that fail or never start have no per-file ended event. If a finite file cannot be read or decoded, the operation emits playback.failed with reason: media_unavailable and no ended event for that file. Silence, live streams, and pushed audio do not emit per-file events. Consumers can track playlist progress by file while continuing to use the terminal event for the operation’s final outcome.

  • Room playback reports when each started finite file ends. The new room.playback.file.ended webhook identifies the original play operation, zero-based file_index, and caller-supplied url. Its reason is completed or stopped. It also fires for a single-file request and arrives before the operation’s terminal event. Failed files and files that never started do not produce this event. With loop: true, each completed repeat produces another event for the same file_index. Use it to track progress through a playlist; continue using room.playback.ended or room.playback.failed for the operation’s final outcome.

Changed

  • SMS quotas count one unit per 70 characters or remaining fraction. Previously, each submission attempt consumed one unit regardless of length. Quota usage now equals ceil(character_count / 70), counting Unicode code points for all text. For example, 70 characters consume one unit, 71 consume two, and 1,600 consume 23. The default monthly_messages limit remains 3,000 units. If the remaining quota cannot cover the whole message, the API returns 429 resource_limit_exceeded without consuming units or submitting it. Replays do not consume additional units, and usage recorded before this change keeps its original count. Update usage estimates for messages longer than 70 characters; one request still sends the complete text.

  • Initial monthly quota for outbound SMS. Previously, SMS had request-rate limits but no monthly cap. After the quota is exhausted, creating a message returns HTTP 429 with code resource_limit_exceeded and does not submit it. The quota resets at the start of each calendar month in Asia/Jerusalem. Application configuration responses include limits.monthly_messages; Admin limits accept an override, with 0 meaning unlimited and omission selecting the default. Applications that need more than 3,000 submissions must increase this limit. A validated, authorized request counts when admitted for submission, including later submission failures and failed deliveries. Replaying a stored outcome or retrying an in-progress request with its original idempotency key does not count again. Retrying after a definitive provider refusal, or reusing a key after its seven-day expiry, creates a new counted attempt. Invalid or unauthorized requests and quota rejections do not count. One request consumes one slot, regardless of SMS segmentation. Update clients to handle the quota response and retry after the quota resets or the limit increases. The character-based accounting above replaces this initial per-request count.

  • Monthly call and dial-attempt quotas accept 0 for unlimited usage. In Admin application limits, monthly_call_minutes: 0 removes the monthly connected-time cap for incoming and outgoing calls, and monthly_dial_attempts: 0 removes the outbound dial-attempt cap. Each setting is independent; usage continues to be counted. Previously, 0 selected the default quota of 10,000. Replace requests that used 0 to restore a default with 10000. Omitting a quota from a replacement limits configuration still selects its default; omitting the entire limits object on update preserves the existing settings. Responses include 0 for an unlimited quota.

  • Each Session has its own limit of four concurrent recordings. A fifth POST /v1/sessions/{uuid}/recordings returns HTTP 409 with code concurrent_recording_limit_reached and starts no recording. Recordings waiting to start or finish count toward that Session limit. Other Sessions and Rooms do not count; a Room separately permits one active Room Recording at a time. Previously, Session recordings had no concurrent limit. If your application starts more than four, stop one recording, wait for recording.ended or recording.failed, then retry with a new idempotency key. If a 409 persists briefly after the event, retry after release completes. Replaying an already accepted request with its original key still returns its accepted response.

October 14, 2026

Fixed

  • Session recordings retain audio from one-sided playback and the final audio fragment at stop. Audio captured while a Recording is masked stays silent after unmasking. Request and webhook schemas are unchanged; no migration is required.

Changed

  • Session recordings continue when an audio queue fills. Audio that cannot enter a full queue is dropped while the retained audio remains available. Queue saturation alone no longer causes recording.failed; after successful finalization, recording.ended can refer to a recording with missing audio from the overloaded interval. Actual capture and file-write failures still report recording.failed with no media URL. Event schemas are unchanged; consumers must not interpret recording.ended as proof that every audio sample was captured.

October 13, 2026

Changed

  • SMS delivery tracking is limited to queue acceptance after the provider switch. When this change is deployed, new accepted messages remain sending_status: queued and delivery_status: pending until the 72-hour monitoring window ends with delivery_status: unknown. The integration does not yet produce message.sent or confirmed delivered or failed updates. Unresolved messages accepted before the switch also close as unknown if they have no saved final result. Request and response schemas are unchanged. If your application previously relied on confirmed delivery, handle unknown explicitly and keep queue acceptance separate from recipient delivery. Do not resend automatically because delivery is unconfirmed. Authenticated delivery reports require further integration work.

October 4, 2026

Changed

  • Breaking: Session playback no longer accepts interrupt_digits. The old behavior let a type: files request list keys that stopped that playback automatically. The replacement is application-controlled: handle dtmf.received, then call POST /v1/sessions/{uuid}/playback/stop when the key should stop audio. The explicit stop can cancel accepted playback before playback.started; after it completes, submit the action selected by the key. Remove interrupt_digits from playback requests and move key selection into your application. The October 2 entry records the former capability.

Fixed

  • Live-stream playback startup is improved and deployed. Real HLS playback verification confirmed the startup improvement. Retrieving the source still affects when playback begins. Request and webhook schemas are unchanged; no integration change is required.

  • Explicit playback stop works before playback starts. POST /v1/sessions/{uuid}/playback/stop can cancel accepted playback before playback.started. Stop dispatch does not wait for playlist preparation to finish; media admission is released after the platform confirms the stop. The normal terminal playback event still identifies the stopped operation.

  • Completed playlists release media admission before resource cleanup. After a file playlist ends, dtmf/collect can begin while playback resources are being cleaned up, instead of returning repeated 409 conflict responses. Request and event schemas are unchanged; no migration is required.

  • Recording completion resumes after a temporary failure without another call event. Pending recording completion and Session termination now retry after the dependency recovers, including when the call has already ended. Completion webhooks retain their original identities and ordering. Request and webhook schemas are unchanged; no migration is required.

  • Discarded empty Session recordings no longer block Session termination. A recording discarded at close because it contains no audio reports recording.failed with its original recording-create operation_uuid, its recording_uuid, and no media URL. Temporary recording finalization failures still retry normally. Request and webhook schemas are unchanged; no migration is required.

October 3, 2026

Fixed

  • Replacing or stopping Session playback keeps its stop outcome after a delayed source-read failure. If the platform acknowledges the exact stop before the playback completion reports a source failure, the stopped operation now sends one playback.stopped webhook instead of playback.failed with file_not_found. A source failure without that acknowledged stop still sends playback.failed. Correlate either terminal with the original playback operation_uuid. Request and event schemas are unchanged; no migration is required.

October 2, 2026

Added

  • Session file playback can stop on selected keypad keys. Add interrupt_digits to a type: files request at POST /v1/sessions/{uuid}/playback/play to list the single DTMF keys (0–9, *, or #) that stop that playback. Other keys leave it playing. The normal Session DTMF event and playback.stopped webhook still arrive; correlate playback.stopped with the accepted playback operation_uuid. Omit the field to keep existing playback behavior. The option is unavailable for Room, push, stream, and silence playback.

Fixed

  • Inbound Session reads retain the call identity. After session.created, GET /v1/sessions/{uuid} and GET /v1/sessions return the caller_id and did reported by that event instead of empty strings. Response and webhook schemas are unchanged; no migration is required.

  • Playback completion permits the next recording command. After receiving playback.stopped, callers can stop a recording without a temporary 409 conflict caused by that completed playback. Use POST /v1/sessions/{uuid}/recordings/{recording_uuid}/stop as before. Request and event schemas are unchanged; no migration is required.

  • Room recordings can remain active after the last participant leaves. The configuration repair is deployed and verified in staging. Explicitly stopping a Room recording now produces a completed download in the verified occupied and empty-Room scenarios. Request and event schemas are unchanged; no migration is required.

  • Completed charge declines survive a lost live event. If the charge was declined and its live event was lost, Session cleanup now delivers payment.charge.completed with the sanitized decline details before session.ended. Request and event schemas are unchanged; no integration change is required.

  • SMS delivery tracking survives concurrent submissions. An accepted SMS that joins a cohort while an earlier message is being reconciled remains tracked through its final delivery result or the 72-hour unknown outcome. Message and webhook schemas are unchanged. No integration change is required.

  • Inbound calls survive routing updates. Calls admitted while routing changes propagate now continue into the normal Session flow, including session.created, instead of being silently discarded. Request and event schemas are unchanged. No integration change is required.

  • Room participant events follow the current Room visit. A delayed join signal from a previous Room, including an earlier visit to the same Room, no longer makes a Session appear joined or lets mute and unmute target another participant. Request and event schemas are unchanged; no migration is required.

  • Room playback start detection follows the current operation. A delayed start signal from a completed play can no longer confirm a subsequent play. A start signal that arrives immediately after the Room accepts its playback command is retained, so the start watchdog does not incorrectly fail a play that began. Requests, responses, and event schemas are unchanged. No integration change is required.

  • Room playback completion follows the originating operation. A delayed completion or failure signal from an earlier play no longer stops a subsequent play or sends its terminal webhook. A play that ends immediately after admission now reports its requested URLs in the terminal event. Event schemas are unchanged. No integration change is required.

October 1, 2026

Fixed

  • Inbound Session reads retain caller and destination numbers. GET /v1/sessions/{uuid} now returns canonical caller_id and did values for newly created inbound phone Sessions. Previously these fields could be empty even when the session.created webhook contained both numbers. No request or response schema changes are needed.

  • A rejected Room switch preserves the caller’s original membership. When the destination Room is full, the request returns 409 room_full without removing the caller from the original Room, changing its participant count, or emitting room.participant.left. A rejected command admission also preserves the original membership. Request and event schemas are unchanged; no migration is required.

  • Answer webhooks no longer stall with low event-delivery concurrency. Previously, a successful call answer could leave session.answered undelivered when the delivery service was configured with concurrency 4. Applications now receive the answer event in that configuration. Request and event schemas are unchanged; no migration is required.

  • Stopping a file playlist no longer waits for remaining files to load. POST /v1/sessions/{uuid}/playback/stop could previously return 202 Accepted while background preparation delayed the actual stop and its playback.stopped event. The platform now admits the stop independently of that preparation. The response remains asynchronous; use playback.stopped to confirm completion. Request and event schemas are unchanged. No migration is required.

September 30, 2026

Fixed

  • Inbound phone call setup no longer fails on affected routes. Some inbound calls could previously fail during setup because of an incorrect route selection. These calls can now connect. Request and webhook schemas are unchanged; no migration is required.

  • Hosted AAC and HLS playback accepts an explicit format. POST /v1/sessions/{uuid}/playback/play previously returned 400 with invalid_request when a type: stream request included format: "aac" or format: "hls", despite those values appearing in the API reference. The request now reaches playback admission. Applications can send either value or omit format for automatic detection; no migration is required.

September 29, 2026

Changed

  • Seeking a live stream now returns 400 invalid_request. POST /v1/sessions/{uuid}/playback/seek and POST /api/v1/rooms/{room_id}/playback/seek previously returned 409 conflict while type: stream playback was active. They now return 400 invalid_request with no seek operation admitted. Handle this response as 400 invalid_request rather than 409 conflict, and use type: files for playback that supports seek. Retrying a seek cannot make a live stream seekable.

Fixed

  • Answering an already answered call now returns 409 Conflict. Previously, a new POST /v1/sessions/{uuid}/answer request could return 202 Accepted and create an operation that never completed. New answer requests now return the stable conflict error code without creating an operation. Answer admission requires an inbound phone Session in ringing or early-media state. Retry an admitted request with its original Idempotency-Key to replay the original response; no migration is required.

September 25, 2026

Fixed

  • Stopping a Room Recording now completes promptly. After the API accepts a request to stop a Room Recording, the corresponding recording.ended event is no longer delayed by the stop operation. Recording event payloads and API schemas are unchanged. No migration is required.

  • Quiet outbound phone calls no longer end based on event silence. Previously, an active outbound phone Session with no lifecycle events for about 120 seconds could receive session.ended. The platform now treats the call as ended only after a terminal outcome, or after the configured max_duration_sec and a short close grace have elapsed; inactivity alone does not end it. Other Session types are unchanged. Event schemas and payloads are unchanged. No migration is required.

September 24, 2026

Fixed

  • Completed Recording events now link to available media. Session and Room recording.ended events are delivered only after the exact Recording has finished finalization. Previously, an event could contain a media_url that returned 404 when finalization failed after the Recording closed. Temporary finalization failures are now retried before the event is delivered, and the event is still delivered exactly once. Event payloads and API schemas are unchanged. No migration is required; applications can fetch media_url as soon as they receive recording.ended.

  • WebSocket Sessions now join Rooms before application audio starts. The last confirmed working Room join was September 11, 2026. The first confirmed failure was September 24, 2026 at 00:18 UTC: an accepted POST /api/v1/rooms/{room_id}/participants request returned 202 with status: "joining", but the WebSocket Session did not become a Room participant. Applications could write audio to the WebSocket while callers in the Room heard nothing.

    The request did not return an HTTP error or stable error code. The expected room.participant.joined webhook for the WebSocket Session was absent; other Session lifecycle events continued normally. Request, response, and webhook schemas are unchanged. No migration is required.

September 22, 2026

Changed

  • Room playback no longer reports a waiting status. After the API accepts a live stream playback request, the Room reports stopped until playback starts, then reports playing. Remove waiting from Room status handling and use the operation webhooks to track the accepted request before playback starts.

  • Messages support SMS only. POST /v1/messages no longer accepts channel: "fax", and the temporary PDF upload endpoints under /v1/files are no longer available. Remove fax requests and file uploads from your integration. SMS request, resource, and event contracts are unchanged.

  • Payment charge metadata length uses Unicode characters. The 128-character limit now counts Unicode code points instead of UTF-8 bytes. Existing ASCII input is unchanged, and names, addresses, or notes in languages such as Hebrew can use the full documented limit.

Fixed

  • Sequential DTMF collections report the terminator used by each operation. When one collection ended with # or * and the next collection on the same Session ended after reaching max_digits, the second digits.collected event could incorrectly repeat the first operation’s terminator. The second event now returns an empty terminator, matching how that collection ended. Request and event schemas are unchanged, and no integration changes are required.

  • Accepted Room live streams now report a terminal playback event. A live stream that ends emits room.playback.ended. A source failure emits room.playback.failed. Previously, either source event could leave an accepted operation without the terminal webhook documented by the endpoint. Correlate the event with the operation_uuid returned by the 202 response.

  • Generated API schemas match the values returned and accepted by the API. GET /v1/sessions/{uuid} now documents the returned lifecycle field as state; Recording creation lists only the supported direction: "both" and channels: "mono" values; and a Nedarim charge amount is documented as whole ILS. Existing runtime behavior is unchanged for these fields. Regenerate clients that use these enums or response properties after the updated OpenAPI is published.

  • The Idempotency guide documents the 24-hour replay window. It previously stated one hour and overstated which mutation endpoints support the header. Check each endpoint reference for Idempotency-Key support.

September 18, 2026

Fixed

  • Successful outbound faxes now reach their terminal Message status. After the fax result confirms that every page was transferred, the Message changes from queued/pending to sent/delivered. Its fax.pages_sent and fax.delivery_extent fields and terminal delivery webhooks now reflect the completed transmission. No integration changes are required.

September 17, 2026

Fixed

  • Recording downloads use the Vorbal recording host on standard HTTPS. New recording.became_available events use https://recordings.vorbal.dev for signed media_url and live_url values, and new recording.ended events use it for media_url. The URLs use port 443 without an explicit port. Previously, affected events used https://recordings.telekesher.dev:30900; that host stopped resolving after the branding migration, so clients received a DNS failure before any HTTP response or stable API error code.

    Update network allowlists and any host validation to accept recordings.vorbal.dev on port 443. Read the signed URL from each webhook instead of constructing or pinning its host. Request paths, signatures, expiration, and audio behavior are unchanged.

September 16, 2026

Added

  • Outbound fax and temporary PDF uploads. Send and track fax Messages using a PDF URL from the Files API or another HTTPS source. Existing SMS integrations are unchanged; the Message list remains SMS-only.

Changed

  • Fax admission uses one queued event. Fax creation now returns sending_status: "queued" instead of "accepted". Update handlers for message.accepted to use message.queued; starting transmission no longer emits another queued event. SMS behavior is unchanged. See Messages.

  • Production API host. The canonical API base URL is now https://api.vorbal.dev. Existing integrations that use https://api.telekesher.dev remain supported temporarily for backward compatibility. Request paths, authentication, and API behavior are unchanged. Update API clients and network allowlists to use https://api.vorbal.dev.

September 14, 2026

Fixed

  • WebSocket Sessions remain connected after joining Rooms. From the first affected platform revision on September 8, 2026 at 04:19 Israel time until this fix, an accepted POST /api/v1/rooms/{room_id}/participants request with status: "joining" could end the WebSocket Session about two seconds after it joined. The last known-good platform revision predates that deployment; the first confirmed failure was September 14, 2026 at 12:39 Israel time.

    The request did not return an HTTP error or stable error code. The failure appeared as a terminal event such as {"event":"session.ended","session_uuid":"00000000-0000-4000-8000-000000000101","hangup_cause":"completed","duration_seconds":0}. Request, response, and webhook schemas are unchanged. No migration is required.

September 13, 2026

Fixed

  • Playback seek validation for active live streams. Seek requests to type: "stream" Playback now return 400 invalid_request instead of being incorrectly admitted as in progress.

September 10, 2026

Changed

  • Requests that accept only server-sent events now return 406 Not Acceptable. Public API endpoints previously ignored Accept: text/event-stream and could return 200 with a JSON response. A request now returns 406 when its Accept header requests an event-stream representation without permitting an application/json response. The response has Content-Type: application/json, code invalid_request, and message Requested response format is not supported; use application/json. The request is rejected before authentication, rate limiting, or endpoint processing.

    A mixed Accept header still receives the endpoint’s normal JSON response when content negotiation permits JSON through application/json, application/*, or */* at a positive quality value. An exact application/json;q=0 range makes JSON unacceptable even if a broader wildcard has a positive quality value.

    Migration: send Accept: application/json for API responses, and remove text/event-stream if the request doesn’t accept JSON. Use signed webhooks instead of server-sent events to receive lifecycle updates.

September 7, 2026

Changed

  • Breaking: POST /v1/sessions/{uuid}/payment/charges requires merchant and customer metadata for provider: "nedarim". Three request fields are now required, and three optional fields were added:

    • required: category, customer_name, customer_address
    • optional: customer_email, customer_phone, note

    Each is a string of at most 128 characters with no control characters. The three required fields must be present but their value may be an empty string. They are forwarded verbatim to Nedarim (Groupe, ClientName, Adresse, Mail, Phone, Avour).

    Nedarim’s Online acquiring path rejects card-not-present charges that reach it from a data-center source address without customer identification, so a real customer_name and customer_address are needed for charges to clear in that environment even though the provider marks the fields optional.

    Migration: add category, customer_name, and customer_address to every charge request. Send the customer’s name and address where you have them; an empty string is accepted by the API but may be declined by the acquirer. The platform does not receive, store, or log a card number, security code, or identity number — these fields are ordinary charge metadata.

Fixed

  • Recording stops now emit recording.ended. Explicit Session recording stops again deliver their terminal webhook.

September 6, 2026

Changed

  • Breaking: all Idempotency-Key replay windows are now 24 hours. The platform now applies one retention window to every endpoint that supports Idempotency-Key. General API mutations and secure-payment charges increase from one hour to 24 hours; message creation decreases from seven days to 24 hours. Operation-Id remains a separate correlation mechanism, including its existing 10-minute duplicate-admission behavior on supported media controls.

    Reuse the same key only for the same logical request and retry it within 24 hours. After that window, the key is treated as new and the operation may run again. In particular, reconcile message and payment status before retrying an expired request, because a new submission can create another billed external side effect. Existing keys expire according to the TTL assigned when they were created; the uniform window applies to newly admitted keys.

  • Breaking: Room members are now Room participants. The Room API now uses participant terminology consistently and does not retain compatibility aliases. Update integrations as follows:

    • /api/v1/rooms/{room_id}/members → /api/v1/rooms/{room_id}/participants
    • /api/v1/rooms/{room_id}/members/{uuid} → /api/v1/rooms/{room_id}/participants/{uuid}
    • response field members → participants
    • member_count → participant_count
    • application setting limits.max_members_per_room → limits.max_participants_per_room
    • room.member.* webhooks → room.participant.*
    • volume directions to_member and from_member → to_participant and from_participant

    The Session UUID remains the participant identity, and request/response behavior is otherwise unchanged. Migrate every endpoint, field, webhook subscription, event handler, and volume-direction value before upgrading.

September 3, 2026

Changed

  • Provider-authored decline messages were removed from charge webhooks. The error.message field is no longer emitted in payment.charge.completed. Provider text can contain sensitive card data, so the platform now discards it at the payment-vault boundary. Integrations that displayed this optional field must use their own copy based on the stable error.type, error.code, and error.decline_code values instead.

  • Room member removal is now physically ordered. DELETE /api/v1/rooms/{room_id}/members/{uuid} still returns 202 with status: "leaving", but it no longer emits room.member.left or updates the room member count at request admission. The webhook and count update are committed only after the underlying media layer confirms the matching member has left the requested room. Correlate the webhook by both session_uuid and room_id; retries and duplicate media events produce at most one webhook and one count decrement. Integrations that treated the 202 response as proof of physical departure must wait for room.member.left instead.

Fixed

  • Answered Session durations no longer report zero because of a timestamp conversion error. session.ended.duration_seconds reports whole seconds from answer to termination, excluding ringing time.

  • Playback can start again after a previous playback has finished. POST /v1/sessions/{uuid}/playback/play could return 503 with code unavailable for the rest of a Session after an earlier playback completed. This also affected flows that collected digits between the two playbacks. Completed playback now releases its ownership so later playback requests can be accepted normally. Request and webhook schemas are unchanged. After the fix is deployed and verified in your environment, remove any workaround that uses dtmf/collect solely to play an announcement.

  • Live push playback emits an empty urls array when playback starts. A playback.started webhook for live push playback now returns urls: [], as required by the webhook schema. It previously returned urls: null. Live push playback has no URL-based sources, so consumers should continue to parse urls as an array. No migration is required.

  • Secure-payment terminal webhooks release collection ownership before delivery. When payment.card.collected or payment.cvv.collected becomes visible to your webhook handler, the completed secure collection no longer owns the Session’s media controls. You can immediately submit the next media or DTMF collection command from that handler without receiving a transient conflict from the completed collection.

    Webhook schemas and outcomes are unchanged. Continue to correlate events by operation_uuid, deduplicate at-least-once delivery, and reuse the same Operation-Id and Idempotency-Key for an exact request retry. No migration is required.

September 2, 2026

Changed

  • Breaking: secure-payment collections accept at most four prompt files. POST /v1/sessions/{uuid}/payment/card/collect and POST /v1/sessions/{uuid}/payment/cvv/collect previously accepted up to 128 entries in prompt_files; they now accept up to four. If a flow uses more than four prompt files, concatenate them into at most four WAV or MP3 files before submitting the collection request.

    The platform checks the complete prompt list within one eight-second deadline and applies an application-wide admission budget to these outbound checks. A burst that exceeds the budget returns 429 Too Many Requests with Retry-After. Retry later with the same Operation-Id and, if you supplied one, the same Idempotency-Key.

  • New idempotency keys have an application-wide allocation budget. Exact retries of an existing Idempotency-Key do not consume this budget. A burst of high-cardinality new keys can return 429 Too Many Requests with Retry-After. Reuse one key for every retry of the same logical request and create a new key only for a new operation. Rejected requests do not retain a replay result; an uncertain server failure remains protected from duplicate execution.

  • Payment state is retained for at most one hour. The platform previously retained some sanitized collection, charge-recovery, dispatch, and deduplication records for up to 24 hours. It now applies the same one-hour limit to all payment-specific records, including the Idempotency-Key request identity and sanitized HTTP result. State transitions don’t restart the one-hour window, and delayed internal processing can’t recreate an expired payment workflow.

    Reuse the same Idempotency-Key and Operation-Id for exact retries only during the one-hour window. After it expires, the HTTP result is no longer replayable and the old operation is no longer available for recovery or dispatch fencing. Reconcile the terminal payment.charge.completed webhook before taking further action. If you intentionally start another charge, collect new card and CVV references and use a new Operation-Id and Idempotency-Key.

Fixed

  • Outbound session.created classification. Sessions created by POST /v1/sessions:dial now emit session.created with type: "phone_out", the originating number in caller_id, and the destination number in did. These events previously reported type: "unknown" with empty caller_id and did values. Inbound session.created behavior is unchanged.

August 31, 2026

Added

  • Secure card payments. Use POST /v1/sessions/{uuid}/payment/card/collect and POST /v1/sessions/{uuid}/payment/cvv/collect to receive call-scoped references without receiving card or CVV digits. A successful payment.card.collected webhook includes a card_uuid reference starting with card_ and safe display values under payment_method_details.card.last4 and .brand. A successful payment.cvv.collected webhook includes a cvv_uuid reference starting with cvv_. Invalid input produces a sanitized outcome, such as reason: "luhn_check_failed", without disclosing the rejected digits.

    Secure collection and Room membership are mutually exclusive. Starting collection for a Session that is a Room member, or adding a Session to a Room while collection is active, returns 409 Conflict. While collection is active, the platform also rejects new Recordings and unmask requests. After the terminal collection webhook arrives, each Recording that the platform masked or paused remains in that state; call the Recording’s unmask endpoint when the application is ready to resume writing sensitive-free audio.

    Submit the opaque references to POST /v1/sessions/{uuid}/payment/charges while the Session is connected. Include exp_month, exp_year, a positive amount in major currency units, lowercase currency: "ils", provider: "nedarim", and the customer’s seven-digit terminal_number. For ILS, amount may have at most two fractional digits; excess precision is rejected rather than rounded. Providers can impose narrower limits. Nedarim currently accepts only whole-ILS charges, so use a whole number such as amount: 18 with that provider.

    Idempotency-Key is optional. Supply one unique key per logical charge and reuse it for retries to receive one-hour HTTP replay protection. Omitting the key disables retry deduplication, so repeating the request can create another charge attempt.

    A charge’s first provider attempt atomically consumes its card_uuid and cvv_uuid. An exact replay with the same Idempotency-Key returns the original sanitized HTTP result and terminal outcome. A new operation cannot reuse either reference, so collect new card and CVV references before another charge attempt. An unused reference expires no later than one hour after collection, and Session cleanup normally removes it sooner.

    The terminal payment.charge.completed webhook reports the trusted amount, currency, provider, terminal number, status, and retryability. A successful result can also include the transaction creation time, receipt_created, validated card details, installment count, and normalized transaction type, card classification, issuer, and acquirer. Use status and the originating operation_uuid to correlate the outcome. The webhook excludes provider transaction identifiers, authorization codes, voucher numbers, UIDs, raw responses, provider codes, PAN, CVV, expiry, identity numbers, customer PII, receipt URLs, and bearer capabilities. A decline can include an error object with Stripe-compatible type, code, and decline_code values when a safe closed mapping is known. Provider-authored text is discarded and is never included in the webhook. Observed Nedarim error 004 maps to card_error, card_declined, and generic_decline; the raw provider code is not exposed.

    The platform durably stages each accepted collection result. A service interruption or Session end completes the staged result, or emits a terminal failed result if collection cannot be recovered. A charge timeout is reported as indeterminate with retryable: false and is never automatically retried.

Changed

  • Nedarim terminals must be assigned to the calling application. Continue sending the customer’s seven-digit terminal_number with every charge; the value is not a secret and is not replaced by a server-side default. The platform now verifies that the terminal is assigned to the authenticated application before accepting the charge. An unassigned terminal returns 403 Forbidden without creating a charge operation. Ask the platform operator to assign each customer’s terminal before sending its first charge.

  • Every accepted charge retains a terminal outcome across Session end. If the Session hangs up or a service restarts before provider processing begins, the platform emits a terminal failed outcome. If provider submission already began and the result cannot be proven, it emits indeterminate with retryable: false; a stored sanitized completion remains replayable. Do not create a replacement charge merely because the Session ended. Wait for and deduplicate payment.charge.completed by operation_uuid.

Fixed

  • Standalone WebSocket lifecycle webhook delivery. The platform again delivers websocket.connected, websocket.disconnected, websocket.failed, and websocket.audio_playback_completed for standalone WebSocket Sessions. The last confirmed working configuration was August 18, 2026 at 01:31 Israel time; the first affected configuration was August 21, 2026 at 04:30 Israel time. During that interval, these events could be omitted before webhook delivery, so there was no webhook HTTP response or stable error code. For example, an expected redacted event such as {"event":"websocket.disconnected","session_uuid":"00000000-0000-4000-8000-000000000101"} could be absent. Missed historical events are not replayed. No request or webhook schema changed, and no migration is required; continue normal webhook handling and at-least-once deduplication.

August 30, 2026

Added

  • Optional inclusion of recording gaps. POST /v1/sessions/{uuid}/recordings now accepts the optional boolean include_gaps, which defaults to false. Omit it or set it to false to remove redacted periods and periods without recordable audio from the artifact. Set "include_gaps": true to include those periods as silence. The setting applies only to that Recording and is returned by the Recording list and inspection endpoints. Existing requests do not need to change.

  • Session hold-state webhooks. Applications now receive session.held when a Session enters hold and session.resumed when it returns from hold. Both events carry session_uuid in addition to the standard webhook fields. Existing integrations do not need to change. Add either event to your webhook handler when your application needs to track whether a Session is on hold.

  • MP3 file playback. Session Playback, Room Playback, and optional DTMF collection prompts now accept finite HTTP or HTTPS MP3 URLs in addition to WAV URLs. Submit the original .mp3 URL in urls or prompt_files; no client-side conversion or alternate filename is required. Existing WAV integrations are unchanged.

  • Opt-in Room voice-activity events. POST /api/v1/rooms now accepts the optional boolean voice_activity_events. Set it to true when the Room must emit room.member.voice_activity_changed as members start and stop talking. It defaults to false, so existing Room creation requests continue to work without receiving these high-frequency events. The Room resource reports the selected value. The event was previously documented without an activation control, although Rooms did not emit it. If your integration was built from that earlier documentation and expects voice-activity events, add "voice_activity_events": true when creating each Room; existing Rooms must be replaced with Rooms created with the option enabled.

  • Room-member volume results. POST /v1/sessions/{uuid}/room/volume now reports its asynchronous result through exactly one terminal webhook. room.member.volume_changed confirms that every requested direction was applied. room.member.volume_change_failed reports an execution failure and includes applied_directions and failed_directions, so a direction: both request can report a partial result without implying an atomic two-direction change. Both events include the submitted volume and direction, plus the session_uuid, room_id, and originating operation_uuid.

  • Reject inbound calls or cancel outbound dialing before answer. Use POST /v1/sessions/{uuid}/reject with a required reason of number_not_found, invalid_number, timeout, busy, or rejected for an unanswered inbound phone call. Use POST /v1/sessions/{uuid}/cancel to stop an outbound phone call before the callee answers. Both operations return 202 Accepted, followed by the existing session.ended webhook. For a rejection, that webhook reports the selected reason unchanged as hangup_cause. Both operations return 409 Conflict for an incompatible Session or lifecycle state. Use DELETE /v1/sessions/{uuid} when you want to disconnect regardless of answer state.

Changed

  • Session Playback request variants are now named separately in OpenAPI. The POST /v1/sessions/{uuid}/playback/play request remains one oneOf contract, with named schemas for file playback, pushed audio, and silence. Generated-audio uploads use SessionPlaybackPushRequest. This schema-organization change does not alter endpoint URLs or wire payloads beyond the changes documented separately below, and requires no additional migration.

  • Breaking: generated-audio Session Playback uses type: push and an HTTPS streaming upload. POST /v1/sessions/{uuid}/playback/play now distinguishes audio pushed by the application from live HTTP/HTTPS URL sources. Send type: push with the raw audio format. The 202 Accepted response includes an ingest object with the short-lived URL, required HTTP method and headers, and expiry. Follow that object to stream raw mono audio in one request body. Authenticate the ingest request with the same application Bearer credentials that you use for other Voice API requests. The response does not repeat those credentials in ingest.headers. There is no compatibility alias: a Session request that uses type: stream with media_format now returns 400 Bad Request.

    Previous Session requestReplacement Session request
    {"type":"stream","url":"https://media.example.com/live"}{"type":"push","media_format":{"encoding":"pcm_s16le","sample_rate":24000}}
    {"type":"stream","media_format":{"encoding":"pcm_s16le","sample_rate":24000}}{"type":"push","media_format":{"encoding":"pcm_s16le","sample_rate":24000}}

    Replace flat upload metadata with the nested ingest contract:

    Previous 202 fieldsReplacement 202 field
    "upload_url":"https://...","expires_at":"2026-08-30T19:00:00Z""ingest":{"url":"https://...","method":"POST","headers":{"Content-Type":"application/octet-stream"},"expires_at":"2026-08-30T19:00:00Z"}

    The initial documentation described the upload as a WebSocket and later as a streaming PUT. Replace either transport with the returned ingest contract:

    Previous upload transportReplacement
    Connect to wss://... and send binary WebSocket messagesSend the returned streaming POST with application Bearer authentication and every returned header
    Send a streaming PUT to upload_urlSend the returned streaming POST to ingest.url with application Bearer authentication and every entry from ingest.headers

    Supported formats are little-endian PCM16 at 8, 16, 24, or 48 kHz and G.711 mu-law at 8 kHz. The body is unframed raw audio. Close it after the final audio bytes. A 2xx ingest response confirms that the platform accepted the complete request body; it does not confirm that the caller heard all audio. The Voice API verifies the Playback operation before it accepts the upload. Do not send Voice API credentials to any URL other than the Voice API origin that your integration is configured to use.

    type: stream remains the discriminator for live HTTP/HTTPS URL playback, such as radio. Room Playback keeps its {"type":"stream","url":"https://..."} request shape; live Room streams remain unsupported and return 502. This entry supersedes the earlier August 22 statement that live streaming upload was not part of the contract.

  • Breaking: Session recordings are addressable resources. A Session can now own multiple concurrent recordings, including recordings with identical settings. Each accepted create request returns a new recording_uuid, and subsequent inspection and control requests address that exact Recording. There is no singleton Recording, implicit “current recording,” or stop-all operation. Update endpoint usage as follows:

    Previous endpointReplacement
    POST /v1/sessions/{uuid}/recording/startPOST /v1/sessions/{uuid}/recordings
    POST /v1/sessions/{uuid}/recording/stopPOST /v1/sessions/{uuid}/recordings/{recording_uuid}/stop
    POST /v1/sessions/{uuid}/recording/maskPOST /v1/sessions/{uuid}/recordings/{recording_uuid}/mask
    POST /v1/sessions/{uuid}/recording/unmaskPOST /v1/sessions/{uuid}/recordings/{recording_uuid}/unmask

    The old endpoints have been removed without compatibility aliases. Persist the recording_uuid returned by the create request and use it for later commands. Use GET /v1/sessions/{uuid}/recordings to reconcile active or stopping recordings, or GET /v1/sessions/{uuid}/recordings/{recording_uuid} to inspect one live Recording. These endpoints do not provide recording history.

    Create requests now require direction (both, session_in, or session_out) and channels (mono or stereo). The initial release accepts only {"direction":"both","channels":"mono"}; other valid combinations return HTTP 400 as unsupported.

    Voice-activity events remain optional on each create request and default to disabled. Each Recording configures voice-activity detection independently, so multiple concurrent Recordings may enable events with different voice_activity_config values. Each Recording receives its own events, correlated by recording_uuid and the originating operation_uuid. Stopping one Recording stops only its events and does not affect the other active Recordings.

  • Breaking: WebSocket failure reason renamed. The websocket.failed webhook now reports reason: connection_precondition_failed instead of relay_precondition_failed when the platform cannot satisfy a prerequisite for opening the WebSocket Session connection. Update enum validation and failure handling to use the new value; there is no compatibility alias. The event name, error bucket, HTTP status semantics, and other reason values are unchanged.

  • Room-wide webhook ordering. The platform now delivers all room.* events for one Room in production order. Failures block only that Room. Payloads are unchanged, and no migration is required.

  • Breaking: Room member volume directions. The direction field in POST /v1/sessions/{uuid}/room/volume requests and the corresponding direction, applied_directions, and failed_directions webhook fields now use names relative to the Room member. Rename in to from_member and out to to_member; both is unchanged. from_member is the member’s audio sent into the Room, while to_member is the Room mix sent to the member. WebSocket media-format fields remain from_session and to_session because those directions are relative to the Session.

  • Silence playback admission is fully asynchronous. POST /v1/sessions/{uuid}/playback/play with type: silence now returns 202 Accepted after durable workflow admission, without waiting for the earlier playback stop to complete. The platform still stops existing playback before starting indefinite silence, but performs that sequence in the background and reports progress or failure through the existing playback webhooks. Treat 202 as admission only. A silence request no longer returns 503 solely because confirmation of the preceding playback stop was not received during the HTTP request. The background replacement now waits for the preceding playback to finish stopping before it starts the replacement; this fixes replacements that could be accepted and report playback.started while remaining silent. Rapid successive replacements continue to be accepted and converge on the latest request. No request, response, or webhook schema changed, and applications do not need to migrate.

  • Playback start webhook timing. For file playback, the platform now publishes playback.started at a later point in startup, using a best-effort estimate that playback of the first source has begun. Previously, the event followed an earlier source-preparation signal.

Fixed

  • One logical terminal Session event per call. The platform now creates one session.ended event when multiple termination signals or recovery checks observe the same call ending. The event uses the public session_uuid; an internal call identifier is no longer emitted as a separate Session event. The event schema is unchanged, and no migration is required. Webhook delivery remains at least once, so continue to deduplicate deliveries by (aud, Webhook-Id).

  • Terminal event after unrecovered playback media failure. If playback media fails after a request is accepted, the platform first attempts to recover it. A successful recovery remains transparent. If recovery is exhausted, the platform now emits the operation’s existing terminal event: playback.failed with reason: media_unavailable for Session playback, room.playback.failed for Room playback, or digits.collected with status: failure for prompt-and-collect. Failures before acceptance remain synchronous HTTP errors and do not emit these webhooks. Treat webhook delivery as at least once and deduplicate by (aud, Webhook-Id).

August 29, 2026

Changed

  • Breaking: recording media URL renamed. Recording API responses and webhook payloads now expose media_url instead of pull_url. Update your response and webhook models to read media_url; there is no compatibility alias. The URL behavior and expiry are unchanged: a GET starts at byte zero and follows the growing recording until EOF. After finalization, the same URL serves a finite, range-capable WAV. live_url is unchanged and continues to join near the current position.

August 28, 2026

Added

  • SMS Messages API. Send outbound SMS with POST /v1/messages, retrieve a message’s latest status with GET /v1/messages/{message_uuid}, and list messages with GET /v1/messages. Use Message webhooks for status updates.

Changed

  • Breaking: retry timing moved to the standard response header. Public HTTP error response bodies no longer include the top-level retry_after field. Retry timing remains available in the standard Retry-After response header for 429 rate-limit and 529 overload responses. Those JSON bodies now keep the uniform code and message shape, and their human-readable messages end with See Retry-After header.

    Update your response model and retry handling as follows:

    ResponsePrevious bodyNew bodyMigration
    429 Too Many Requests{"code":"rate_limited","message":"Command rate limit exceeded","retry_after":1}{"code":"rate_limited","message":"Command rate limit exceeded. See Retry-After header."}Remove retry_after from the body model. Read the delay from the Retry-After response header, wait at least that many seconds, and then retry.
    529 Site Overloaded{"code":"unavailable","message":"overloaded","retry_after":5}{"code":"unavailable","message":"overloaded. See Retry-After header."}Remove retry_after from the body model. Read the delay from the Retry-After response header, wait at least that many seconds, and then retry.

    Continue to branch on the stable code, not the human-readable message. This change does not remove unrelated error fields: playback source failures retain detail, and unexpected 500 responses retain support_id.

Fixed

  • Session command bodies with idempotency keys. Session commands that send a nonempty JSON request body with Idempotency-Key no longer reject the valid body as invalid JSON.

August 27, 2026

Added

  • Read application configuration. GET /api/v1/app returns assigned inbound phone numbers, permitted outbound caller IDs, webhook URL, blocked webhook events, and effective Room limits. Use inbound_phone_numbers and outbound_caller_ids to populate your own number-assignment interface. The GET operation is read-only: it returns assigned numbers and configuration but does not assign numbers. Existing integrations do not need to change.

Changed

  • Breaking: application configuration replaces webhook-specific routes. Replace GET /api/v1/app/webhook with GET /api/v1/app, and replace PUT /api/v1/app/webhook with PATCH /api/v1/app. Use the PATCH request to set webhook_url. A successful update returns 204 without a response body. Use the generic GET operation to verify the saved setting. The removed routes have no compatibility aliases.

  • Breaking: HTTP response fields standardized. All Voice Platform HTTP API responses, including successful and error responses, no longer include the transport-level success field. Use the HTTP status code to determine whether a request succeeded or failed, and remove success from your response models and checks.

    Error responses now use top-level code and message fields. Rename the previous error string to message; for example, change {"success":false,"error":"...","code":"invalid_request"} to {"code":"invalid_request","message":"..."}. Unexpected-error fields are also top-level, so change {"error":{"code":"internal_error","message":"...","support_id":"..."}} to {"code":"internal_error","message":"...","support_id":"..."}. At the time of this change, detail and retry_after remained available when applicable. Genuine domain lifecycle status fields are unchanged.

  • Breaking: specific Authorization header error code. A missing or malformed application Authorization header now returns HTTP 400 with code invalid_authorization_header instead of invalid_request. Update any error handling that matches these header failures by code. The message identifies whether the header is missing or invalid and shows the expected format: Bearer <app_uuid>:<api_key>. Use the app UUID and API key issued to your application. A credential with that structure but a rejected API key remains HTTP 401 unauthorized; other invalid requests remain invalid_request.

  • Breaking: one request-validation error per response. Request-schema validation failures now return exactly one issue with top-level code and message fields. The message identifies the relevant request path and explains how to correct it. For example: {"code":"invalid_request","message":"The \"to\" phone number must match E.164 format"}.

    Validation responses no longer include success, error, body status, detail, errors, param, location, or value. Read code and message and stop parsing detail.errors. When a request has multiple issues, fix the returned issue and retry to reveal a later issue. The platform selects the returned issue deterministically according to the product contract, not the validator’s raw finding order.

  • JSON request header compatibility. Continue to send Content-Type: application/json with JSON request bodies. For compatibility, the platform now tolerates a missing or blank Content-Type header and attempts JSON parsing. This does not add another supported request media type. An explicitly supplied unsupported or malformed media type is still rejected with HTTP 415.

  • Clearer Dial validation errors. Invalid request bodies now return 422 application/json with one readable message. Runtime phone-policy errors remain 400 invalid_request.

August 26, 2026

Changed

  • Choose when recording starts. POST /v1/sessions/{uuid}/recording/start now accepts the optional boolean start_only_after_answer. Omit it or set it to false to start as early as media is available; the recording may include ringing or early media. Set it to true to wait for canonical answer.

August 24, 2026

Changed

  • Breaking: standalone WebSocket event names. Standalone WebSocket Session lifecycle events now use the public websocket.* namespace instead of the removed sidecar relay’s internal name. Update webhook filters as follows:
    • websocket_relay.connected → websocket.connected
    • websocket_relay.disconnected → websocket.disconnected
    • websocket_relay.failed → websocket.failed
    • websocket_relay.audio_playback_completed → websocket.audio_playback_completed This pre-launch change has no compatibility aliases. Payload fields and delivery behavior are unchanged.

August 23, 2026

Added

  • G.711 mu-law relay audio. WebSocket relay media_format.from_session and media_format.to_session now independently accept encoding: "mulaw" with the required sample_rate: 8000. Binary messages contain raw mono G.711 mu-law bytes without a WAV header, JSON, or base64 wrapping. Existing pcm_s16le integrations do not need to change. This adds the audio encoding only; it does not implement the Twilio Media Streams message protocol.
  • Custom WebSocket ports. External WebSocket URLs now accept any TCP port from 1 through 65535 for both ws:// and wss://. Ports other than the scheme defaults must be explicit. You can omit the port: ws:// defaults to 80, and wss:// defaults to 443. This applies to the websocket object accepted by POST /v1/sessions:connectWebsocket. You can connect a standalone WebSocket Session directly to a public server on a custom port, such as wss://media.example.com:8443/audio. This change supersedes the August 21 requirement to include an explicit default port.

Changed

  • Breaking: sidecar WebSocket relay removed. The API no longer registers POST /v1/sessions/{uuid}/websocket-relay, DELETE /v1/sessions/{uuid}/websocket-relay, or the POST routes ending in /pause, /resume, /mute, /unmute, and /flush. Requests to those paths return 404, and the operations are absent from the OpenAPI endpoint reference. This pre-launch removal has no compatibility alias or deprecation period. For passive call audio, use the Recording API and its signed media_url or live_url. For a bidirectional agent, create a standalone WebSocket Session and add it with the phone Sessions to a Room. For one-way audio, use Session or Room Playback. Live streaming upload is not part of the current contract.

  • Breaking: Finite-file requests to POST /api/v1/rooms/{room_id}/playback/play now require type: "files". The endpoint no longer treats an omitted type as files; requests without type return HTTP 400 with code invalid_request. Change {"urls":[...]} to {"type":"files","urls":[...]}. Requests for a live stream continue to require type: "stream" and url.

  • Breaking: explicit request choices. The API no longer supplies default values for the non-boolean request fields below. Requests that omit a required field return HTTP 400 with code invalid_request. To preserve the previous behavior, send the former value explicitly:

    RequestPreviously omittedSend explicitly
    A key action in POST /v1/sessions/{uuid}/dtmf/sendduration_ms"duration_ms":250, or your selected duration
    POST /v1/sessions/{uuid}/dtmf/collectDigit limits, timeouts, or terminators"min_digits":1, "max_digits":10, "first_digit_timeout_ms":5000, "between_digits_timeout_ms":2000, and "terminators":["#"]

    For DTMF collection, an explicit "terminators":[] now disables terminators. Send "terminators":["#"] to preserve the previous behavior. Boolean fields whose omitted behavior is false remain optional. This includes enable_voice_activity_events, muted, loop, and start_muted. Send true only when you want to enable the corresponding behavior.

August 21, 2026

Added

  • HTTP/2 support. The public Voice API edge now negotiates HTTP/2 over TLS with ALPN and keeps HTTP/1.1 connections reusable. Existing HTTP/1.1 integrations do not need to change requests or authentication. Clients can use HTTP/2 multiplexing or reuse one HTTP/1.1 connection instead of opening a connection per request. HTTP/3 is not supported by this edge.

Changed

  • Breaking: playback source errors. Synchronous 502 failures while admitting a playback or DTMF prompt source now return code: playback_source_error instead of the generic code: unavailable. The response includes bounded diagnostic detail:
    • reason is one of unsupported, not_found, access_denied, timeout, unreachable, rejected, invalid_response, or preparation_failed.
    • source_index identifies the failing zero-based item when the request contains multiple URLs.
    • retryable says whether the same source may succeed if retried later.
    • upstream_status contains the source origin’s HTTP status when one was received. Update integrations that branch on code: unavailable for these 502 responses to branch on playback_source_error and use detail.reason for handling. The API does not return source URLs, credentials, raw upstream response bodies, filesystem paths, or media-engine details.

Fixed

  • Plaintext webhook and WebSocket URLs. Webhook delivery URLs accept both http:// and https://, and WebSocket relay URLs accept both ws:// and wss://. Plaintext schemes do not require an operator opt-in flag. Relay URLs must include a nonempty path. At the time of this change, relay URLs also required an explicit default port. The August 23 change supersedes that port requirement. Existing https:// webhook and wss:// relay configurations do not need to change. Use https:// and wss:// unless you explicitly accept exposing webhook payloads and signature headers, or call audio and relay connection headers, without transport encryption. Both URL types remain subject to address-safety checks; the host must resolve to a permitted public address.

August 20, 2026

Added

  • WebSocket relay failure reasons. The websocket_relay.failed webhook event now carries a reason field: a closed, stable enum that classifies why the relay could not be established. It is delivered alongside the existing error and http_status fields, which are unchanged. Existing consumers that only read error and http_status do not need to change. The reason values are:
    • session_codec_unsupported — the active call is not carried in an audio codec the relay supports. This is a platform-side precondition, not a problem reaching your WebSocket server.
    • destination_unreachable — the target WebSocket server could not be reached (DNS resolution, TCP connection, or routing failed).
    • tls_failed — the TLS handshake to the target WebSocket server failed.
    • handshake_rejected — the target returned a non-success HTTP status to the WebSocket upgrade request.
    • relay_precondition_failed — a precondition for starting the relay was not met. Switch on reason for actionable handling rather than parsing error. Treat session_codec_unsupported as a platform-side precondition failure, not a network problem on your side.

Fixed

  • Faster finite WAV playback start. Session Playback for finite PCM WAV files now starts without first reading the complete file. Existing playback requests do not need to change; playback.started may arrive sooner, especially for larger files or files hosted on slower origins.

August 19, 2026

Changed

  • Breaking: caller-supplied operation IDs. For applicable asynchronous commands, move caller-supplied correlation from body or query operation_uuid to the optional Operation-Id header. Responses and webhooks continue to use operation_uuid; Idempotency-Key remains separate for retry deduplication.

Fixed

  • Sidecar WebSocket playback limitation. The API reference now documents a playback limitation: you cannot play a file while a sidecar WebSocket is connected to the same Session. We are working to remove this limitation. Disconnect the sidecar WebSocket before starting Session Playback. To play both sources at once, add a standalone WebSocket Session and the phone Session to the same Room, then use Room Playback.
  • JSON route errors. Unknown public paths and unsupported HTTP methods on known public paths now return the standard JSON error envelope with Content-Type: application/json instead of plain text. A route-level 404 uses code: not_found; a 405 uses code: invalid_request. Integrations that special-cased these plain-text responses can now parse them like other API errors and branch on code.

August 15, 2026

Changed

  • Breaking: Standard Webhooks signing. Webhook delivery now follows Standard Webhooks with Ed25519. Update delivery headers and verification as follows; the former headers and signing input are no longer sent or accepted:
    • Idempotency-Key (delivery) → Webhook-Id
    • signature timestamp field t= → Webhook-Timestamp
    • X-Webhook-Signature: t=...,akid=...,v1a=... → Webhook-Signature: v1a,...
    • X-Webhook-App-UUID → required signed body claim aud
    • v1a\n<t>\n<app_uuid>\n<idempotency_key>\n<raw_body> → <webhook-id>.<webhook-timestamp>.<raw-body> Require aud to equal the configured application UUID after signature verification and before side effects. During key rotation, Webhook-Signature can contain two space-delimited v1a signatures; accept when at least one verifies against the active public-key set. The incoming API request header Idempotency-Key is unchanged.
  • Breaking: WebSocket relay resource. The per-session WebSocket relay is now a singleton resource. Update request URLs and methods as follows; the former command routes are no longer accepted:
    • POST /v1/sessions/{uuid}/websocket/start → POST /v1/sessions/{uuid}/websocket-relay
    • POST /v1/sessions/{uuid}/websocket/stop → DELETE /v1/sessions/{uuid}/websocket-relay
    • POST /v1/sessions/{uuid}/websocket/{pause|resume|mute|unmute|flush} → POST /v1/sessions/{uuid}/websocket-relay/{pause|resume|mute|unmute|flush}
  • Relay deletion requests. Send relay deletion without a request body. To supply your own correlation identifier, send the optional request header Operation-Id: <lowercase-uuid-v4>; otherwise, the platform generates one. The 202 Accepted response returns the selected value in its operation_uuid field. Operation-Id is the only request input that selects this correlation value; unrelated query parameters receive no special handling. Continue to use Idempotency-Key separately when retrying the same deletion.
  • Breaking: WebSocket text messages. Custom text messaging over the WebSocket relay is no longer supported in either direction. The removed final_text deletion property has no replacement, and websocket.message_received is removed without a replacement event. Exchange application media as binary audio frames only. A backend text frame that is not a platform-reserved relay control closes the WebSocket connection with code 1003; reserved controls are internal and are not an application extension point.
  • WebSocket relay event names. WebSocket relay lifecycle events moved from the generic websocket namespace to the relay resource namespace. Update webhook filters as follows:
    • websocket.connected → websocket_relay.connected
    • websocket.disconnected → websocket_relay.disconnected
    • websocket.failed → websocket_relay.failed
    • websocket.audio_playback_completed → websocket_relay.audio_playback_completed
  • Superseded: webhook delivery IDs. This intermediate pre-launch contract moved the platform-generated delivery key from X-Idempotency-Key to Idempotency-Key. Under that contract, the key value and Ed25519 signing preimage were unchanged, and the fourth preimage line remained the unmodified delivery-key value. The delivery header was distinct from the client-generated Idempotency-Key on supported API mutation requests. The Standard Webhooks change above superseded this contract; use Webhook-Id for the final delivery header.

August 12, 2026

Added

  • Room Playback correlation. Every Room Playback command now accepts an optional client-generated lowercase UUID v4 as operation_uuid. When supplied, the API echoes that exact value in the command’s 202 Accepted response; when omitted, the platform generates one. Use it for correlation, not request deduplication.
  • Idempotent Room Playback pause and stop. POST /api/v1/rooms/{room_id}/playback/pause and POST /api/v1/rooms/{room_id}/playback/stop now support Idempotency-Key, completing idempotent retry support across Room Playback commands. Keep operation_uuid stable for correlation and reuse Idempotency-Key separately when retrying the same request.

Changed

  • Breaking: WebSocket media format. WebSocket relay requests now require a media_format object for each audio direction. This applies to POST /v1/sessions/{uuid}/websocket/start and the websocket object accepted by POST /v1/sessions:connectWebsocket. Replace the removed fields as follows:
    • from_session_audio_rate → media_format.from_session.sample_rate
    • to_session_audio_rate → media_format.to_session.sample_rate
  • Required PCM encoding. Add encoding: "pcm_s16le" to both directions. Both directions, encodings, and sample rates are required, with no defaults. Sample rates must be multiples of 8000 from 8000 through 192000. Old flat fields and other encodings are rejected; additional encodings are planned but not yet accepted.

Fixed

  • Room Playback terminal correlation. room.playback.ended and room.playback.failed now always carry a non-empty, lowercase UUID v4 operation_uuid for the originating play request. The platform suppresses a malformed terminal event instead of delivering one whose correlation identifier is missing or invalid. Consumers may validate this required field as a UUID.
  • WebSocket audio wire format. Both directions use raw signed 16-bit little-endian mono PCM in binary frames. Send samples without a WAV header and decode received frames at the configured sample rate.

August 11, 2026

Added

  • Promptless DTMF collection. DTMF collection can now run without prompt playback. Omit prompt_files from POST /v1/sessions/{uuid}/dtmf/collect to collect input without playing audio first. Existing requests with prompt_files still play those prompts before collecting input.

Changed

  • WebSocket connection headers. The WebSocket relay request property headers is now connection_headers for POST /v1/sessions/{uuid}/websocket/start and the websocket object accepted by POST /v1/sessions:connectWebsocket. Update serialized request bodies; headers is no longer accepted.

  • Reserved WebSocket headers. connection_headers accepts only customer-controlled HTTP headers. Requests are rejected when the map contains User-Agent, Origin, Host, Connection, Upgrade, Sec-WebSocket-*, Content-Length, Transfer-Encoding, Proxy-*, Forwarded, X-Forwarded-*, X-Real-IP, or CF-Connecting-IP, case-insensitively. The platform sends User-Agent: Voice-Platform/1.0 and derives Origin from the target WebSocket URL.

  • Operation resource removed. The API no longer models command acknowledgements as a separate Operation resource. operation_uuid is unchanged in requests, responses, and webhooks; it remains an opaque token for correlating lifecycle events.

August 10, 2026

Added

  • Send DTMF. Applications can send ordered DTMF keypad presses and timed waits to the remote party on an answered phone session with POST /v1/sessions/{uuid}/dtmf/send. Only one sequence can run per session; ringing calls and standalone WebSocket sessions are unsupported. 202 Accepted is the sole admission acknowledgement and does not confirm remote handling.
  • Cancel DTMF. Applications can cancel the active outbound DTMF sequence with DELETE /v1/sessions/{uuid}/dtmf/send. A key already submitted for transmission may finish, but later keys and waits are discarded. 202 Accepted is the sole cancellation acknowledgement; no terminal webhook is emitted.

Changed

  • Command admission webhooks. command.*.accepted admission webhooks are no longer emitted. Treat 202 Accepted as the sole admission acknowledgement and use later domain lifecycle webhooks only for observed state changes or outcomes. Remove command.*.accepted filters; correlate lifecycle events through operation_uuid when present.
  • DTMF collection routes. DTMF collection routes moved to the Keypad API namespace. Update request URLs as follows; the former routes are no longer accepted:
  • Recording silence duration. Session recording voice-activity detection now accepts silence_duration_ms up to 3,600,000 milliseconds (one hour), increased from 10,000. Existing values remain valid. Use the larger range to tolerate extended quiet periods before recording.speech.ended.

Fixed

  • WebSocket relay restart. Starting a WebSocket relay after websocket.disconnected or websocket.failed now reliably creates a fresh relay instead of failing on a remnant of the previous connection. Applications can retry POST /v1/sessions/{uuid}/websocket/start after either event without first sending a stop command.
  • WebSocket URL terminology. WebSocket relay URLs now consistently document ws:// and wss:// as accepted schemes and call the remote endpoint the WebSocket server. This is a documentation correction; existing relay URLs and the backend mute-leg value are unchanged.

August 7, 2026

Added

  • Webhook delivery URL. Applications could read and update their own webhook delivery URL with GET /api/v1/app/webhook and PUT /api/v1/app/webhook. Sending null or an empty string disabled delivery. These routes were replaced on August 27 by GET /api/v1/app and PATCH /api/v1/app.
  • Health endpoints. Unauthenticated GET /health and GET /livez are now part of the public contract. Use /health for dependency readiness and /livez only for process liveness.

Changed

  • Webhook delivery window. Webhook events more than five minutes old at delivery time go to the dead-letter queue without an HTTP attempt. After a prolonged endpoint outage or delivery backlog, reconcile current resource state instead of assuming every lifecycle event will arrive. See Webhook delivery.
  • Session base path. The canonical API base URL is https://api.telekesher.dev. Session endpoints moved from /api/v1/sessions... to /v1/sessions.... Remove the /api segment from constructed Session request URLs.

August 6, 2026

Changed

  • Session event names. Session webhook names now use the session resource namespace: call.created, call.answered, call.ringing_started, call.early_media_started, and call.ended became their corresponding session.* events. Update event filters and webhook dispatch logic to use the new names.
  • Room Playback terminal events. Terminal room playback events are now room.playback.ended for natural completion or an API stop, and room.playback.failed for errors and timeouts. The former room.playback.finished is no longer used. Inspect the event’s reason to distinguish the terminal outcome.
  • Session lifecycle fields. Session lifecycle fields are endpoint-specific. Items returned by GET /sessions use state, while GET /sessions/{session_uuid} use status. Both fields have the same lifecycle values. Applications that deserialize both responses into one model should accept the field used by each endpoint.
  • Shared rate-limit bucket. playback/seek, recording/mask, and recording/unmask draw from one shared seek rate-limit bucket. A burst of one of these commands can reduce the capacity available to the other two. See Rate limits.
  • Idempotent command 503 responses. A 503 after an idempotent command may mean the command completed but its saved response could not be returned. Retry once with the same Idempotency-Key. If it remains unavailable, reconcile from the resulting resource or webhook events before issuing a new key.
  • WebSocket pause and resume. WebSocket pause and resume affect audio in both directions. Pausing preserves buffered backend audio, and resuming does not change mute state.

Fixed

  • Zero-value playback seeks. Playback seek requests now reject seek_ms: 0. Use a positive value to seek forward or a negative value to rewind.
  • Authentication error responses. Authentication and client-error responses now distinguish malformed credentials (400), missing or invalid credentials (401), forbidden operations (403), and resources hidden by application ownership (404). Error responses expose a stable code for programmatic handling.