@myronsi/messenger-api 2.1.0-alpha.14 → 2.1.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/openapi.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Messenger API
4
- version: 2.1.0-alpha.14
4
+ version: 2.1.0-alpha.15
5
5
  summary: REST contract of the Messenger backend (v2).
6
6
  description: |
7
7
  Contract for the Go backend. The WebSocket protocol is described in `websocket/` and
@@ -58,6 +58,8 @@ tags:
58
58
  description: Approval requests (direct messages and group invites)
59
59
  - name: websocket
60
60
  description: Realtime connection (see `websocket.md`)
61
+ - name: calls
62
+ description: Audio calls (see `docs/calls.md` and `docs/calls-e2ee.md`)
61
63
 
62
64
  paths:
63
65
  /meta:
@@ -3394,6 +3396,316 @@ paths:
3394
3396
  schema:
3395
3397
  $ref: "#/components/schemas/Problem"
3396
3398
 
3399
+ /calls/config:
3400
+ parameters:
3401
+ - $ref: "#/components/parameters/ClientVersion"
3402
+ - $ref: "#/components/parameters/ClientApiVersion"
3403
+ get:
3404
+ tags: [calls]
3405
+ operationId: getCallConfig
3406
+ summary: Whether audio calls are available and their limits
3407
+ description: |
3408
+ `enabled` is `false` when the server has no media service configured or calls are switched off; clients then
3409
+ hide every call control. Clients must also check that the browser supports call encryption before offering
3410
+ calls.
3411
+ responses:
3412
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3413
+ "400": { $ref: "#/components/responses/BadRequest" }
3414
+ "200":
3415
+ description: Call availability
3416
+ content:
3417
+ application/json:
3418
+ schema: { $ref: "#/components/schemas/CallConfig" }
3419
+ "401": { $ref: "#/components/responses/Unauthorized" }
3420
+ /me/call-identity:
3421
+ parameters:
3422
+ - $ref: "#/components/parameters/ClientVersion"
3423
+ - $ref: "#/components/parameters/ClientApiVersion"
3424
+ put:
3425
+ tags: [calls]
3426
+ operationId: setCallIdentity
3427
+ summary: Register this session's call identity public key
3428
+ description: |
3429
+ One key per session (see `docs/calls-e2ee.md`). Registering a different key replaces the old one; if the
3430
+ session is in a call, the call's epoch increases. The private key never leaves the client.
3431
+ requestBody:
3432
+ required: true
3433
+ content:
3434
+ application/json:
3435
+ schema: { $ref: "#/components/schemas/CallIdentityRequest" }
3436
+ responses:
3437
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3438
+ "400": { $ref: "#/components/responses/BadRequest" }
3439
+ "204":
3440
+ description: Stored
3441
+ "401": { $ref: "#/components/responses/Unauthorized" }
3442
+ "422": { $ref: "#/components/responses/ValidationFailed" }
3443
+ "429": { $ref: "#/components/responses/TooManyRequests" }
3444
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3445
+ /calls:
3446
+ parameters:
3447
+ - $ref: "#/components/parameters/ClientVersion"
3448
+ - $ref: "#/components/parameters/ClientApiVersion"
3449
+ get:
3450
+ tags: [calls]
3451
+ operationId: listMyCalls
3452
+ summary: Calls that are not over and involve the current user
3453
+ description: |
3454
+ Calls ringing the user and calls the user has joined, newest first. Clients load this after every
3455
+ (re)connect, because realtime events are best effort.
3456
+ responses:
3457
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3458
+ "400": { $ref: "#/components/responses/BadRequest" }
3459
+ "200":
3460
+ description: Current calls
3461
+ content:
3462
+ application/json:
3463
+ schema: { $ref: "#/components/schemas/CallList" }
3464
+ "401": { $ref: "#/components/responses/Unauthorized" }
3465
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3466
+ /chats/{chat_id}/call:
3467
+ parameters:
3468
+ - $ref: "#/components/parameters/ClientVersion"
3469
+ - $ref: "#/components/parameters/ClientApiVersion"
3470
+ - $ref: "#/components/parameters/ChatId"
3471
+ get:
3472
+ tags: [calls]
3473
+ operationId: getChatCall
3474
+ summary: The chat's call that is not over
3475
+ responses:
3476
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3477
+ "400": { $ref: "#/components/responses/BadRequest" }
3478
+ "200":
3479
+ description: The current call of the chat
3480
+ content:
3481
+ application/json:
3482
+ schema: { $ref: "#/components/schemas/Call" }
3483
+ "401": { $ref: "#/components/responses/Unauthorized" }
3484
+ "404": { $ref: "#/components/responses/NotFound" }
3485
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3486
+ post:
3487
+ tags: [calls]
3488
+ operationId: startChatCall
3489
+ summary: Start a call in the chat, or get the one already running
3490
+ description: |
3491
+ Starts ringing the other members. A chat has at most one call that is not over: when one exists it is
3492
+ returned with `200` and the client joins it instead. `client_request_id` makes retries idempotent for
3493
+ 24 hours. Starting does not join: the caller then calls `POST /calls/{call_id}/join`.
3494
+
3495
+ Refused with `403` (`calls_not_allowed`) when the peer of a direct chat has calls turned off or one user
3496
+ blocked the other, `503` (`calls_unavailable`) when calls are off on the server.
3497
+ requestBody:
3498
+ required: true
3499
+ content:
3500
+ application/json:
3501
+ schema: { $ref: "#/components/schemas/StartCallRequest" }
3502
+ responses:
3503
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3504
+ "400": { $ref: "#/components/responses/BadRequest" }
3505
+ "200":
3506
+ description: A call was already running in the chat
3507
+ content:
3508
+ application/json:
3509
+ schema: { $ref: "#/components/schemas/Call" }
3510
+ "201":
3511
+ description: The new call, ringing
3512
+ content:
3513
+ application/json:
3514
+ schema: { $ref: "#/components/schemas/Call" }
3515
+ "401": { $ref: "#/components/responses/Unauthorized" }
3516
+ "403": { $ref: "#/components/responses/Forbidden" }
3517
+ "404": { $ref: "#/components/responses/NotFound" }
3518
+ "422": { $ref: "#/components/responses/ValidationFailed" }
3519
+ "429": { $ref: "#/components/responses/TooManyRequests" }
3520
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3521
+ /calls/{call_id}:
3522
+ parameters:
3523
+ - $ref: "#/components/parameters/ClientVersion"
3524
+ - $ref: "#/components/parameters/ClientApiVersion"
3525
+ - $ref: "#/components/parameters/CallId"
3526
+ get:
3527
+ tags: [calls]
3528
+ operationId: getCall
3529
+ summary: A call of one of the user's chats
3530
+ description: Ended calls stay readable for 90 days.
3531
+ responses:
3532
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3533
+ "400": { $ref: "#/components/responses/BadRequest" }
3534
+ "200":
3535
+ description: The call
3536
+ content:
3537
+ application/json:
3538
+ schema: { $ref: "#/components/schemas/Call" }
3539
+ "401": { $ref: "#/components/responses/Unauthorized" }
3540
+ "404": { $ref: "#/components/responses/NotFound" }
3541
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3542
+ /calls/{call_id}/join:
3543
+ parameters:
3544
+ - $ref: "#/components/parameters/ClientVersion"
3545
+ - $ref: "#/components/parameters/ClientApiVersion"
3546
+ - $ref: "#/components/parameters/CallId"
3547
+ post:
3548
+ tags: [calls]
3549
+ operationId: joinCall
3550
+ summary: Answer or join a call with this session
3551
+ description: |
3552
+ Answers a ringing call or joins a running group call, and returns a short-lived media grant for this
3553
+ session. The session must have a call identity (`422`, `call_identity_required`). A user is in at most one
3554
+ call with one session: `409` (`already_in_call`) unless `takeover` is `true`, which moves the user's
3555
+ participation to this session (the other session is removed from the media room). Other `409` codes:
3556
+ `call_ended`, `call_full`. The epoch of the call increases. The body is optional: no body, an empty body
3557
+ and `{}` all join without takeover.
3558
+ requestBody:
3559
+ required: false
3560
+ content:
3561
+ application/json:
3562
+ schema: { $ref: "#/components/schemas/JoinCallRequest" }
3563
+ responses:
3564
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3565
+ "400": { $ref: "#/components/responses/BadRequest" }
3566
+ "200":
3567
+ description: Joined
3568
+ content:
3569
+ application/json:
3570
+ schema: { $ref: "#/components/schemas/CallJoin" }
3571
+ "401": { $ref: "#/components/responses/Unauthorized" }
3572
+ "403": { $ref: "#/components/responses/Forbidden" }
3573
+ "404": { $ref: "#/components/responses/NotFound" }
3574
+ "409": { $ref: "#/components/responses/Conflict" }
3575
+ "422": { $ref: "#/components/responses/ValidationFailed" }
3576
+ "429": { $ref: "#/components/responses/TooManyRequests" }
3577
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3578
+ /calls/{call_id}/grant:
3579
+ parameters:
3580
+ - $ref: "#/components/parameters/ClientVersion"
3581
+ - $ref: "#/components/parameters/ClientApiVersion"
3582
+ - $ref: "#/components/parameters/CallId"
3583
+ post:
3584
+ tags: [calls]
3585
+ operationId: renewCallGrant
3586
+ summary: A fresh media grant for the session that joined
3587
+ description: For reconnecting to the media server. Only the session that joined gets one (`409` otherwise).
3588
+ responses:
3589
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3590
+ "400": { $ref: "#/components/responses/BadRequest" }
3591
+ "200":
3592
+ description: The grant
3593
+ content:
3594
+ application/json:
3595
+ schema: { $ref: "#/components/schemas/CallGrant" }
3596
+ "401": { $ref: "#/components/responses/Unauthorized" }
3597
+ "404": { $ref: "#/components/responses/NotFound" }
3598
+ "409": { $ref: "#/components/responses/Conflict" }
3599
+ "429": { $ref: "#/components/responses/TooManyRequests" }
3600
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3601
+ /calls/{call_id}/decline:
3602
+ parameters:
3603
+ - $ref: "#/components/parameters/ClientVersion"
3604
+ - $ref: "#/components/parameters/ClientApiVersion"
3605
+ - $ref: "#/components/parameters/CallId"
3606
+ post:
3607
+ tags: [calls]
3608
+ operationId: declineCall
3609
+ summary: Decline a ringing call
3610
+ description: |
3611
+ Stops the ringing on all of the user's devices. In a direct call this ends the call (`declined`); in a group
3612
+ call only this user's invitation ends. Declining a call that no longer rings changes nothing.
3613
+ responses:
3614
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3615
+ "400": { $ref: "#/components/responses/BadRequest" }
3616
+ "204":
3617
+ description: Declined
3618
+ "401": { $ref: "#/components/responses/Unauthorized" }
3619
+ "404": { $ref: "#/components/responses/NotFound" }
3620
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3621
+ /calls/{call_id}/leave:
3622
+ parameters:
3623
+ - $ref: "#/components/parameters/ClientVersion"
3624
+ - $ref: "#/components/parameters/ClientApiVersion"
3625
+ - $ref: "#/components/parameters/CallId"
3626
+ post:
3627
+ tags: [calls]
3628
+ operationId: leaveCall
3629
+ summary: Leave the call
3630
+ description: |
3631
+ A direct call ends when either side leaves. A group call ends when the last participant has left and the
3632
+ short reconnect grace has passed. The caller leaving a call nobody answered cancels it (`cancelled`).
3633
+ responses:
3634
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3635
+ "400": { $ref: "#/components/responses/BadRequest" }
3636
+ "204":
3637
+ description: Left
3638
+ "401": { $ref: "#/components/responses/Unauthorized" }
3639
+ "404": { $ref: "#/components/responses/NotFound" }
3640
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3641
+ /calls/{call_id}/end:
3642
+ parameters:
3643
+ - $ref: "#/components/parameters/ClientVersion"
3644
+ - $ref: "#/components/parameters/ClientApiVersion"
3645
+ - $ref: "#/components/parameters/CallId"
3646
+ post:
3647
+ tags: [calls]
3648
+ operationId: endCall
3649
+ summary: End the call for everyone
3650
+ description: |
3651
+ Allowed for the initiator, for both members of a direct call, and for the owner, admins and moderators of a
3652
+ group (`403` otherwise). Everyone is removed from the media room.
3653
+ responses:
3654
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3655
+ "400": { $ref: "#/components/responses/BadRequest" }
3656
+ "204":
3657
+ description: Ended
3658
+ "401": { $ref: "#/components/responses/Unauthorized" }
3659
+ "403": { $ref: "#/components/responses/Forbidden" }
3660
+ "404": { $ref: "#/components/responses/NotFound" }
3661
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3662
+ /calls/{call_id}/keys:
3663
+ parameters:
3664
+ - $ref: "#/components/parameters/ClientVersion"
3665
+ - $ref: "#/components/parameters/ClientApiVersion"
3666
+ - $ref: "#/components/parameters/CallId"
3667
+ get:
3668
+ tags: [calls]
3669
+ operationId: getCallKeys
3670
+ summary: Key envelopes addressed to this session in the current epoch
3671
+ description: For catching up after a reconnect; the same envelopes also arrive as `call_keys` events.
3672
+ responses:
3673
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3674
+ "400": { $ref: "#/components/responses/BadRequest" }
3675
+ "200":
3676
+ description: The envelopes
3677
+ content:
3678
+ application/json:
3679
+ schema: { $ref: "#/components/schemas/CallKeyEnvelopeList" }
3680
+ "401": { $ref: "#/components/responses/Unauthorized" }
3681
+ "404": { $ref: "#/components/responses/NotFound" }
3682
+ "409": { $ref: "#/components/responses/Conflict" }
3683
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3684
+ post:
3685
+ tags: [calls]
3686
+ operationId: sendCallKeys
3687
+ summary: Send this session's sealed sender key to the other participants
3688
+ description: |
3689
+ Relays opaque HPKE envelopes (see `docs/calls-e2ee.md`). Only the session that joined may send, only for the
3690
+ current epoch (`409`, `stale_epoch`), and only to sessions on the roster of that epoch (`422`). The server
3691
+ never sees a plaintext key.
3692
+ requestBody:
3693
+ required: true
3694
+ content:
3695
+ application/json:
3696
+ schema: { $ref: "#/components/schemas/SendCallKeysRequest" }
3697
+ responses:
3698
+ "426": { $ref: "#/components/responses/ClientOutdated" }
3699
+ "400": { $ref: "#/components/responses/BadRequest" }
3700
+ "204":
3701
+ description: Relayed
3702
+ "401": { $ref: "#/components/responses/Unauthorized" }
3703
+ "404": { $ref: "#/components/responses/NotFound" }
3704
+ "409": { $ref: "#/components/responses/Conflict" }
3705
+ "422": { $ref: "#/components/responses/ValidationFailed" }
3706
+ "429": { $ref: "#/components/responses/TooManyRequests" }
3707
+ "503": { $ref: "#/components/responses/CallsUnavailable" }
3708
+
3397
3709
  components:
3398
3710
  securitySchemes:
3399
3711
  bearerAuth:
@@ -3459,6 +3771,11 @@ components:
3459
3771
  in: path
3460
3772
  required: true
3461
3773
  schema: { type: string, format: uuid }
3774
+ CallId:
3775
+ name: call_id
3776
+ in: path
3777
+ required: true
3778
+ schema: { $ref: "#/components/schemas/Id" }
3462
3779
 
3463
3780
  ClientVersion:
3464
3781
  name: X-Client-Version
@@ -3539,6 +3856,18 @@ components:
3539
3856
  content:
3540
3857
  application/problem+json:
3541
3858
  schema: { $ref: "#/components/schemas/Problem" }
3859
+ CallsUnavailable:
3860
+ description: |
3861
+ Calls are switched off on the server, or the server is at its call capacity (both `calls_unavailable`), or
3862
+ the media service or a store is away for a moment (`internal_error`). Retry after `Retry-After` when it is
3863
+ present; without it calls stay off until `GET /calls/config` reports them enabled.
3864
+ headers:
3865
+ Retry-After:
3866
+ description: Seconds until a retry may succeed
3867
+ schema: { type: integer, minimum: 0 }
3868
+ content:
3869
+ application/problem+json:
3870
+ schema: { $ref: "#/components/schemas/Problem" }
3542
3871
 
