# 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.

## Before you start

- 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](/guides/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](/guides/commerce/) and [Webhooks overview](/webhooks/overview/).

```sh
export MC=https://app.mondochat.io/v1
export SESSION=<session token>
```

## Steps

1. Start a call with `POST /v1/calls`. `video` defaults to false and only tells the callee's client how to ring.

   ```sh
   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](/guides/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}`.

## Check it works

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.

## When it fails

| 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`. |

## Limits

- 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`.

## Related topics

- [Start a call](/api/operations/start-call/), [answer](/api/operations/answer-call/), [decline](/api/operations/decline-call/), [end](/api/operations/end-call/), [ringing calls](/api/operations/ringing-calls/), [renew a token](/api/operations/renew-token/), [request a frame](/api/operations/request-frame/), [complete a frame](/api/operations/complete-frame/)
- [call.ringing webhook](/api/webhooks/call-ringing/), [call.ended webhook](/api/webhooks/call-ended/), [call_minute callout](/api/webhooks/call-minute/)
- [Moderation](/guides/moderation/)
- [Settings reference](/reference/settings/) for `calls` and `moderation`
- [Errors](/errors/)