/cu1/v1/calls and /cu1/v2/calls accept that exact request shape and return that exact response envelope. Migrating means changing three things — host, API key, base path — and nothing else in your code.
This is a compatibility surface, not the primary API. For a new integration use
POST /calls instead: same engine underneath, but native field names, richer responses, and endpoints for listing, inspecting and cancelling a call — none of which exist here.The endpoints
Both paths run the same handler and behave identically. v2 exists only so a client already pointed at a v2 URL doesn’t have to change; its
number_pool_id is accepted and ignored, because Lehar has no managed number pools.
Like POST /calls, these reject bearer logins and agent-session tokens even though both can carry sessions:write.
Success uses this envelope; pre-flight rejections do not. A workspace out of credits returns a native
402 insufficient_credits, and — where the deployment restricts destinations — a mobile_number outside the allowed prefixes returns a native 400 invalid_request, both shaped as { "error": { "code", "message" } } rather than { "status": "success", … }. This matches the 404 an unknown from_number already returns.How fields map
Every request is translated onto a nativePOST /calls body, then validated by the native endpoint — so the two contracts can’t drift apart.
Reserved prompt variables
name and mobile_number are also made available to the prompt as {{callee_name}} and {{mobile_number}}, matching the source contract. They are defaults, not overrides — if you pass either key inside custom_args_values, your value wins.
Calling window and retries
Older integrations put the calling window directly oncall_config and send call_retry_config as a sibling of it, rather than nesting both:
call_config.call_time, the retry policy under call_config.call_retry_config — so your window and retry count are honoured rather than quietly ignored. The fold is driven by the shape of what you send, not by which path you call, so a body that already nests is untouched. If a body somehow carries both, the nested form wins.
Correlating with your own records
metadata is an opaque passthrough. Send any JSON object — your own ids, your own key names, no schema — and it is stored on the call and echoed back unchanged on every webhook event as a top-level metadata field. That is how you line an event up against your own database rows without keeping a map of our call ids.
Nothing interprets it, nothing requires particular keys, and it is never merged into the agent’s runtime settings. It is not returned on the POST response — the response mirrors the source contract, which echoes custom_args_values only. Capped at 16 KiB, counted together with the unsupported fields stashed alongside it.
Response status vocabulary
call_status is the compatibility vocabulary, not Lehar’s native call status:
Fields that are accepted but do nothing
The contract is deliberately lenient: a field Lehar can’t honour is stored rather than rejected, so an unimplemented setting never breaks a migrating caller mid-cutover.voice_id, version_id, call_category, parent_call_id, scheduled_at, webhook_url, language, v2’s number_pool_id — and anything else unrecognised — are preserved under metadata.cu1 on the call record for traceability, nested so they can never act as a dispatch override.
Your own metadata lands in the same place (at metadata.cu1.metadata) but is not one of these: it is honoured, and echoed on every webhook. The nesting is for a different reason — the top level of a call’s metadata is merged into the agent’s dispatch settings, so an un-nested bag could override runtime configuration.
What this surface doesn’t have
There is no compatibility endpoint for reading calls. Use the native ones — they work on the same records, andcall_id from the response above is the id they take:
GET /calls/{call_id}— status, outcome, and one entry per dial.POST /calls/{call_id}/cancel— stop a call before the next dial.- Outbound webhooks —
call_completedwith transcript, cost and sentiment.
Receiving the result
The request side is compatible by translation; the result side is compatible by configuration. Register a subscription per agent with:headers are replayed verbatim, so your existing static-header check keeps working and signature verification is not required. payload_format: "ringg" reshapes the body — transcript as an array of {"bot": …} / {"user": …} turns, Ringg’s status/sub_status vocabulary, a 24-hour recording_url, and retry_count/attempts on call_completed. And because subscriptions are per agent, per-agent routing works the same way it does today.
Your metadata comes back verbatim on every event, which is the piece that makes a migrating receiver’s correlation logic work unchanged.
Three differences remain deliberately:
to_numberis masked.version_id,version_slug,version_descriptionandtool_call_logsare not sent — Lehar has no equivalent.custom_args_valuesis not echoed. Those are your prompt variables, and an integration that ships whole prompt blocks through them would get tens of KB of its own text back on three events per call. Put correlation keys inmetadata; read the prompt variables fromGET /calls/{call_id}if you need them.
Calls
The native API this translates onto.
Outbound webhooks
Receive the result of the call.