3543
3872
  schemas:
3544
3873
  NotificationPreferences:
@@ -3691,6 +4020,13 @@ components:
3691
4020
  - invalid_client_version
3692
4021
  - blocked_by_user
3693
4022
  - approval_required
4023
+ - calls_unavailable
4024
+ - calls_not_allowed
4025
+ - call_identity_required
4026
+ - call_full
4027
+ - call_ended
4028
+ - already_in_call
4029
+ - stale_epoch
3694
4030
  - internal_error
3695
4031
  Problem:
3696
4032
  type: object
@@ -3971,6 +4307,7 @@ components:
3971
4307
  - direct_messages
3972
4308
  - group_invites
3973
4309
  - search_visibility
4310
+ - calls_enabled
3974
4311
  - exceptions
3975
4312
  properties:
3976
4313
  avatar_visibility: { $ref: "#/components/schemas/Visibility" }
@@ -3980,6 +4317,9 @@ components:
3980
4317
  direct_messages: { $ref: "#/components/schemas/DirectMessagePolicy" }
3981
4318
  group_invites: { $ref: "#/components/schemas/GroupInvitePolicy" }
3982
4319
  search_visibility: { $ref: "#/components/schemas/SearchVisibility" }
4320
+ calls_enabled:
4321
+ type: boolean
4322
+ description: Whether others may ring this user. Calls are only possible from an existing direct chat or a shared group.
3983
4323
  exceptions: { $ref: "#/components/schemas/PrivacyExceptions" }
