Skip to content

Calls

The monitoring API

The six REST endpoints behind live-call monitoring: list the calls in progress, mint a browser credential, take over, end the call, close a session, and read the audit.

A member of your staff opens a call that is happening right now, reads the conversation as it is spoken, and listens to the audio. If the caller needs a person, they press a button, the assistant says one short line and goes quiet, and your colleague speaks to the caller directly. Closing the panel returns the call to the assistant; hanging up ends it.

The integration has the same two security boundaries as browser calling:

  1. Your backend calls the endpoints on this page with an OnCore API key.
  2. Your browser receives only a short-lived join credential, then connects to the call with it.

Keep the API key server-side

Never call these endpoints from browser JavaScript. Anyone can read credentials bundled into frontend code or sent by the browser. Only the short-lived join credential belongs in the client.

Start with the introduction if you have not read it yet. This page is the endpoint reference.

Before you start#

You need:

  • An inbound call in progress on one of your OnCore phone numbers. Monitoring covers phone calls only; a browser or WhatsApp call cannot be monitored.
  • An API key held in your backend: either a tenant key, or an organization key plus X-Tenant-Id naming the customer tenant, exactly as in the reseller quickstart. A tenant key already carries its tenant and needs no header.
  • A place in your product where the signed-in user has already been authorized to listen. Read Authorization is yours to enforce before you build the button.

The REST API is available, the browser package is preview

Every REST endpoint on this page is available now. Listening to a call and speaking on it are not REST features and need a browser connected to the call as well: In the browser is that half, and it can be built either on the preview @oncore/live-call-sdk or directly on the public livekit-client against the documented wire protocol. Capabilities and availability is the canonical status.

The examples use Next.js App Router. The same split applies in any framework: session creation and every control call on your server, the connection in browser code.

The endpoints#

MethodPathPurpose
GET/calls/liveList the calls in progress for this tenant
POST/calls/{id}/monitor-sessionMint one browser join credential for one live call
POST/calls/{id}/takeoverStand the assistant down and let the monitor speak
POST/calls/{id}/endHang up the call
DELETE/calls/{id}/monitor-sessionClose one monitor session. Does not end the call
GET/calls/{id}/monitor-sessionsRead who monitored a call, during it or after it

id and callId are different identifiers

Every live-calls row carries both. id is the call's identifier in the API and the only one a path ever takes, the same one GET /calls/{id} already takes, and the value every response on this page echoes back. callId is the identifier the assistant writes into its own records, which is what you quote on a support ticket. It is never accepted in a path, and passing it produces a 404.

The two GET routes use the standard response envelope, so their payloads sit under data. The three POST routes do not: their success bodies are bare JSON, because a browser's fetch hands the join material straight to the SDK, and their 404, 409, and 429 bodies are bare JSON too. Their 400 is the one exception and does use the envelope, because an over-length field is an ordinary validation failure with no reason code to carry. The DELETE answers 204 with no body at all. Errors and recovery has all of them.

Wire your buttons to these endpoints#

Six endpoints, and each one is a whole feature from your backend's point of view. This table is the map from a control in your interface to the call that backs it.

Your buttonCallBodyOn success
Refresh the list of calls in progressGET /calls/livenonedata.calls[], each with an id, who is calling, how long it has been running, and whether it can be monitored
Listen to this callPOST /calls/{id}/monitor-session{ "actorLabel": "who asked" }, optional{ id, url, token, expiresAt, agentIdentity, monitorIdentity }, the join credential for one browser
Speak to this callerPOST /calls/{id}/takeover{ "monitorIdentity": "monitor-..." }{ id, monitorIdentity, state: "bridging", takeoverStartedAt }
Hang upPOST /calls/{id}/end{ "monitorIdentity": "monitor-...", "reason": "resolved" }, both optional{ id, state: "ending" }
Close the panelDELETE /calls/{id}/monitor-session?monitorIdentity=monitor-...none204, and the call carries on without that listener
Show who listenedGET /calls/{id}/monitor-sessionsnonedata.sessions[], one row per session with its actorLabel and takeover timestamps

Every one takes your API key in an Authorization: ApiKey header, from your backend. Every {id} is the id field of a /calls/live row.

Two of these six need a browser in the call, and the API cannot supply it

Listening and speaking are not REST features. The audio and the live transcript travel over the browser's own connection to the call, so POST /calls/{id}/monitor-session returns join material rather than a stream, and there is no server-side transcript feed to poll.

