Skip to content

Calls

A call is a 1:1 audio and video call inside a conversation. Media runs on Agora. mondochat keeps the call record, rings the callee and logs durations that are good enough to bill minutes.

  • Turn on calls.enabled in your settings. It is off by default.
  • Add call: true to the policy row from the caller’s role to the callee’s role.
  • A conversation exists between the two users. See Conversations.
  • Both users have a session token. Every route below is a user route.
  • To charge by the minute you also need calls.per_minute.enabled, an endpoint marked Answers callouts, and the call_minute callout. See Commerce and Webhooks overview.
Terminal window
export MC=https://app.mondochat.io/v1
export SESSION=<session token>
  1. Start a call with POST /v1/calls. video defaults to false and only tells the callee’s client how to ring.

    Terminal window
    curl -X POST $MC/calls -H "Authorization: Bearer $SESSION" \
    -H 'content-type: application/json' -d "{\"conversation_id\": \"$ID\", \"video\": true}"

    The answer is 201 with a CallJoin: {call, agora_app_id, channel, uid, peer_uid, rtc_token, expires_at}. The call is ringing. mondochat posts a call_invite card, publishes call.ringing to the callee and sends the call.ringing webhook.

  2. Join the Agora channel with agora_app_id, channel, uid and rtc_token. React only to the user whose uid is peer_uid: moderators watching and moderation bots also sit in the channel. The frame uses mode rtc, and the codec and area from your calls settings.

  3. Ring the callee. The callee’s client gets call.ringing over the realtime socket and your server gets the call.ringing webhook, so you can reach the callee when the chat is closed. A client that boots or resyncs asks GET /v1/calls/ringing for the calls still ringing to the user, newest first. A call rings for calls.ring_timeout_seconds (45 by default). When it runs out the call is missed and a call_missed card is posted.

  4. Answer with POST /v1/calls/{id}/answer. The answer is a CallJoin for the callee. The call becomes active, answered_at is set and call.updated goes to both users. Decline with POST /v1/calls/{id}/decline. The call becomes declined and a call_missed card is posted. Declining answers the Call.

  5. End with POST /v1/calls/{id}/end. Either user may end it. The call becomes ended, durations are computed, one call_ended card is posted, call.updated is published and the call.ended webhook is sent with answered_at, ended_at, end_reason and the seconds of each participant. A call that was never answered has answered_at: null. A participant who never left is closed at the call’s end time and marked estimated.

  6. Renew the token. rtc_token expires calls.max_duration_minutes (240 by default) after the call started, or at the paid-until time when per-minute is on. Request a new one with POST /v1/calls/{id}/token, which answers {rtc_token, expires_at}. Do it when the Agora SDK reports token-privilege-will-expire.

  7. Charge by the minute. Set calls.per_minute to {enabled: true, price_minor, currency, preauthorize_minutes, payer_role}. payer_role empty means the caller pays. Prices are in minor units.

    • When both users have joined, mondochat calls your call_minute callout with {call_id, payer, minute, price_minor, currency} for the first preauthorize_minutes and sets paid_until.
    • Each POST /v1/calls/{id}/token charges the next minute first.
    • Answer the callout with {status: paid | insufficient | failed}. Anything but paid ends the call with the reason insufficient_funds.
    • The callout carries an Idempotency-Key of <call_id>:<minute>. Charge each key once.
  8. Send call frames for moderation. With moderation.calls.agora on, mondochat starts Agora call moderation for each participant when the call becomes active. If a participant’s task did not start and moderation.calls.fallback_capture is on, the other participant’s client gets call.capture with {call_id, subject_id, interval_seconds, max_frames}. A client that implements this:

    1. Draws the subject’s remote video to a canvas every interval_seconds and encodes a JPEG of at most 480 px on the long edge.
    2. Calls POST /v1/calls/{id}/frames with {subject_id, content_type: "image/jpeg"}. The answer is {frame_id, url, fields, expires_at}, a presigned POST valid for 2 minutes and 1 MB.
    3. Uploads the JPEG as in Media, then calls POST /v1/calls/{id}/frames/{frame_id}/complete, which answers 202.
    4. Stops at max_frames, on hang-up and when the video stops. Nothing is shown to either user.

    The frame route accepts only a participant of an active call, naming the other participant, and only after call.capture was issued for that subject. Past moderation.calls.capture_max_frames (30 by default) it answers 429.

  9. React to call webhooks. call.ringing is {call_id, conversation_id, caller, callee, recipient_online, open_url, video}. call.ended is {call_id, conversation_id, caller, callee, answered_at, ended_at, end_reason, participants}.

  1. Start a call from user A to user B. The answer is 201 and the call is ringing. B’s GET /v1/calls/ringing lists it.
  2. Answer as B. Both users get call.updated and the call is active.
  3. End it. The call is ended, the thread has one call_ended card, and your endpoint got a call.ended webhook.
Answer Cause
403 policy_denied, reason calls calls.enabled is off.
403 policy_denied, reason call The policy row from the caller’s role to the callee’s has call off. Reason inbound when the callee takes messages from members only and the caller is a guest.
403 forbidden, reason you_blocked The caller blocked the callee.
409 busy The callee is in a ringing or active call, or was rung in the last 45 seconds (both users get a call_missed card), or the callee blocked the caller, which the caller is not told.
409 conflict The caller already has an open call, or the call is not in a state the route works on: call is declined, for example.
403 banned The caller is banned.
502 upstream_failed Agora is not configured on the server.
403 forbidden, reason insufficient_funds POST /v1/calls/{id}/token charged the next minute and your call_minute callout did not answer paid. The call is ended.
403 forbidden A frame request for a participant that was never sent call.capture.
429 rate_limited A frame upload is past capture_max_frames.
404 not_found The call does not exist in your app, or the caller is not one of its two users.
Call ends with insufficient_funds Your call_minute callout answered something but paid, or failed.
Call ends with max_duration The call reached calls.max_duration_minutes.
  • A ringing call times out after calls.ring_timeout_seconds (45 by default) and an active call ends after calls.max_duration_minutes (240 by default).
  • A call_invite card stays joinable for 10 minutes while its call is ringing or active.
  • When a call ends, mondochat counts ceil(seconds / 60) call minutes of usage from the seconds both participants overlapped.
  • Group calls, recording and screen share are not offered.
  • A blocked pair cannot call each other. A call card for call_ended is still delivered across a block.
  • Call frames come from the participants’ browsers, so a modified client can send clean frames. A case from client frames never holds, blocks or ends the call.
  • With calls.single_card on, a call shows as one card that turns into Call ended, and an unanswered call posts call_ended instead of call_missed. Only a busy callee posts call_missed.