3984
4324
  UpdatePrivacySettingsRequest:
3985
4325
  type: object
@@ -3992,6 +4332,7 @@ components:
3992
4332
  direct_messages: { $ref: "#/components/schemas/DirectMessagePolicy" }
3993
4333
  group_invites: { $ref: "#/components/schemas/GroupInvitePolicy" }
3994
4334
  search_visibility: { $ref: "#/components/schemas/SearchVisibility" }
4335
+ calls_enabled: { type: boolean }
3995
4336
  PrivacyExceptionsRequest:
3996
4337
  type: object
3997
4338
  required: [user_ids]
@@ -4242,13 +4583,14 @@ components:
4242
4583
  height: { type: [integer, "null"], minimum: 1 }
4243
4584
  SystemEventKind:
4244
4585
  type: string
4245
- enum: [group_created, member_added, member_joined, member_removed, member_left]
4586
+ enum: [group_created, member_added, member_joined, member_removed, member_left, call]
4246
4587
  SystemEvent:
4247
4588
  type: object
4248
4589
  description: |
4249
4590
  What a `system` message records; set on system messages only, whose `content` is then `null`. The names
4250
4591
  are the display names at the time of the event. `target` fields are set for `member_added` and
4251
- `member_removed`.
4592
+ `member_removed`. `call` is set for `call` messages: one per call, posted when the call ends, `actor` is the
4593
+ initiator.
4252
4594
  required: [kind, actor_id, actor_name]