POST /calls/{id}/takeover inherits that. It does not move the call to a person: it stands the assistant down and promotes an already-connected browser participant to speak. Its monitorIdentity has to name a session whose browser has joined the call, and a takeover called without one is refused 409 session_closed rather than half-working. Calling it from a backend with no browser attached would silence the assistant while nobody is there to talk, so the API refuses instead.

So a backend-only integration gets four of the six on its own: list the calls in progress, hang one up, close a session, and read the audit. Listening and taking over need a browser in the call, which is In the browser.

The order the calls go in#

http
GET    /calls/live                          # poll every few seconds, render the rows
POST   /calls/{id}/monitor-session          # when your user opens one: hand url and token
                                            #   to the browser, keep the rest server-side
POST   /calls/{id}/takeover                 # when your user presses "speak", and only
                                            #   after the browser has connected
POST   /calls/{id}/end                      # when your user hangs up
DELETE /calls/{id}/monitor-session          # when the panel closes, always

DELETE is the one that is easy to forget and the one worth wiring first: nothing else closes a session, and sessions that are never closed count against the per-call monitor cap until their credential lapses.

Authorization is yours to enforce#

An OnCore API key carries a tenant and nothing else. It carries no user, no role, and no record of who is holding it. When your backend asks for a monitor session, OnCore can tell the request came from your tenant. It cannot tell which of your staff is behind it, and so it cannot refuse one of them.

So the decision about which of your people may listen to a live customer call is enforced in your backend, before it calls OnCore. There is no OnCore-side permission to switch on and no role to assign. If the code path from your browser to POST /calls/{id}/monitor-session has no authorization check in it, every signed-in user of that tenant can listen to every live call in that tenant.

actorLabel does not change this. It is free text, at most 200 characters, recorded verbatim on the audit record so a later reviewer can see who your backend said was listening. OnCore never verifies it and never makes a decision with it. Send the identity your own authentication already established, and treat the field as a log line rather than as a credential.

This delegation is identical to the one browser calling already relies on, and to the tenant boundary in the reseller quickstart: you authenticate the person, OnCore authenticates the tenant.

Disclosure is yours to make#

A live person listening to a call in progress is call monitoring, and several jurisdictions require the people on the call to be told. Whether disclosure is required for a given call, and what it has to say, is your obligation as the operator of the line. OnCore does not review your compliance position, and nothing on this page is legal advice.

The cheapest mechanism the platform gives you is the assistant's own greeting. firstMessage is spoken before anyone has said anything, so one sentence there reaches every caller on that line whether or not anybody ends up listening:

Terminal
curl -X PATCH https://core-api.heysadie.ai/assistants/0195f1e2-2222-7000-8000-000000000000 \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "firstMessage": "Thanks for calling Riverside Dental. Calls may be monitored by our team. How can I help?" }'

Recording consent is a separate question with its own answer: see Recording and transcripts. Recording and monitoring are independent settings, and a call can be monitored whether or not it is recorded.

List the calls in progress#

GET /calls/live takes no query parameters and no pagination.

bash
curl https://core-api.heysadie.ai/calls/live \
  -H "Authorization: ApiKey YOUR_API_KEY"
200 response
{
  "data": {
    "calls": [
      {
        "id": "0195f1e2-1111-7000-8000-000000000000",
        "callId": "0195f1e2-3333-7000-8000-000000000000",
        "assistantId": "0195f1e2-2222-7000-8000-000000000000",
        "assistantName": "Front desk",
        "customerNumber": "+15555550123",
        "callSource": "pstn",
        "status": "initiated",
        "startedAt": "2026-09-02T14:03:11.000Z",
        "durationSeconds": 74,
        "monitorSupported": true,
        "monitorUnsupportedReason": null,
        "monitorCount": 1
      }
    ],
    "total": 1
  },
  "metadata": { "timestamp": "2026-09-02T14:04:25.000Z", "version": "1.0", "path": "/calls/live" }
}
FieldTypeDescription
idUUIDThe identifier every path on this page takes
callIdstringThe identifier the assistant records. Never used in a path
assistantId / assistantNameUUID / stringWhich assistant is handling the call
customerNumberstringThe caller's number, E.164
callSourcestringpstn, webrtc, or whatsapp
statusstringThe call's current status, from the same set as /calls/statuses. Not a ringing-versus-connected signal: a live row commonly reads initiated for the whole call
startedAtISO 8601When the call began
durationSecondsnumberHow long the call has been running, computed on the server so a skewed browser clock still renders a sane number
monitorSupportedbooleanWhether a monitor session can be minted for this call
monitorUnsupportedReasonstring | nullnull when the call can be monitored. Otherwise agent_build_unsupported or not_phone_call
monitorCountnumberMonitor sessions currently counting against the per-call cap

