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.

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 opaque references without receiving card or CVV digits. A successful payment.card.collected webhook includes a card_uuid reference in the card_<lowercase-uuid-v4> format and safe display values under payment_method_details.card.last4 and .brand. A successful payment.cvv.collected webhook includes a cvv_uuid reference in the cvv_<lowercase-uuid-v4> format. 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 a required Idempotency-Key, exp_month, exp_year, a positive amount in the currency’s smallest unit, lowercase currency: "ils", provider: "nedarim", and the customer’s seven-digit terminal_number. Nedarim accepts only whole-ILS amounts, so an ILS amount in agorot must be divisible by 100.

    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-authored prose, PAN, CVV, expiry, identity numbers, customer PII, receipt URLs, and bearer capabilities.

    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":"sess_..."} 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 that the authenticated application owns 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.

  • 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. Authenticated applications can use GET /api/v1/app to read their 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 does not assign numbers or expose unassigned numbers or another application’s configuration. 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.connectedwebsocket.connected
    • websocket_relay.disconnectedwebsocket.disconnected
    • websocket_relay.failedwebsocket.failed
    • websocket_relay.audio_playback_completedwebsocket.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 pull_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/startPOST /v1/sessions/{uuid}/websocket-relay
    • POST /v1/sessions/{uuid}/websocket/stopDELETE /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.connectedwebsocket_relay.connected
    • websocket.disconnectedwebsocket_relay.disconnected
    • websocket.failedwebsocket_relay.failed
    • websocket.audio_playback_completedwebsocket_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_ratemedia_format.from_session.sample_rate
    • to_session_audio_ratemedia_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.