4253
4595
  properties:
4254
4596
  kind: { $ref: "#/components/schemas/SystemEventKind" }
@@ -4259,6 +4601,7 @@ components:
4259
4601
  actor_name: { type: string }
4260
4602
  target_id: { $ref: "#/components/schemas/Id" }
4261
4603
  target_name: { type: string }
4604
+ call: { $ref: "#/components/schemas/CallSummary" }
4262
4605
  MessagePage:
4263
4606
  type: object
4264
4607
  required: [items, next_cursor, prev_cursor]
@@ -4862,3 +5205,229 @@ components:
4862
5205
  properties:
4863
5206
  message:
4864
5207
  type: string
5208
+
5209
+ # ----------------------------------------------------------- calls
5210
+ CallConfig:
5211
+ type: object
5212
+ required: [enabled, max_participants, ring_timeout_seconds]
5213
+ properties:
5214
+ enabled: { type: boolean }
5215
+ max_participants:
5216
+ type: integer
5217
+ minimum: 2
5218
+ description: Most sessions joined to one call at the same time.
5219
+ ring_timeout_seconds:
5220
+ type: integer
5221
+ minimum: 1
5222
+ description: How long an invitation rings before it counts as missed.
5223
+ CallIdentityRequest:
5224
+ type: object
5225
+ required: [public_key]
5226
+ properties:
5227
+ public_key: { $ref: "#/components/schemas/CallIdentityKey" }
5228
+ CallIdentityKey:
5229
+ type: string
5230
+ description: |
5231
+ P-256 public key as an uncompressed point (65 bytes, first byte `0x04`), base64url without padding.
5232
+ minLength: 87
5233
+ maxLength: 87
5234
+ pattern: "^[A-Za-z0-9_-]+$"
5235
+ CallState:
5236
+ type: string
5237
+ description: "`ringing` until someone answers, `active` while people are in it, `ended` afterwards."
5238
+ enum: [ringing, active, ended]
5239
+ CallEndReason:
5240
+ type: string
5241
+ description: |
5242
+ Why the call ended: `completed` (people talked), `missed` (nobody answered in time), `declined` (the peer of a
5243
+ direct call declined), `cancelled` (the caller hung up before an answer), `failed` (the media service lost
5244
+ the call).
5245
+ enum: [completed, missed, declined, cancelled, failed]
5246
+ CallParticipantState:
5247
+ type: string
5248
+ description: "`left` and `declined` can become `joined` again in a group call."
5249
+ enum: [ringing, joined, declined, missed, left]
5250
+ CallParticipant:
5251
+ type: object
5252
+ required: [user_id, state, session_id, identity_key, joined_at]
5253
+ properties:
5254
+ user_id: { $ref: "#/components/schemas/Id" }
5255
+ state: { $ref: "#/components/schemas/CallParticipantState" }
5256
+ session_id:
5257
+ description: The session that joined; `null` unless `state` is `joined`.
5258
+ oneOf:
5259
+ - { type: string, format: uuid }
5260
+ - type: "null"
5261
+ identity_key:
5262
+ description: Call identity of the joined session; `null` unless `state` is `joined`.
5263
+ oneOf:
5264
+ - $ref: "#/components/schemas/CallIdentityKey"
5265
+ - type: "null"
5266
+ joined_at:
5267
+ description: When the participant last joined; `null` if never.
5268
+ oneOf:
5269
+ - $ref: "#/components/schemas/Timestamp"
5270
+ - type: "null"
5271
+ Call:
5272
+ type: object
5273
+ required:
5274
+ - id
5275
+ - chat_id
5276
+ - kind
5277
+ - state
5278
+ - end_reason
5279
+ - initiator_id
5280
+ - created_at
5281
+ - started_at
5282
+ - ended_at
5283
+ - ring_expires_at
5284
+ - epoch
5285
+ - version
5286
+ - max_participants
5287
+ - participants
5288
+ properties:
5289
+ id: { $ref: "#/components/schemas/Id" }
5290
+ chat_id: { $ref: "#/components/schemas/Id" }
5291
+ kind:
5292
+ type: string
5293
+ enum: [direct, group]
5294
+ state: { $ref: "#/components/schemas/CallState" }
5295
+ end_reason:
5296
+ oneOf:
5297
+ - $ref: "#/components/schemas/CallEndReason"
5298
+ - type: "null"
5299
+ initiator_id: { $ref: "#/components/schemas/Id" }
5300
+ created_at: { $ref: "#/components/schemas/Timestamp" }
5301
+ started_at:
5302
+ description: First answer; `null` while nobody answered.
5303
+ oneOf:
5304
+ - $ref: "#/components/schemas/Timestamp"
5305
+ - type: "null"
5306
+ ended_at:
5307
+ oneOf:
5308
+ - $ref: "#/components/schemas/Timestamp"
5309
+ - type: "null"
5310
+ ring_expires_at:
5311
+ description: When ringing invitations of this call turn into `missed`.
5312
+ $ref: "#/components/schemas/Timestamp"
5313
+ epoch:
5314
+ type: integer
5315
+ minimum: 0
5316
+ description: Key epoch; increases whenever the set of joined sessions or their identity keys change.
5317
+ version:
5318
+ type: integer
5319
+ minimum: 1
5320
+ description: Increases with every change of the call; ignore events with a lower version than one already seen.
5321
+ max_participants: { type: integer, minimum: 2 }
5322
+ participants:
5323
+ type: array
5324
+ description: Everyone invited or joined, the initiator first.
5325
+ items: { $ref: "#/components/schemas/CallParticipant" }
5326
+ CallList:
5327
+ type: object
5328
+ required: [items]
5329
+ properties:
5330
+ items:
5331
+ type: array
5332
+ items: { $ref: "#/components/schemas/Call" }
5333
+ StartCallRequest:
5334
+ type: object
5335
+ required: [client_request_id]
5336
+ properties:
5337
+ client_request_id:
5338
+ type: string
5339
+ format: uuid
5340
+ description: Idempotency key chosen by the client.
5341
+ JoinCallRequest:
5342
+ type: object
5343
+ properties:
5344
+ takeover:
5345
+ type: boolean
5346
+ default: false
5347
+ description: Move the user's participation from another session to this one.
5348
+ CallGrant:
5349
+ type: object
5350
+ required: [url, token, identity, expires_at]
5351
+ properties:
5352
+ url:
5353
+ type: string
5354
+ description: WebSocket URL of the media server (`wss://`).
5355
+ token:
5356
+ type: string
5357
+ description: Media access token for this call and session only; microphone audio only.
5358
+ identity:
5359
+ type: string
5360
+ description: The participant identity inside the media room (`<user_id>:<session_id>`).
5361
+ expires_at: { $ref: "#/components/schemas/Timestamp" }
5362
+ CallJoin:
5363
+ type: object
5364
+ required: [call, grant]
5365
+ properties:
5366
+ call: { $ref: "#/components/schemas/Call" }
5367
+ grant: { $ref: "#/components/schemas/CallGrant" }
5368
+ CallKeyEnvelope:
5369
+ type: object
5370
+ description: An HPKE Auth-mode envelope carrying one sender key (see `docs/calls-e2ee.md`).
5371
+ required: [epoch, sender_user_id, sender_session_id, recipient_session_id, enc, ciphertext]
5372
+ properties:
5373
+ epoch: { type: integer, minimum: 0 }
5374
+ sender_user_id: { $ref: "#/components/schemas/Id" }
5375
+ sender_session_id: { type: string, format: uuid }
5376
+ recipient_session_id: { type: string, format: uuid }
5377
+ enc:
5378
+ type: string
5379
+ description: HPKE encapsulated key, base64url without padding.
5380
+ maxLength: 128
5381
+ pattern: "^[A-Za-z0-9_-]+$"
5382
+ ciphertext:
5383
+ type: string
5384
+ description: Sealed sender key, base64url without padding.
5385
+ maxLength: 256
5386
+ pattern: "^[A-Za-z0-9_-]+$"
5387
+ CallKeyEnvelopeList:
5388
+ type: object
5389
+ required: [call_id, epoch, items]
5390
+ properties:
5391
+ call_id: { $ref: "#/components/schemas/Id" }
5392
+ epoch: { type: integer, minimum: 0 }
5393
+ items:
5394
+ type: array
5395
+ items: { $ref: "#/components/schemas/CallKeyEnvelope" }
5396
+ SendCallKeysRequest:
5397
+ type: object
5398
+ required: [epoch, envelopes]
5399
+ properties:
5400
+ epoch: { type: integer, minimum: 0 }
5401
+ envelopes:
5402
+ type: array
5403
+ minItems: 1
5404
+ maxItems: 64
5405
+ items: { $ref: "#/components/schemas/OutgoingCallKeyEnvelope" }
5406
+ OutgoingCallKeyEnvelope:
5407
+ type: object
5408
+ required: [recipient_session_id, enc, ciphertext]
5409
+ properties:
5410
+ recipient_session_id: { type: string, format: uuid }
5411
+ enc:
5412
+ type: string
5413
+ maxLength: 128
5414
+ pattern: "^[A-Za-z0-9_-]+$"
5415
+ ciphertext:
5416
+ type: string
5417
+ maxLength: 256
5418
+ pattern: "^[A-Za-z0-9_-]+$"
5419
+ CallSummary:
5420
+ type: object
5421
+ description: The call a `call` system message records.
5422
+ required: [call_id, kind, outcome, duration_seconds]
5423
+ properties:
5424
+ call_id: { $ref: "#/components/schemas/Id" }
5425
+ kind:
5426
+ type: string
5427
+ enum: [direct, group]
5428
+ outcome: { $ref: "#/components/schemas/CallEndReason" }
5429
+ duration_seconds:
5430
+ description: From the first answer to the end; `null` if nobody answered.
5431
+ oneOf:
5432
+ - { type: integer, minimum: 0 }
5433
+ - type: "null"