Render a row with monitorSupported: false and disable its button rather than hiding the call, and put monitorUnsupportedReason in the tooltip. A greyed-out button with no reason is the kind of dead end that becomes a support ticket. not_phone_call means the call did not arrive over a phone line, and agent_build_unsupported means the software handling this particular call cannot accept a monitor. Branch on the two values you know and show the raw string for anything else.

monitorCount is the number that decides whether a supervisor may join, so it is the same number you should show them. A session counts while its join window is open, and for as long as it is holding the call however old its token is. A supervisor who shut their tab without ending the session stops counting once their window lapses, and minting a session also releases any earlier session whose owner never arrived. So a long call cannot be locked out by abandoned panels, and a supervisor reloading their own panel does not eat into the cap.

The list is bounded by a maximum call age, which is the platform's own ceiling on how long a call may run rather than a guess at how long calls usually last. A call past it stops being listed whatever its status says, because a call that has stopped reporting looks exactly like a live one if you trust status alone. A row dropping off the list is not a live call you can no longer see; it is a call that is no longer known to be live. The figure is not published.

Two things a row deliberately does not carry, said here so you do not go hunting. There is no dialled number: a call record does not store which of a multi-line tenant's numbers rang, and it is not derivable from the assistant, which may answer several. And there is no direction, because v1 monitors inbound phone calls only. Both are planned rather than hidden.

The row is otherwise deliberately lean. If your list needs more than these fields, read the full record with GET /calls/{id} for the same id.

Poll this endpoint on a timer for the list view; there is no push feed of live calls. A few seconds between polls is enough, and every polling loop should also respect rate limits. Wrap the call in a route of your own, behind the same authorization check as the session route below, so the key never reaches the browser.

Mint a monitor session#

POST /calls/{id}/monitor-session returns one join credential for one call. This is the endpoint your authorization check guards.

Body fieldTypeRequiredDescription
actorLabelstringNoFree text, maximum 200 characters, recorded verbatim on the audit record. Opaque to OnCore and never treated as an identity
bash
curl -X POST https://core-api.heysadie.ai/calls/0195f1e2-1111-7000-8000-000000000000/monitor-session \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "actorLabel": "[email protected]" }'
200 response (no envelope)
{
  "id": "0195f1e2-1111-7000-8000-000000000000",
  "url": "wss://example.livekit.cloud",
  "token": "eyJhbGciOi...",
  "expiresAt": "2026-09-02T14:14:31.000Z",
  "agentIdentity": "agent-AJ4nQ8vTzWpb",
  "monitorIdentity": "monitor-0195f1e2-4444-7000-8000-000000000000"
}
FieldDescription
idThe call this credential is for, echoing the path
urlThe WebSocket URL the browser connects to
tokenThe join credential. Subscribe only: it cannot publish audio
expiresAtWhen the token stops being accepted for a new connection
agentIdentityThe assistant's participant identity in the room. The SDK keys the assistant's audio, state, and messages off it
monitorIdentityThis session's own identity. Send it back on takeover and end

The token expiry is a join deadline and nothing else#

The token is valid for ten minutes from the moment it is minted, and it is checked exactly once, when the browser connects. Three consequences follow, and the third is the one that gets built wrong.

A browser that connects inside the window keeps listening for as long as the call runs, however long that is. The token lapsing does not disconnect anybody.

A browser that tries to connect after the window has closed is refused, and needs a new monitor session. A page refresh at minute twelve is exactly this case, which is why the session route should stay callable rather than being hit once when the page loads.

Takeover and end do not check it. A supervisor who listens for twenty minutes and then presses Take over succeeds. A session stays usable for control for as long as it is open and the call is live, and nothing else is consulted.

Take over and end#

Each call identifies the session with the monitorIdentity your monitor-session response returned, required on takeover, optional on end.

Both are idempotent with respect to a retry, not with respect to a second intervention. A double-submit while the monitor already holds the call returns the original timestamp and sends nothing further to the assistant, which is what idempotency is for. A supervisor whose hold has since been released (by closing the panel, or by leaving the room) and who then needs to step in again on the same session gets a genuine second takeover. Two supervisors racing for the same call is the other case: the first wins, and the second gets 409 held_by_other_monitor.

One honest consequence of allowing the second intervention. A re-takeover overwrites the session's takeover timestamps, so the audit record carries the most recent intervention per session rather than every one. Per-intervention history is planned. The session-level record still answers who was listening and that they intervened.

A hold is only honoured while its holder is still in the room. If a supervisor refreshes their page mid-takeover, their old session's hold is released rather than blocking them, so the new panel can take the call over again. The caller hears no second bridge line for that, because the assistant is already standing down.

Takeover promotes the microphone and nothing else. A promoted monitor still cannot publish a camera or a screen share, which matters because the call recording is a single mixed composite.

POST /calls/{id}/takeover

Body fieldTypeRequiredDescription
monitorIdentitystringYesFrom the monitor-session response

POST /calls/{id}/end

Body fieldTypeRequiredDescription
monitorIdentitystringNoCloses that session and attributes the end to it
reasonstringNoRecorded with the end

Ending does not require having taken over, and that is deliberate. Anyone who can obtain a monitor session for a call can already make the assistant stand down, so there is no extra privilege in hanging up. Requiring a takeover first would only mean that a supervisor who hears something that has to stop must take the call over in order to end it, which is two steps and a bridge line for no gain.

bash
CALL=0195f1e2-1111-7000-8000-000000000000
MONITOR=monitor-0195f1e2-4444-7000-8000-000000000000
API=https://core-api.heysadie.ai

curl -X POST "$API/calls/$CALL/takeover" \
  -H "Authorization: ApiKey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d "{ \"monitorIdentity\": \"$MONITOR\" }"

curl -X POST "$API/calls/$CALL/end" \
  -H "Authorization: ApiKey YOUR_API_KEY" -H "Content-Type: application/json" \
  -d "{ \"monitorIdentity\": \"$MONITOR\", \"reason\": \"resolved\" }"
200 response (takeover)
{
  "id": "0195f1e2-1111-7000-8000-000000000000",
  "monitorIdentity": "monitor-0195f1e2-4444-7000-8000-000000000000",
  "state": "bridging",
  "takeoverStartedAt": "2026-09-02T14:05:12.000Z"
}

End answers with { "id": "...", "state": "ending" }.

state is the conversation state at the moment the endpoint answered, not the final one. Takeover returns bridging because the assistant is still saying its bridge line; the state moves to human a moment later, and the SDK's control event is what tells you it arrived. Drive your interface off the event and treat the response as an acknowledgement.

What the caller hears#

On takeover the assistant finishes its sentence, says one short bridge line, and goes quiet, so nobody is dropped into silence mid-word. When the assistant resumes it comes back with a line of its own before carrying on.

Ending a call that a person took over deliberately skips the assistant's goodbye. The caller has just been speaking to a human, and the assistant reappearing to say "have a great day" is worse than a clean hangup.

What the transcript does during a takeover#

The transcript does not stop, but it becomes one-sided, and this is the shape your panel has to be designed around.

The caller keeps being transcribed for the whole takeover. Their segments keep arriving exactly as before. The assistant standing down does not touch them.

The assistant's voice and the assistant's own transcript stop together. That is what standing down means, and the pair moves as one.

Your colleague is never transcribed. Not live, and not into the stored record afterwards.

So during a takeover a panel shows the caller live and the supervisor's side as a gap. Label the gap, or fill it from what your own interface knows your colleague did, but do not wait for segments that are not coming. What the call record shows afterwards covers the same asymmetry in the stored transcript.

The assistant also does not remember the turns it heard while standing down, so it comes back without a garbled memory of a conversation it was not part of. Your note is how it learns what happened.

Close the session when the panel goes away#

DELETE /calls/{id}/monitor-session?monitorIdentity=monitor-... closes one monitor session. It answers 204 with no body, and it is idempotent.

This closes the session. It does not end the call

DELETE .../monitor-session and POST .../end sit next to each other and do opposite things. The DELETE detaches one supervisor and leaves the caller talking to the assistant. POST .../end hangs up on a customer. That is exactly why closing a session is a DELETE on the resource the mint created, rather than a third verb alongside takeover and end: the shape of the request says which one you meant.

Call it whenever your panel goes away, whether the supervisor closes it, navigates elsewhere, or shuts the tab.

The reason to bother is a consequence rather than housekeeping. The per-call monitor cap counts open sessions, so a panel that opens and closes without cleaning up holds a slot until its token lapses. On a call several people are watching, that is the difference between a colleague being able to join and being told the call is full.

Mint-time release is the backstop: a session whose owner never arrived is released the next time anyone mints one on that call. So a close that never lands costs a few minutes of one slot rather than costing correctness. Do not build retry machinery around this request. Send it once, ignore the outcome, and let the backstop cover what gets lost.

Two more things worth knowing:

  • An identity that names no open session still returns 204. Already closed, never existed, and belonging to another tenant are all the same answer, deliberately, so the endpoint cannot be used to find out which sessions exist.
  • Closing while holding the call releases the hold, and this is how you give a call back. A few seconds after the supervisor leaves the room the assistant resumes on its own, and the audit's takeoverEndedAt records when human control actually ended.

From the browser, send it with keepalive so the request survives the page actually unloading rather than only a component unmounting. navigator.sendBeacon, the usual tool for a request on the way out, cannot send a DELETE, so fetch with keepalive: true is the one that works. The panel above does this in closeSession, from both its unmount and a pagehide listener.

app/api/live-calls/[id]/session/route.ts
// Alongside the POST handler that mints the session.
export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const user = await getSession();
  if (!user || !user.permissions.includes("calls:monitor")) {
    return Response.json({ error: "forbidden" }, { status: 403 });
  }

  const monitorIdentity = new URL(request.url).searchParams.get("monitorIdentity");
  if (!monitorIdentity) return new Response(null, { status: 204 });

  const query = new URLSearchParams({ monitorIdentity });
  const upstream = await oncore(
    user.tenantId,
    `/calls/${id}/monitor-session?${query}`,
    { method: "DELETE" },
  );

  return new Response(null, { status: upstream.status });
}

Read who monitored a call#

GET /calls/{id}/monitor-sessions is the audit read path. It is read-only, tenant-scoped, and works during the call and after it has ended, because after is usually when somebody asks.

Terminal
curl https://core-api.heysadie.ai/calls/0195f1e2-1111-7000-8000-000000000000/monitor-sessions \
  -H "Authorization: ApiKey YOUR_API_KEY"
200 response
{
  "data": {
    "sessions": [
      {
        "id": "0195f1e2-5555-7000-8000-000000000000",
        "monitorIdentity": "monitor-0195f1e2-4444-7000-8000-000000000000",
        "actorLabel": "[email protected]",
        "createdAt": "2026-09-02T14:04:31.000Z",
        "expiresAt": "2026-09-02T14:14:31.000Z",
        "takeoverStartedAt": "2026-09-02T14:05:12.000Z",
        "takeoverEndedAt": "2026-09-02T14:06:40.000Z",
        "closedAt": null
      }
    ],
    "total": 1
  },
  "metadata": {
    "timestamp": "2026-09-02T14:31:08.000Z",
    "version": "1.0",
    "path": "/calls/0195f1e2-1111-7000-8000-000000000000/monitor-sessions"
  }
}
FieldDescription
idThe monitor session's own identifier, not the call's
monitorIdentityThe identity that joined the room
actorLabelExactly the string your backend sent, or null
createdAtWhen the session was minted
expiresAtWhen its join window closed
takeoverStartedAt / takeoverEndedAtThe most recent intervention on this session, or null if it only listened. takeoverEndedAt is when human control ended
closedAtWhen the session was closed, or null

actorLabel comes back exactly as it was supplied and is not evidence of anything. It is a label the caller attached, recorded verbatim and never believed, because an API key carries a tenant and no person. Say so wherever you display it: a reviewer reading "[email protected]" should know it means "your backend asserted this", not "OnCore verified this".

Errors and recovery#

WhereSymptomAction
OnCore 400actorLabel over 200 characters, or reason over 200Clamp them in your own code before sending
OnCore 401Key missing or invalidCheck the server-side key and the ApiKey scheme
OnCore 403Key cannot reach that tenantCheck organization-key tenant selection
OnCore 404Call unknown, another tenant's, or deletedConfirm you sent the id from /calls/live, not the callId beside it
OnCore 409A reason code, listed belowBranch on reason. Retry agent_absent, never retry not_live
OnCore 429Per-tenant session limitWait retryAfterSeconds, or read the Retry-After header
SDK disconnectedThe room is gone or the browser left itTreat it as final. Re-check /calls/live, and mint a new session if the call is still there. The reason is an optional free-text string, so show it rather than branching on it
setMicrophoneEnabled rejectsmic_requires_takeover, or mic_deniedNever retry either one; the SDK already has. Treat mic_requires_takeover as a bug in your own ordering, and mic_denied as a browser permission to explain. See Opening the microphone

404 is byte-identical for a call that does not exist, a call in another tenant, and a deleted call, so the endpoint cannot be used to discover which calls exist:

404 response
{ "error": "not_found" }

409 carries a reason code. The values below are a contract, so branching on them is safe. Treat any other reason as an unknown refusal rather than failing.

409 response
{ "error": "conflict", "reason": "not_live" }
reasonMeaning
not_liveThe call has ended, or has run past the platform's maximum call duration
agent_absentThe call is live, but no assistant is in the room right now
not_monitorableThe call cannot be monitored. monitorUnsupportedReason on the live-calls row says which of the two cases applies
monitor_cap_reachedThe per-call monitor cap is already met
session_closedThe named monitor session has been closed. Mint a new one
held_by_other_monitorAnother session is holding the call and its holder is still in the room

Two of those codes look alike and mean opposite things. not_live means stop. agent_absent means try again shortly.

An assistant that reconnects part-way through a call is out of the room for a moment while the call itself is perfectly alive, and agent_absent is that moment. Retry it once or twice with a short delay before giving up. Never retry not_live, which is final. A client that treats the two alike will tell a supervisor "this call has ended" while they are listening to it.

There is no reason code for an expired token, because the control endpoints do not consider one. session_closed means the session was actually closed, by an end call or by the call finishing, and the answer to it is a new session rather than a retry.

400 is the exception among these bodies: it is the ordinary error envelope with error.code and per-field details, because an over-length field is a plain validation failure. Clamp in your own code rather than relying on it.

429 carries its cooldown at the top level, not inside an envelope:

429 response
{ "error": "rate_limited", "retryAfterSeconds": 42 }

The same number is in a standard Retry-After header. Read the header or the top-level field; error.details is not present on that response.

What the call record shows afterwards#

Two facts about a monitored call matter more than anything else on this page if you review calls for compliance, and neither has a workaround today.

The person who takes over is not transcribed. Nothing your colleague says reaches the transcript, live or retroactively. What the stored record carries instead is a labelled marker showing that a person took the call over, when, and when they handed it back, so a later reader sees a marked gap rather than a non-sequitur. Do not parse that marker: this release does not publish its exact wording. The caller is unaffected, and is transcribed for the whole call including the part a person handled.

The recording is one mixed track. When recording is on, a call is captured as a single mixed audio track of the whole conversation. Your colleague's voice lands on the same track as the caller's and the assistant's, and there is no way to separate them afterwards. If your review process needs the supervisor's half on its own, this is not the tool for it.

Everything else about a monitored call is ordinary. It appears in call activity like any other call, and it fires the same end-of-call report.

Limits, and what you cannot change#

  • The bridge line and the resume line are fixed. What the caller hears when a supervisor steps in, and when the assistant comes back, is not a per-assistant setting in v1. This is a deliberate deferral rather than an oversight: a per-assistant setting has to reach three separate hand-maintained lists before it takes effect on a call, and a feature has already shipped broken by missing one of them.
  • The monitor cap, the rate limit, the maximum call age, and the wait before the assistant resumes are platform constants. They are not per-tenant configuration and no figures are published, so read the 409 and the 429 rather than predicting them.
  • Inbound phone calls only. Browser and WhatsApp calls report monitorSupported: false, and a row carries no direction because there is nothing else it could be.
  • One person speaks at a time. Takeover is exclusive, and a second monitor asking for it gets held_by_other_monitor while the holder is present. Listeners stay listeners.
  • Refreshing recovers, and costs nothing. A supervisor who reloads their panel mints a fresh session and carries on, whether they were listening or mid-takeover. The reload does not consume a monitor cap slot, and mid-takeover it does not make the caller hear a second bridge line.
  • A dropped browser does not leave the caller in silence. If the supervisor's page closes for good while they are holding the call, the assistant takes the conversation back after a few seconds. A reload normally lands well inside that window.
  • There is no server-side transcript feed. The transcript rides the browser's own connection. If you need mid-call transcripts on your server, this release does not provide them.
  • There is no prebuilt panel. OnCore supplies the endpoints and the browser package; the interface is yours, as it is for browser calling.

Next steps#