From 077409f0d843464b50156868e9fd94c911b4cbf5 Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Mon, 5 Oct 2026 22:08:15 +0000 Subject: [PATCH 1/3] docs: add the X Calls API (preview) A Calls API product: introduction, quickstart, authentication, the media WebSocket, calling people, scheduled calls, an example agent, errors and limits, and an API reference rendered from xcalls/openapi.json (generated from the X API route registry's development version). --- docs.json | 55 + .../api-reference/cancel-a-scheduled-call.mdx | 4 + xcalls/api-reference/create-a-call.mdx | 4 + xcalls/api-reference/get-a-call.mdx | 4 + xcalls/api-reference/get-a-scheduled-call.mdx | 4 + xcalls/api-reference/join-a-call.mdx | 4 + xcalls/api-reference/leave-a-call.mdx | 4 + xcalls/api-reference/list-scheduled-calls.mdx | 4 + .../respond-to-a-scheduled-call.mdx | 4 + xcalls/api-reference/schedule-a-call.mdx | 4 + .../api-reference/update-a-scheduled-call.mdx | 4 + xcalls/authentication.mdx | 48 + xcalls/calling-people.mdx | 46 + xcalls/errors-and-limits.mdx | 60 + xcalls/example-agent.mdx | 117 + xcalls/introduction.mdx | 90 + xcalls/media-websocket.mdx | 122 + xcalls/openapi.json | 2101 +++++++++++++++++ xcalls/quickstart.mdx | 115 + xcalls/scheduled-calls.mdx | 184 ++ 20 files changed, 2978 insertions(+) create mode 100644 xcalls/api-reference/cancel-a-scheduled-call.mdx create mode 100644 xcalls/api-reference/create-a-call.mdx create mode 100644 xcalls/api-reference/get-a-call.mdx create mode 100644 xcalls/api-reference/get-a-scheduled-call.mdx create mode 100644 xcalls/api-reference/join-a-call.mdx create mode 100644 xcalls/api-reference/leave-a-call.mdx create mode 100644 xcalls/api-reference/list-scheduled-calls.mdx create mode 100644 xcalls/api-reference/respond-to-a-scheduled-call.mdx create mode 100644 xcalls/api-reference/schedule-a-call.mdx create mode 100644 xcalls/api-reference/update-a-scheduled-call.mdx create mode 100644 xcalls/authentication.mdx create mode 100644 xcalls/calling-people.mdx create mode 100644 xcalls/errors-and-limits.mdx create mode 100644 xcalls/example-agent.mdx create mode 100644 xcalls/introduction.mdx create mode 100644 xcalls/media-websocket.mdx create mode 100644 xcalls/openapi.json create mode 100644 xcalls/quickstart.mdx create mode 100644 xcalls/scheduled-calls.mdx diff --git a/docs.json b/docs.json index 9b0456ffe..8635a2049 100644 --- a/docs.json +++ b/docs.json @@ -1358,6 +1358,61 @@ } ] }, + { + "product": "Calls API", + "tabs": [ + { + "tab": "Documentation", + "groups": [ + { + "group": "Start here", + "pages": [ + "xcalls/introduction", + "xcalls/quickstart", + "xcalls/authentication" + ] + }, + { + "group": "Guides", + "pages": [ + "xcalls/media-websocket", + "xcalls/calling-people", + "xcalls/scheduled-calls", + "xcalls/example-agent", + "xcalls/errors-and-limits" + ] + }, + { + "group": "API reference", + "pages": [ + { + "group": "Calls", + "expanded": true, + "pages": [ + "xcalls/api-reference/create-a-call", + "xcalls/api-reference/get-a-call", + "xcalls/api-reference/join-a-call", + "xcalls/api-reference/leave-a-call" + ] + }, + { + "group": "Scheduled calls", + "expanded": true, + "pages": [ + "xcalls/api-reference/list-scheduled-calls", + "xcalls/api-reference/schedule-a-call", + "xcalls/api-reference/get-a-scheduled-call", + "xcalls/api-reference/update-a-scheduled-call", + "xcalls/api-reference/cancel-a-scheduled-call", + "xcalls/api-reference/respond-to-a-scheduled-call" + ] + } + ] + } + ] + } + ] + }, { "product": "Enterprise", "hidden": true, diff --git a/xcalls/api-reference/cancel-a-scheduled-call.mdx b/xcalls/api-reference/cancel-a-scheduled-call.mdx new file mode 100644 index 000000000..added6174 --- /dev/null +++ b/xcalls/api-reference/cancel-a-scheduled-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json DELETE /2/calls/scheduled/{id}" +playground: "none" +--- diff --git a/xcalls/api-reference/create-a-call.mdx b/xcalls/api-reference/create-a-call.mdx new file mode 100644 index 000000000..3eaa2ea4d --- /dev/null +++ b/xcalls/api-reference/create-a-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json POST /2/calls" +playground: "none" +--- diff --git a/xcalls/api-reference/get-a-call.mdx b/xcalls/api-reference/get-a-call.mdx new file mode 100644 index 000000000..e3282c271 --- /dev/null +++ b/xcalls/api-reference/get-a-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json GET /2/calls/{id}" +playground: "none" +--- diff --git a/xcalls/api-reference/get-a-scheduled-call.mdx b/xcalls/api-reference/get-a-scheduled-call.mdx new file mode 100644 index 000000000..5c2e2c7f3 --- /dev/null +++ b/xcalls/api-reference/get-a-scheduled-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json GET /2/calls/scheduled/{id}" +playground: "none" +--- diff --git a/xcalls/api-reference/join-a-call.mdx b/xcalls/api-reference/join-a-call.mdx new file mode 100644 index 000000000..ef14d3e3e --- /dev/null +++ b/xcalls/api-reference/join-a-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json POST /2/calls/{id}/join" +playground: "none" +--- diff --git a/xcalls/api-reference/leave-a-call.mdx b/xcalls/api-reference/leave-a-call.mdx new file mode 100644 index 000000000..899ea1843 --- /dev/null +++ b/xcalls/api-reference/leave-a-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json POST /2/calls/{id}/leave" +playground: "none" +--- diff --git a/xcalls/api-reference/list-scheduled-calls.mdx b/xcalls/api-reference/list-scheduled-calls.mdx new file mode 100644 index 000000000..55b5c4089 --- /dev/null +++ b/xcalls/api-reference/list-scheduled-calls.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json GET /2/calls/scheduled" +playground: "none" +--- diff --git a/xcalls/api-reference/respond-to-a-scheduled-call.mdx b/xcalls/api-reference/respond-to-a-scheduled-call.mdx new file mode 100644 index 000000000..c9a365c57 --- /dev/null +++ b/xcalls/api-reference/respond-to-a-scheduled-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json POST /2/calls/scheduled/{id}/rsvp" +playground: "none" +--- diff --git a/xcalls/api-reference/schedule-a-call.mdx b/xcalls/api-reference/schedule-a-call.mdx new file mode 100644 index 000000000..e1e4e7bb0 --- /dev/null +++ b/xcalls/api-reference/schedule-a-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json POST /2/calls/scheduled" +playground: "none" +--- diff --git a/xcalls/api-reference/update-a-scheduled-call.mdx b/xcalls/api-reference/update-a-scheduled-call.mdx new file mode 100644 index 000000000..df3321f9b --- /dev/null +++ b/xcalls/api-reference/update-a-scheduled-call.mdx @@ -0,0 +1,4 @@ +--- +openapi: "/xcalls/openapi.json PUT /2/calls/scheduled/{id}" +playground: "none" +--- diff --git a/xcalls/authentication.mdx b/xcalls/authentication.mdx new file mode 100644 index 000000000..b5e0496d1 --- /dev/null +++ b/xcalls/authentication.mdx @@ -0,0 +1,48 @@ +--- +title: Authentication +sidebarTitle: Authentication +description: "X Calls API endpoints take a user-context token for the account your agent speaks as; the media WebSocket takes the single-use media token from the join response." +keywords: ["X Calls API authentication", "OAuth 2.0 scopes", "user context", "dm.write", "media token", "OAuth 1.0a"] +--- + +Every X Calls API endpoint needs a **user-context** token for the account your agent speaks as. App-only bearer tokens are rejected with `403 Unsupported Authentication`, because a call participant is always a user. + +## OAuth 2.0 + +Use the Authorization Code flow with PKCE. The token must carry the endpoint's scopes, or the request fails with `403 Missing OAuth2 Scopes`: + +| Endpoint | Scopes | +|:---------|:-------| +| `POST /2/calls` | `tweet.read` `users.read` `dm.write` | +| `GET /2/calls/:id` | `tweet.read` `users.read` `dm.read` | +| `POST /2/calls/:id/join` | `tweet.read` `users.read` `dm.write` | +| `POST /2/calls/:id/leave` | `tweet.read` `users.read` `dm.write` | +| `GET /2/calls/scheduled` | `tweet.read` `users.read` `dm.read` | +| `POST /2/calls/scheduled` | `tweet.read` `users.read` `dm.write` | +| `GET /2/calls/scheduled/:id` | `tweet.read` `users.read` `dm.read` | +| `PUT /2/calls/scheduled/:id` | `tweet.read` `users.read` `dm.write` | +| `DELETE /2/calls/scheduled/:id` | `tweet.read` `users.read` `dm.write` | +| `POST /2/calls/scheduled/:id/rsvp` | `tweet.read` `users.read` `dm.write` | + +## OAuth 1.0a + +With OAuth 1.0a user context, your app needs the **Read, write, and Direct Messages** permission. + +## What your user may do + +Calls follow the same rules as calls placed from the X app: + +- Your bot joins a call only as its [join policy](/xcalls/introduction#join-policies) allows. +- It can start a call in a conversation only if it is a member of that conversation. +- Only a scheduled call's organizer changes or cancels it, and only its invitees answer it. See [Scheduled calls](/xcalls/scheduled-calls). + +## The media token + +The media WebSocket does **not** take your OAuth token. It takes the single-use `media_token` from the [join response](/xcalls/api-reference/join-a-call), so the socket can be opened by another process, such as your media server, without sharing user credentials. The token is valid for about 2 minutes (`expires_at`) and works once. + +```text +GET wss://api.x.com/2/calls/media +Authorization: Bearer +``` + +Clients that cannot set headers, such as browsers, pass `?token=` instead. See [Media WebSocket](/xcalls/media-websocket). diff --git a/xcalls/calling-people.mdx b/xcalls/calling-people.mdx new file mode 100644 index 000000000..ffbf50487 --- /dev/null +++ b/xcalls/calling-people.mdx @@ -0,0 +1,46 @@ +--- +title: Calling people +sidebarTitle: Calling people +description: "Ring people from your bot's XChat conversations, answer calls started in a conversation with your bot, and admit people to the closed calls your bot hosts." +keywords: ["call someone", "ring", "answer a call", "conversation_id", "closed call", "join.requested", "join.admit"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +## Calling someone + +Start the call in your bot's conversation with them, then join it: + + + + `POST /2/calls` with `conversation_id`: a 1:1 id (`-`, as the chat endpoints return it, or `:`) or a group id (`g…`). Your bot must be a member of the conversation. + + + `POST /2/calls/:id/join`, then open the [media WebSocket](/xcalls/media-websocket). + + + When the host (your bot) enters the empty call, X rings the other members: in a group, every other member; in a 1:1 conversation, the other person, if X's messaging trust check allows your account to reach them. Ringing is a VoIP push, which reaches the X and XChat apps on iOS; people on other devices join from the conversation or the call link. + + + +If nobody joins within 60 seconds, your agent's session ends with `alone`. + +If the conversation already has a call in progress, `POST /2/calls` returns **that** call instead of a new one: its `host_id` is whoever started it, and its title and policy are theirs. + +## Answering a call + +X does not notify bots of incoming calls yet. When you know someone has started a call in a conversation with your bot, for example because they messaged your bot, `POST /2/calls` with that conversation's id returns the call in progress (`host_id` is the caller), and joining it answers. + +Conversation ids are matched exactly, so use the id in the form the caller's call uses (the chat endpoints return 1:1 ids as `-`). If no call is in progress, the request starts a new one. + +## Hosting a closed call + +When your bot hosts a `closed` or `trusted` call, people who are not admitted automatically ask to join. Your agent receives `join.requested` and answers with `join.admit` or `join.reject`: + +```json +{ "type": "join.admit", "user_id": "783214" } +``` + +Requests made while your agent is not in the call are delivered once it joins. diff --git a/xcalls/errors-and-limits.mdx b/xcalls/errors-and-limits.mdx new file mode 100644 index 000000000..1c9d8bf28 --- /dev/null +++ b/xcalls/errors-and-limits.mdx @@ -0,0 +1,60 @@ +--- +title: Errors and limits +sidebarTitle: Errors and limits +description: "The errors X Calls API endpoints return, in the standard X API problem format, and the limits on media tokens, sessions, admission, audio and calls." +keywords: ["X Calls API errors", "problem types", "403", "404", "409", "limits", "media token", "alone timeout"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +## Errors + +Errors use the standard X API problem format (`type`, `title`, `detail`). Lookups (`GET /2/calls/:id`, `GET /2/calls/scheduled/:id`) return an unknown or unavailable resource as a `200` with the problem under `errors`, like other X API lookups. + +| Status | `type` | When | +|:-------|:-------|:-----| +| `400` | `https://api.x.com/2/problems/invalid-request` | A malformed body (for example an unknown `media.type`) or a value calls reject (an unsupported `sample_rate`, a `conversation_id` that is not an XChat id). `errors[]` says what to fix. | +| `403` | `https://api.x.com/2/problems/unsupported-authentication` | An app-only token. Use a user-context token. | +| `403` | `https://api.x.com/2/problems/missing-scopes` | An OAuth 2.0 token without the endpoint's scopes. | +| `403` | `https://api.x.com/2/problems/oauth1-permissions` | An OAuth 1.0a app without the Direct Messages permission. | +| `403` | `https://api.x.com/2/problems/not-authorized-for-resource` | Your user may not join or act on this call, or change, cancel or answer this scheduled call (only its organizer changes and cancels it; only invitees answer). | +| `403` | `about:blank` | Your user is not a member of `conversation_id`, or may not schedule calls. | +| `404` | `https://api.x.com/2/problems/resource-not-found` | Join or leave on an unknown or ended call, or a change to an unknown scheduled call. | +| `409` | `https://api.x.com/2/problems/conflict` | A change to a scheduled call that was cancelled. | +| `429` | rate limit | Too many requests for your plan. | +| `503` | `about:blank` | Calls are temporarily unavailable, or a conversation's membership could not be checked. Retry with backoff. | + +An unknown call looked up with `GET /2/calls/:id`: + +```json +{ + "errors": [ + { + "value": "zzz-zzz-zzz", + "detail": "Could not find call with id: [zzz-zzz-zzz].", + "title": "Not Found Error", + "resource_type": "call", + "parameter": "id", + "resource_id": "zzz-zzz-zzz", + "type": "https://api.x.com/2/problems/resource-not-found" + } + ] +} +``` + +Errors on the media WebSocket arrive as `error` events and `session.ended` reasons. See [Media WebSocket](/xcalls/media-websocket#error-codes). + +## Limits + +| Limit | Value | +|:------|:------| +| Media token | Single use, valid about 2 minutes (`expires_at`). | +| Sessions per user per call | 1; joining again replaces the earlier session, which ends with `superseded`. | +| Waiting for the host to admit your bot | 5 minutes. | +| Alone in a call | The session ends after 60 seconds. | +| Agent audio queued ahead of playback | 2 minutes; beyond that the oldest audio is dropped. | +| Audio | Mono PCM16 at 8, 16, 24 or 48 kHz; no video. | +| Calls | X calls (XChat). Legacy 1:1 DM calls are not supported. | +| Scheduled call window | `GET /2/calls/scheduled` covers at most 7 days per request. | diff --git a/xcalls/example-agent.mdx b/xcalls/example-agent.mdx new file mode 100644 index 000000000..52a14cb04 --- /dev/null +++ b/xcalls/example-agent.mdx @@ -0,0 +1,117 @@ +--- +title: Example agent +sidebarTitle: Example agent +description: "A complete X Calls agent in plain Node.js: join a call, stream its audio, speak, stop when someone interrupts, and leave cleanly; plus how to bridge a realtime speech-to-speech model." +keywords: ["X Calls agent example", "Node.js", "media WebSocket", "realtime model", "speech-to-speech", "barge-in"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +This agent joins a call, plays a greeting tone where your text-to-speech would speak, stops talking when someone interrupts, and logs events and audio levels. It runs on Node.js 22 or later with no dependencies. + +```bash +USER_ACCESS_TOKEN= node agent.mjs t69-8sb-6z5 +``` + +```javascript agent.mjs +// A minimal X Calls agent: joins a call as your bot, plays a greeting tone +// (stand-in for your TTS), stops talking when someone interrupts, and logs +// what happens. Node.js 22+ with no dependencies. +// +// USER_ACCESS_TOKEN= node agent.mjs + +const API = "https://api.x.com/2"; +const RATE = 16000; // Hz, PCM16 mono, both directions + +async function join(callId) { + const res = await fetch(`${API}/calls/${callId}/join`, { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.USER_ACCESS_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + media: { type: "websocket", audio: { encoding: "pcm16", sample_rate: RATE } }, + }), + }); + const body = await res.json(); + if (!res.ok) throw new Error(`join failed: ${res.status} ${JSON.stringify(body)}`); + return body.data; +} + +// PCM16 little-endian, whatever the host's byte order. +function tone(hz, seconds) { + const n = Math.round(RATE * seconds); + const view = new DataView(new ArrayBuffer(n * 2)); + for (let i = 0; i < n; i++) { + view.setInt16(i * 2, Math.round(6000 * Math.sin((2 * Math.PI * hz * i) / RATE)), true); + } + return view.buffer; +} + +function level(buffer) { + const view = new DataView(buffer); + let sum = 0; + for (let i = 0; i + 1 < view.byteLength; i += 2) sum += view.getInt16(i, true) ** 2; + return Math.round(Math.sqrt(sum / Math.max(1, view.byteLength / 2))); +} + +function runAgent(session) { + const url = `${session.media_url}?token=${encodeURIComponent(session.media_token)}`; + const ws = new WebSocket(url); + ws.binaryType = "arraybuffer"; + const send = (command) => ws.send(JSON.stringify(command)); + let frames = 0; + let loudest = 0; + + ws.onmessage = ({ data }) => { + if (data instanceof ArrayBuffer) { + // 20 ms of everyone else in the call: feed it to your model here. + loudest = Math.max(loudest, level(data)); + if (++frames % 50 === 0) { + console.log(`heard ${frames / 50}s, loudest ${loudest}`); + loudest = 0; + } + return; + } + const event = JSON.parse(data); + console.log(event.type, JSON.stringify(event)); + switch (event.type) { + case "session.joined": + ws.send(tone(440, 2)); // any chunk size; X paces it into the call + send({ type: "audio.mark", name: "greeting" }); + break; + case "playback.mark": + if (event.name === "greeting" && !event.cleared) console.log("greeting played"); + break; + case "speech.started": + send({ type: "audio.clear" }); // let the person talk + break; + case "session.ended": + ws.close(); + break; + } + }; + ws.onclose = () => process.exit(0); + process.on("SIGINT", () => { + if (ws.readyState === WebSocket.OPEN) send({ type: "leave" }); + else process.exit(0); + }); +} + +runAgent(await join(process.argv[2])); +``` + +## Bridging a realtime model + +To connect a realtime speech-to-speech model instead, keep the same loop: + +1. Join at the model's native rate (for example 24 kHz) so no resampling is needed. +2. Forward each binary frame from X to the model's audio input. +3. Forward each audio chunk the model produces to X as a binary frame. +4. When the model reports that the user started speaking (or X sends `speech.started`), send `audio.clear`. +5. On `session.ended`, close the model session. + +See [Media WebSocket](/xcalls/media-websocket) for every event and command. diff --git a/xcalls/introduction.mdx b/xcalls/introduction.mdx new file mode 100644 index 000000000..fe55912ef --- /dev/null +++ b/xcalls/introduction.mdx @@ -0,0 +1,90 @@ +--- +title: X Calls API +sidebarTitle: Introduction +description: "Put a voice agent in an X call: start and join calls as your app's user, stream raw call audio both ways over a WebSocket, and schedule calls on the X Calls calendar." +keywords: ["X Calls API", "Calls API", "voice agent", "X call", "media WebSocket", "PCM16", "scheduled calls"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +The X Calls API puts a voice agent in an X call. Your agent joins as your app's user, typically a bot account, hears everyone in the call and talks back, like a person in the X app. Use it to: + +- **Call people**: start a call in a conversation with your bot; X rings the other members and your agent greets whoever picks up. +- **Host calls**: start a call, share its link, and let your agent run it, admitting people if the call is closed. +- **Join calls** your bot was given the link to: a meeting assistant, an interpreter, a game host. +- **Schedule calls** on your user's X Calls calendar, and find the calls on it to join. + +You bring the agent: a speech-to-speech model, or speech-to-text → LLM → text-to-speech, connected to a WebSocket that streams raw audio both ways. X handles everything about the call itself. + +Endpoints are hosted under `https://api.x.com`. + + + + Create a call and put an agent in it. + + + Audio frames, events and commands. + + + Schedule, list, move and cancel calls. + + + +--- + +## How it works + +```text + your app X API X call + ──────── ───── ────── + POST /2/calls ───────────────▶ create the call ───────────────▶ (empty call) + POST /2/calls/:id/join ──────▶ admit your user, mint a + ◀── media_url + media_token (single use, ~2 min) + WebSocket media_url ─────────▶ your user enters the call ─────▶ participants see + (Bearer media_token) and hear your bot + ◀── binary: call audio (PCM16) ──────────────────────────────── everyone else + ─── binary: agent audio (PCM16) ───────────────────────────────▶ everyone hears it + ◀── text: events (participant.joined, speech.started, …) + POST /2/calls/:id/leave ─────▶ (or send {"type":"leave"}, or close the socket) +``` + +## Core concepts + +- **Call**: has a host, a join policy and a short id such as `t69-8sb-6z5`. Its link is `https://call.x.com/{id}`. Anyone with the id can open it, so treat the id like a secret. +- **Media session**: joining starts a media session for your user. Your user enters the call when your WebSocket connects and leaves when it closes. A user has one session per call. +- **Audio**: plain PCM on the WebSocket, 16-bit little-endian mono at the sample rate you choose (8, 16, 24 or 48 kHz). X handles WebRTC, Opus, jitter and mixing: you receive one mixed stream of everyone else (never your own agent) and send one stream back. +- **Scheduled call**: an X call on the X Calls calendar, with a time, a title, the people invited, and optionally a recurrence. Scheduling creates the call right away, so its link can be shared at once. + +### Join policies + +The join policy decides who gets in: + +| Policy | Who joins directly | +|:-------|:-------------------| +| `open` | Anyone with the id, including people who are not logged in. | +| `closed` | The host, people the host admitted, and people already in the call. Everyone else asks to join. | +| `trusted` | Like `closed`, plus members of the host's Verified Organization. | + +When your bot must be admitted, the join response says `"admission": "requires_approval"` and your WebSocket receives `session.waiting` until the host answers. + +### Key identifiers + +| Identifier | Example | What it is | +|:-----------|:--------|:-----------| +| Call id | `t69-8sb-6z5` | Identifies a call (`id`, or `call_id` on a scheduled call). Also its join secret. | +| Session id | `cs_4f1c2a9e0b7d3a51` | Identifies a media session in events and logs. | +| Media token | `eyJhbGciOiJIUzI1NiJ9…` | Single-use credential for the media WebSocket, valid about 2 minutes. | +| Scheduled call id | `1840000000000000000` | Identifies a call on the X Calls calendar: what change, cancel and rsvp take. It is not the call id. | +| Conversation id | `-`, `g…` | The XChat conversation a call is started in: a 1:1 or a group conversation. | + +--- + +## Next steps + +1. [Quickstart](/xcalls/quickstart): create a call and connect an agent +2. [Authentication](/xcalls/authentication): tokens, scopes and the media token +3. [Media WebSocket](/xcalls/media-websocket): audio, events and commands +4. [Calling people](/xcalls/calling-people): ring someone, answer, host a closed call +5. [Scheduled calls](/xcalls/scheduled-calls): put calls on the calendar and find them diff --git a/xcalls/media-websocket.mdx b/xcalls/media-websocket.mdx new file mode 100644 index 000000000..391ba3f7b --- /dev/null +++ b/xcalls/media-websocket.mdx @@ -0,0 +1,122 @@ +--- +title: Media WebSocket +sidebarTitle: Media WebSocket +description: "The X Calls media WebSocket streams a call's audio to your agent and your agent's voice back as raw PCM16, with JSON events and commands for participants, turn-taking, chat and admission." +keywords: ["media WebSocket", "PCM16", "call audio", "session.joined", "speech.started", "audio.clear", "barge-in", "voice agent"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +After [joining a call](/xcalls/api-reference/join-a-call), your agent takes part in it over a WebSocket: it receives the call's audio and events, and sends its voice and commands. + +## Connecting + +Open a WebSocket to the `media_url` from the join response, with the `media_token` as a bearer credential: + +```text +GET wss://api.x.com/2/calls/media +Authorization: Bearer +``` + +Clients that cannot set headers (browsers, the WHATWG `WebSocket`) pass `?token=` instead. Always use the `media_url` from the join response. + +If the upgrade is refused, the HTTP response says why: + +| Status | Meaning | +|:-------|:--------| +| `401` | Missing, malformed or expired token. Join again for a new one. | +| `409` | The token was already used. | +| `503` | No capacity right now; retry with backoff. | + +## Frames + +**Binary frames are audio**: PCM16 little-endian, mono, at the session's sample rate. + +- **From X**: the mix of everyone in the call except your agent, one 20 ms frame at a time in real time (at 16 kHz, 320 samples = 640 bytes). Silence arrives as zeros, so the stream never stalls. +- **To X**: your agent's voice, in chunks of any size. Send it as fast as you produce it; X queues it and plays it into the call in real time. + +**Text frames are JSON**: events from X and commands to X, each with a `type`. + +## Session lifecycle + +```text +connect ─▶ session.created ─┬───────────────────▶ session.joined ─▶ … ─▶ session.ended + └─▶ session.waiting ─┘ (host admitted) +``` + +1. `session.created`: the token was accepted. Carries the audio format. +2. `session.waiting`: only when the join response said `requires_approval`. The host was asked to admit your bot. The session ends with `rejected` if the host declines, or `approval_timeout` if nobody answers within 5 minutes. +3. `session.joined`: your agent is in the call and audio flows. Carries the people already there. +4. `session.ended`: the last event. X then closes the socket. + +## Events (X → your agent) + +| `type` | Fields | Meaning | +|:-------|:-------|:--------| +| `session.created` | `session_id`, `call_id`, `audio` {`encoding`, `sample_rate`, `channels`, `frame_ms`} | Token accepted; connecting to the call. | +| `session.waiting` | | Waiting for the host to admit your bot. | +| `session.joined` | `participants`: [participant] | Your agent is in the call. | +| `participant.joined` | `participant` | Someone joined. | +| `participant.left` | `user_id` | Someone left. | +| `participant.updated` | `participant` | Someone muted or unmuted, or a guest's name became known. | +| `join.requested` | `participant` | Someone asked to join. Answer with `join.admit` or `join.reject`. | +| `speech.started` | `user_id` | Someone started talking. A good moment to stop your agent (`audio.clear`). | +| `speech.stopped` | `user_id` | They stopped talking. | +| `chat.message` | `user_id`, `text` | A message in the call's chat. | +| `reaction` | `user_id`, `emoji` | An emoji reaction in the call. | +| `playback.mark` | `name`, `cleared` | Everything queued before your `audio.mark` has played (`cleared: true`: it was dropped by `audio.clear` or `mute` instead). | +| `session.ended` | `reason` | The session is over. | +| `error` | `code`, `message` | A command failed, or the session failed (`session_failed`). | + +A **participant** is `{"user_id": "…", "is_guest": false, "muted": false}`. `is_guest` marks logged-out guests and room systems. Participants are the people whose audio is in the call. `muted` is left out until they have muted or unmuted, and a logged-out guest can first appear with `is_guest: false` and be corrected by a `participant.updated`. + +### Error codes + +| `code` | Meaning | +|:-------|:--------| +| `invalid_command` | Not a known command. | +| `invalid_audio` | `audio.append` without base64 PCM. | +| `unknown_join_request` | `join.admit` or `join.reject` for someone who has not asked. | +| `command_forbidden` | Your user may not do that in this call. | +| `command_failed` | Try again. | +| `session_failed` | Join the call again. | + +### Why a session ended + +| `reason` | Meaning | +|:---------|:--------| +| `left` | Your agent sent `leave`, or you called `POST /2/calls/:id/leave`. | +| `agent_disconnected` | Your WebSocket closed. | +| `rejected` | The host declined to admit your bot. | +| `approval_timeout` | Nobody admitted your bot within 5 minutes. | +| `call_not_found` | The call does not exist or has ended. | +| `forbidden` | Your user may not join this call. | +| `alone` | Nobody else was in the call for 60 seconds, including right after joining an empty call. | +| `superseded` | Your user joined the same call again from another session. | +| `media_failed` | The call's audio could not be connected, or was lost. | +| `shutdown` | The service is restarting; join again. | +| `error` | Anything else; join again. | + +## Commands (your agent → X) + +| `type` | Fields | Effect | +|:-------|:-------|:-------| +| `audio.append` | `audio` (base64 PCM16) | Same as a binary frame, for clients that cannot send binary. | +| `audio.clear` | | Drop agent audio that has not played yet (barge-in). | +| `audio.mark` | `name` | Get a `playback.mark` once everything queued so far has played. | +| `mute` / `unmute` | | Show your agent as muted or unmuted. A muted agent is not heard, and its queued audio is dropped. | +| `chat.send` | `text` | Post in the call's chat. | +| `reaction.send` | `emoji` | Send an emoji reaction. | +| `join.admit` | `user_id` | Let in someone who asked to join (host or admitted participant). | +| `join.reject` | `user_id` | Turn them away (host only). | +| `leave` | | Leave the call; the session ends with `left`. | + +## Turn-taking + +Speech detection runs on X's side for each participant: + +1. On `speech.started` while your agent is talking, send `audio.clear` to stop it mid-sentence, or keep talking if you prefer. +2. Feed the call audio to your model continuously; most realtime models do their own voice-activity detection too. +3. Send `audio.mark` after each agent turn to learn when the listener has heard it, for example before leaving after "goodbye". diff --git a/xcalls/openapi.json b/xcalls/openapi.json new file mode 100644 index 000000000..42088105b --- /dev/null +++ b/xcalls/openapi.json @@ -0,0 +1,2101 @@ +{ + "components": { + "parameters": {}, + "schemas": { + "CancelScheduledCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/CancelScheduledCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "CancelScheduledCallResponseData": { + "additionalProperties": false, + "properties": { + "cancelled": { + "description": "Whether the occurrences are cancelled.", + "type": "boolean" + } + }, + "type": "object" + }, + "CreateCallRequest": { + "additionalProperties": false, + "properties": { + "audio_only": { + "description": "Start the call without video.", + "type": "boolean" + }, + "conversation_id": { + "description": "XChat conversation to start the call in.", + "maxLength": 128, + "pattern": "^[A-Za-z0-9:_-]+$", + "type": "string" + }, + "join_policy": { + "description": "open (default): anyone with the link joins; closed: the host admits each person; trusted: members of the host's Verified Organization join, others ask.", + "enum": [ + "open", + "closed", + "trusted" + ], + "type": "string" + }, + "title": { + "description": "Title shown to participants.", + "maxLength": 100, + "type": "string" + } + }, + "type": "object" + }, + "CreateCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/CreateCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "CreateCallResponseData": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "description": "XChat conversation the call belongs to, if any.", + "type": "string" + }, + "created_at": { + "description": "Creation time (RFC 3339).", + "type": "string" + }, + "host_id": { + "description": "X user id of the call's host.", + "type": "string" + }, + "id": { + "description": "Unique identifier of the call; also its join secret.", + "type": "string" + }, + "join_policy": { + "description": "Who may join: open (anyone with the link), closed (the host admits people), or trusted (the host's Verified Organization).", + "type": "string" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "participant_count": { + "description": "People in the call right now.", + "type": "integer" + }, + "title": { + "description": "Title of the call.", + "type": "string" + }, + "viewer_may_join": { + "description": "Whether the authenticated user may join without asking the host.", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "DisallowedResourceProblem": { + "properties": { + "detail": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "section": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/disallowed-resource" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail" + ], + "type": "object" + }, + "DuplicateRuleProblem": { + "properties": { + "detail": { + "type": "string" + }, + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.twitter.com/2/problems/duplicate-rules" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "value" + ], + "type": "object" + }, + "Error": { + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "FieldHydrationFailureProblem": { + "properties": { + "detail": { + "type": "string" + }, + "field": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "section": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/field-hydration-failure" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "field" + ], + "type": "object" + }, + "FieldUnauthorizedProblem": { + "properties": { + "detail": { + "type": "string" + }, + "field": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "section": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/field-unauthorized" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "field" + ], + "type": "object" + }, + "GetCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/GetCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "GetCallResponseData": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "description": "XChat conversation the call belongs to, if any.", + "type": "string" + }, + "created_at": { + "description": "Creation time (RFC 3339).", + "type": "string" + }, + "host_id": { + "description": "X user id of the call's host.", + "type": "string" + }, + "id": { + "description": "Unique identifier of the call; also its join secret.", + "type": "string" + }, + "join_policy": { + "description": "Who may join: open (anyone with the link), closed (the host admits people), or trusted (the host's Verified Organization).", + "type": "string" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "participant_count": { + "description": "People in the call right now.", + "type": "integer" + }, + "title": { + "description": "Title of the call.", + "type": "string" + }, + "viewer_may_join": { + "description": "Whether the authenticated user may join without asking the host.", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "GetScheduledCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/GetScheduledCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "GetScheduledCallResponseData": { + "additionalProperties": false, + "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, + "call_id": { + "description": "The X call people join; also its join secret.", + "type": "string" + }, + "description": { + "description": "What the call is about.", + "type": "string" + }, + "end_time": { + "description": "End of this occurrence (RFC 3339).", + "type": "string" + }, + "id": { + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "type": "string" + }, + "invitees": { + "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "items": { + "$ref": "#/components/schemas/GetScheduledCallResponseDataInvitees" + }, + "type": "array" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "occurrence_start_time": { + "description": "For a recurring call, this occurrence's original start (RFC 3339): pass it as occurrence_start_time to change, cancel or answer just this occurrence.", + "type": "string" + }, + "organizer_id": { + "description": "X user id of the call's organizer.", + "type": "string" + }, + "recurrence": { + "description": "RFC 5545 RRULE the call repeats by, such as FREQ=WEEKLY;BYDAY=MO.", + "type": "string" + }, + "start_time": { + "description": "Start of this occurrence (RFC 3339).", + "type": "string" + }, + "status": { + "description": "scheduled, or cancelled for a cancelled occurrence.", + "type": "string" + }, + "time_zone": { + "description": "IANA time zone the recurrence follows.", + "type": "string" + }, + "title": { + "description": "Title of the call.", + "type": "string" + } + }, + "type": "object" + }, + "GetScheduledCallResponseDataInvitees": { + "additionalProperties": false, + "properties": { + "id": { + "description": "X user id of the invitee.", + "type": "string" + }, + "response": { + "description": "accepted, declined, tentative, or none for no answer yet.", + "type": "string" + } + }, + "type": "object" + }, + "InternalErrorProblem": { + "properties": { + "detail": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/internal-error" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail" + ], + "type": "object" + }, + "InvalidRequestProblem": { + "properties": { + "detail": { + "type": "string" + }, + "parameter": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/invalid-request" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail" + ], + "type": "object" + }, + "InvalidRuleProblem": { + "properties": { + "detail": { + "type": "string" + }, + "details": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.twitter.com/2/problems/invalid-rules" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "details" + ], + "type": "object" + }, + "JoinCallMedia": { + "additionalProperties": false, + "properties": { + "audio": { + "$ref": "#/components/schemas/JoinCallMediaAudio", + "description": "The audio format your agent sends and receives." + }, + "type": { + "description": "websocket (default): your agent over the media WebSocket.", + "enum": [ + "websocket" + ], + "type": "string" + } + }, + "type": "object" + }, + "JoinCallMediaAudio": { + "additionalProperties": false, + "properties": { + "encoding": { + "description": "Audio encoding on the media WebSocket (PCM16 little-endian mono).", + "enum": [ + "pcm16" + ], + "type": "string" + }, + "sample_rate": { + "description": "8000, 16000, 24000 (default) or 48000.", + "type": "integer" + } + }, + "type": "object" + }, + "JoinCallRequest": { + "additionalProperties": false, + "properties": { + "media": { + "$ref": "#/components/schemas/JoinCallMedia", + "description": "The media session to start." + } + }, + "type": "object" + }, + "JoinCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/JoinCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "JoinCallResponseData": { + "additionalProperties": false, + "properties": { + "admission": { + "description": "immediate, or requires_approval when the host must admit the session.", + "type": "string" + }, + "call": { + "$ref": "#/components/schemas/JoinCallResponseDataCall", + "description": "The call." + }, + "expires_at": { + "description": "When media_token expires (RFC 3339).", + "type": "string" + }, + "media_token": { + "description": "Bearer token for media_url; single use, short-lived.", + "type": "string" + }, + "media_type": { + "description": "websocket.", + "type": "string" + }, + "media_url": { + "description": "Where to open the media WebSocket.", + "type": "string" + }, + "session_id": { + "description": "Media session id.", + "type": "string" + } + }, + "required": [ + "session_id" + ], + "type": "object" + }, + "JoinCallResponseDataCall": { + "additionalProperties": false, + "properties": { + "conversation_id": { + "description": "XChat conversation the call belongs to, if any.", + "type": "string" + }, + "created_at": { + "description": "Creation time (RFC 3339).", + "type": "string" + }, + "host_id": { + "description": "X user id of the call's host.", + "type": "string" + }, + "id": { + "description": "Unique identifier of the call; also its join secret.", + "type": "string" + }, + "join_policy": { + "description": "Who may join: open (anyone with the link), closed (the host admits people), or trusted (the host's Verified Organization).", + "type": "string" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "participant_count": { + "description": "People in the call right now.", + "type": "integer" + }, + "title": { + "description": "Title of the call.", + "type": "string" + }, + "viewer_may_join": { + "description": "Whether the authenticated user may join without asking the host.", + "type": "boolean" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "LeaveCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/LeaveCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "LeaveCallResponseData": { + "additionalProperties": false, + "properties": { + "left": { + "description": "Whether the user left the call.", + "type": "boolean" + } + }, + "type": "object" + }, + "ListScheduledCallsResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ListScheduledCallsResponseData" + }, + "type": "array" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + }, + "meta": { + "properties": { + "result_count": { + "description": "Number of items in the data array.", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "ListScheduledCallsResponseData": { + "additionalProperties": false, + "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, + "call_id": { + "description": "The X call people join; also its join secret.", + "type": "string" + }, + "description": { + "description": "What the call is about.", + "type": "string" + }, + "end_time": { + "description": "End of this occurrence (RFC 3339).", + "type": "string" + }, + "id": { + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "type": "string" + }, + "invitees": { + "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "items": { + "$ref": "#/components/schemas/ListScheduledCallsResponseDataInvitees" + }, + "type": "array" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "occurrence_start_time": { + "description": "For a recurring call, this occurrence's original start (RFC 3339): pass it as occurrence_start_time to change, cancel or answer just this occurrence.", + "type": "string" + }, + "organizer_id": { + "description": "X user id of the call's organizer.", + "type": "string" + }, + "recurrence": { + "description": "RFC 5545 RRULE the call repeats by, such as FREQ=WEEKLY;BYDAY=MO.", + "type": "string" + }, + "start_time": { + "description": "Start of this occurrence (RFC 3339).", + "type": "string" + }, + "status": { + "description": "scheduled, or cancelled for a cancelled occurrence.", + "type": "string" + }, + "time_zone": { + "description": "IANA time zone the recurrence follows.", + "type": "string" + }, + "title": { + "description": "Title of the call.", + "type": "string" + } + }, + "type": "object" + }, + "ListScheduledCallsResponseDataInvitees": { + "additionalProperties": false, + "properties": { + "id": { + "description": "X user id of the invitee.", + "type": "string" + }, + "response": { + "description": "accepted, declined, tentative, or none for no answer yet.", + "type": "string" + } + }, + "type": "object" + }, + "NotAuthorizedForFieldProblem": { + "properties": { + "detail": { + "type": "string" + }, + "field": { + "type": "string" + }, + "parameter": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "section": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/not-authorized-for-field" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "field" + ], + "type": "object" + }, + "NotAuthorizedForResourceProblem": { + "properties": { + "detail": { + "type": "string" + }, + "parameter": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "section": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/not-authorized-for-resource" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "resource_type" + ], + "type": "object" + }, + "Problem": { + "discriminator": { + "mapping": { + "https://api.twitter.com/2/problems/duplicate-rules": "#/components/schemas/DuplicateRuleProblem", + "https://api.twitter.com/2/problems/invalid-rules": "#/components/schemas/InvalidRuleProblem", + "https://api.x.com/2/problems/disallowed-resource": "#/components/schemas/DisallowedResourceProblem", + "https://api.x.com/2/problems/field-hydration-failure": "#/components/schemas/FieldHydrationFailureProblem", + "https://api.x.com/2/problems/field-unauthorized": "#/components/schemas/FieldUnauthorizedProblem", + "https://api.x.com/2/problems/internal-error": "#/components/schemas/InternalErrorProblem", + "https://api.x.com/2/problems/invalid-request": "#/components/schemas/InvalidRequestProblem", + "https://api.x.com/2/problems/not-authorized-for-field": "#/components/schemas/NotAuthorizedForFieldProblem", + "https://api.x.com/2/problems/not-authorized-for-resource": "#/components/schemas/NotAuthorizedForResourceProblem", + "https://api.x.com/2/problems/recipient-not-messageable": "#/components/schemas/RecipientNotMessageableProblem", + "https://api.x.com/2/problems/resource-not-found": "#/components/schemas/ResourceNotFoundProblem", + "https://api.x.com/2/problems/resource-unavailable": "#/components/schemas/ResourceUnavailableProblem" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/ResourceNotFoundProblem" + }, + { + "$ref": "#/components/schemas/InvalidRequestProblem" + }, + { + "$ref": "#/components/schemas/NotAuthorizedForResourceProblem" + }, + { + "$ref": "#/components/schemas/NotAuthorizedForFieldProblem" + }, + { + "$ref": "#/components/schemas/FieldUnauthorizedProblem" + }, + { + "$ref": "#/components/schemas/FieldHydrationFailureProblem" + }, + { + "$ref": "#/components/schemas/ResourceUnavailableProblem" + }, + { + "$ref": "#/components/schemas/DisallowedResourceProblem" + }, + { + "$ref": "#/components/schemas/RecipientNotMessageableProblem" + }, + { + "$ref": "#/components/schemas/InternalErrorProblem" + }, + { + "$ref": "#/components/schemas/DuplicateRuleProblem" + }, + { + "$ref": "#/components/schemas/InvalidRuleProblem" + } + ] + }, + "RecipientNotMessageableProblem": { + "properties": { + "detail": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/recipient-not-messageable" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail" + ], + "type": "object" + }, + "ResourceNotFoundProblem": { + "properties": { + "detail": { + "type": "string" + }, + "parameter": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/resource-not-found" + ], + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "resource_type" + ], + "type": "object" + }, + "ResourceUnavailableProblem": { + "properties": { + "detail": { + "type": "string" + }, + "resource_id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "enum": [ + "https://api.x.com/2/problems/resource-unavailable" + ], + "type": "string" + } + }, + "required": [ + "type", + "title", + "detail", + "resource_type" + ], + "type": "object" + }, + "RespondToScheduledCallRequest": { + "additionalProperties": false, + "properties": { + "occurrence_start_time": { + "description": "Start of the occurrence that scope this or this_and_following refers to (RFC 3339).", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,9})?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + "response": { + "description": "The answer to the invitation.", + "enum": [ + "accepted", + "declined", + "tentative" + ], + "type": "string" + }, + "scope": { + "description": "Which occurrences of a recurring call: all (the default without occurrence_start_time), this (the default with it), or this_and_following.", + "enum": [ + "all", + "this", + "this_and_following" + ], + "type": "string" + } + }, + "required": [ + "response" + ], + "type": "object" + }, + "RespondToScheduledCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/RespondToScheduledCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "RespondToScheduledCallResponseData": { + "additionalProperties": false, + "properties": { + "response": { + "description": "The answer now on the invitation.", + "type": "string" + } + }, + "type": "object" + }, + "ScheduleCallRequest": { + "additionalProperties": false, + "properties": { + "description": { + "description": "What the call is about.", + "maxLength": 1000, + "type": "string" + }, + "duration_minutes": { + "description": "How long the call lasts, in minutes. Defaults to 30; at most 31 days.", + "maximum": 44640, + "minimum": 1, + "type": "integer" + }, + "invitee_ids": { + "description": "X user ids of the people to invite.", + "items": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "maxItems": 100, + "type": "array" + }, + "notify_invitees": { + "description": "Whether invitees get an email about the call. Defaults to true.", + "type": "boolean" + }, + "recurrence": { + "description": "RFC 5545 RRULE to repeat the call by, such as FREQ=WEEKLY;BYDAY=MO; the RRULE: prefix is optional.", + "maxLength": 500, + "type": "string" + }, + "start_time": { + "description": "When the call starts (RFC 3339); not in the past.", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,9})?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + "time_zone": { + "description": "IANA time zone the recurrence follows, such as America/Los_Angeles.", + "maxLength": 64, + "type": "string" + }, + "title": { + "description": "Title invitees see.", + "maxLength": 100, + "type": "string" + } + }, + "required": [ + "start_time" + ], + "type": "object" + }, + "ScheduleCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/ScheduleCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "ScheduleCallResponseData": { + "additionalProperties": false, + "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, + "call_id": { + "description": "The X call people join; also its join secret.", + "type": "string" + }, + "description": { + "description": "What the call is about.", + "type": "string" + }, + "end_time": { + "description": "End of this occurrence (RFC 3339).", + "type": "string" + }, + "id": { + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "type": "string" + }, + "invitees": { + "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "items": { + "$ref": "#/components/schemas/ScheduleCallResponseDataInvitees" + }, + "type": "array" + }, + "join_url": { + "description": "Link people open to join the call on X.", + "type": "string" + }, + "occurrence_start_time": { + "description": "For a recurring call, this occurrence's original start (RFC 3339): pass it as occurrence_start_time to change, cancel or answer just this occurrence.", + "type": "string" + }, + "organizer_id": { + "description": "X user id of the call's organizer.", + "type": "string" + }, + "recurrence": { + "description": "RFC 5545 RRULE the call repeats by, such as FREQ=WEEKLY;BYDAY=MO.", + "type": "string" + }, + "start_time": { + "description": "Start of this occurrence (RFC 3339).", + "type": "string" + }, + "status": { + "description": "scheduled, or cancelled for a cancelled occurrence.", + "type": "string" + }, + "time_zone": { + "description": "IANA time zone the recurrence follows.", + "type": "string" + }, + "title": { + "description": "Title of the call.", + "type": "string" + } + }, + "type": "object" + }, + "ScheduleCallResponseDataInvitees": { + "additionalProperties": false, + "properties": { + "id": { + "description": "X user id of the invitee.", + "type": "string" + }, + "response": { + "description": "accepted, declined, tentative, or none for no answer yet.", + "type": "string" + } + }, + "type": "object" + }, + "UpdateScheduledCallRequest": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "start_time" + ] + }, + { + "required": [ + "duration_minutes" + ] + }, + { + "required": [ + "title" + ] + }, + { + "required": [ + "description" + ] + }, + { + "required": [ + "invitee_ids" + ] + }, + { + "required": [ + "recurrence" + ] + }, + { + "required": [ + "time_zone" + ] + } + ], + "description": "At least one of `start_time`, `duration_minutes`, `title`, `description`, `invitee_ids`, `recurrence`, `time_zone` is required.", + "properties": { + "description": { + "description": "What the call is about.", + "maxLength": 1000, + "type": "string" + }, + "duration_minutes": { + "description": "How long the call lasts, in minutes.", + "maximum": 44640, + "minimum": 1, + "type": "integer" + }, + "invitee_ids": { + "description": "Everyone to invite: replaces the invite list.", + "items": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "maxItems": 100, + "minItems": 1, + "type": "array" + }, + "notify_invitees": { + "description": "Whether invitees get an email about the change. Defaults to true.", + "type": "boolean" + }, + "occurrence_start_time": { + "description": "Start of the occurrence that scope this or this_and_following refers to (RFC 3339).", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,9})?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + "recurrence": { + "description": "RFC 5545 RRULE to repeat the call by, such as FREQ=WEEKLY;BYDAY=MO.", + "maxLength": 500, + "minLength": 1, + "type": "string" + }, + "scope": { + "description": "Which occurrences of a recurring call: all (the default without occurrence_start_time), this (the default with it), or this_and_following.", + "enum": [ + "all", + "this", + "this_and_following" + ], + "type": "string" + }, + "start_time": { + "description": "When the call (or the chosen occurrence) starts now (RFC 3339).", + "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])T([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(\\.\\d{1,9})?(Z|[+-]([01]\\d|2[0-3]):[0-5]\\d)$", + "type": "string" + }, + "time_zone": { + "description": "IANA time zone the recurrence follows.", + "maxLength": 64, + "type": "string" + }, + "title": { + "description": "Title invitees see.", + "maxLength": 100, + "type": "string" + } + }, + "type": "object" + }, + "UpdateScheduledCallResponse": { + "properties": { + "data": { + "$ref": "#/components/schemas/UpdateScheduledCallResponseData" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/Problem" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateScheduledCallResponseData": { + "additionalProperties": false, + "properties": { + "updated": { + "description": "Whether the scheduled call was changed.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "BearerToken": { + "scheme": "bearer", + "type": "http" + }, + "OAuth2UserToken": { + "flows": { + "authorizationCode": { + "authorizationUrl": "https://api.x.com/2/oauth2/authorize", + "scopes": { + "block.read": "View accounts you have blocked.", + "block.write": "Block and unblock accounts on your behalf.", + "bookmark.read": "Read your bookmarked Posts.", + "bookmark.write": "Create and delete your bookmarks.", + "broadcast.read": "View your live broadcasts and their chat.", + "broadcast.write": "Manage your live broadcasts and send chat messages on your behalf.", + "developer.read": "View your developer accounts, apps, and settings.", + "developer.write": "Create and manage your X Developer Platform account.", + "dm.read": "Read all your Direct Messages.", + "dm.write": "Send and manage your Direct Messages.", + "follows.read": "View accounts you follow and accounts following you.", + "follows.write": "Follow and unfollow accounts on your behalf.", + "like.read": "View Posts you have liked and likes you can see.", + "like.write": "Like and unlike Posts on your behalf.", + "list.read": "View Lists, members, and followers of Lists you created or are a member of, including private Lists.", + "list.write": "Create and manage Lists on your behalf.", + "media.write": "Upload media, such as photos and videos, on your behalf.", + "mute.read": "View accounts you have muted.", + "mute.write": "Mute and unmute accounts on your behalf.", + "offline.access": "Request a refresh token for the app.", + "space.read": "View all Spaces you have access to.", + "timeline.read": "View all Custom Timelines you can see.", + "tweet.moderate.write": "Hide and unhide replies to your posts.", + "tweet.read": "View all posts you can see, including those from protected accounts.", + "tweet.write": "Create and repost on your behalf.", + "users.read": "View any account you can see, including protected accounts." + }, + "tokenUrl": "https://api.x.com/2/oauth2/token" + } + }, + "type": "oauth2" + }, + "UserToken": { + "scheme": "OAuth", + "type": "http" + } + } + }, + "info": { + "title": "X Calls API", + "version": "2", + "description": "Start, join and schedule X calls as your app's user, and put a voice agent in them. In development: endpoints and payloads may change." + }, + "openapi": "3.0.0", + "paths": { + "/2/calls": { + "post": { + "description": "Starts an X call hosted by the authenticated user (typically a bot). Pass an XChat conversation_id to start the call in that conversation, or omit it for a standalone call anyone can join with the returned join_url (subject to join_policy). The call starts empty: put an agent in it with POST /2/calls/:id/join. When the host joins a conversation call, the other members are rung.", + "operationId": "createCall", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCallRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "users.read", + "dm.write", + "tweet.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Create a call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/scheduled": { + "get": { + "description": "Returns the X calls on the authenticated user's calendar, earliest first: events on their X Calls calendar, and events on the Google or Outlook calendars they linked to X whose conference is an X call. Calls still running at start_time are included. The window defaults to the next 24 hours and may span at most 7 days.\n\nWhen both are provided, `start_time` must be earlier than `end_time`.", + "operationId": "listScheduledCalls", + "parameters": [ + { + "description": "Calls still running at this time or starting after it (RFC 3339). Defaults to now.", + "in": "query", + "name": "start_time", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + }, + "style": "form" + }, + { + "description": "Calls starting before this time (RFC 3339). Defaults to 24 hours after start_time.", + "in": "query", + "name": "end_time", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + }, + "style": "form" + }, + { + "description": "The most calls to return. Defaults to 20.", + "in": "query", + "name": "max_results", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 1, + "type": "integer" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListScheduledCallsResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "dm.read", + "tweet.read", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "List scheduled calls", + "tags": [ + "Calls" + ] + }, + "post": { + "description": "Puts an X call on the authenticated user's X Calls calendar, with them as its organizer, and invites people to it. The call exists right away: share its join_url, or put an agent in it at start time with POST /2/calls/:id/join. A recurrence (an RFC 5545 RRULE) repeats it. Invitees get an email unless notify_invitees is false.", + "operationId": "scheduleCall", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleCallRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "dm.write", + "tweet.read", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Schedule a call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/scheduled/{id}": { + "delete": { + "description": "Cancels a scheduled call the authenticated user organizes, and takes it off invitees' calendars. For a recurring call, scope and occurrence_start_time pick the occurrences: all of them (the default), one, or one and every one after it. Cancelling what is already cancelled succeeds.", + "operationId": "cancelScheduledCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "style": "simple" + }, + { + "description": "Which occurrences: all (the default without occurrence_start_time), this (the default with it), or this_and_following.", + "in": "query", + "name": "scope", + "required": false, + "schema": { + "enum": [ + "all", + "this", + "this_and_following" + ], + "type": "string" + }, + "style": "form" + }, + { + "description": "Start of the occurrence that scope this or this_and_following refers to (RFC 3339).", + "in": "query", + "name": "occurrence_start_time", + "required": false, + "schema": { + "format": "date-time", + "type": "string" + }, + "style": "form" + }, + { + "description": "Whether invitees get an email about it. Defaults to true.", + "in": "query", + "name": "notify_invitees", + "required": false, + "schema": { + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelScheduledCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "tweet.read", + "dm.write", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Cancel a scheduled call", + "tags": [ + "Calls" + ] + }, + "get": { + "description": "Returns a scheduled call the authenticated user organizes or is invited to, at its occurrence running now or its next one within a year: its time, call, recurrence and who is invited and how they answered. Changes can take a few seconds to show here.", + "operationId": "getScheduledCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduledCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "dm.read", + "users.read", + "tweet.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Get a scheduled call", + "tags": [ + "Calls" + ] + }, + "put": { + "description": "Moves, renames or re-invites a scheduled call the authenticated user organizes. Only the fields sent change. For a recurring call, scope and occurrence_start_time pick the occurrences: all of them (the default), one, or one and every one after it.", + "operationId": "updateScheduledCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduledCallRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduledCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "tweet.read", + "dm.write", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Update a scheduled call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/scheduled/{id}/rsvp": { + "post": { + "description": "Accepts, declines or tentatively accepts an invitation to a scheduled call for the authenticated user. For a recurring call, scope and occurrence_start_time pick the occurrences the answer is for: all of them (the default), one, or one and every one after it.", + "operationId": "respondToScheduledCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[0-9]{1,19}$", + "type": "string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespondToScheduledCallRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RespondToScheduledCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "tweet.read", + "dm.write", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Respond to a scheduled call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/{id}": { + "get": { + "description": "Returns a call's title, join policy, host and participant count, and whether the authenticated user may join it without asking the host.", + "operationId": "getCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[A-Za-z0-9_-]{1,64}$", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "dm.read", + "tweet.read", + "users.read" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Get a call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/{id}/join": { + "post": { + "description": "Starts a media session that takes part in the call as the authenticated user. The response carries a media_url and a short-lived media_token: open a WebSocket to media_url with the token as a Bearer credential to exchange PCM16 audio and events with the call; the user enters the call when the WebSocket connects. On a closed or trusted call the session asks the host first (admission: requires_approval).", + "operationId": "joinCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[A-Za-z0-9_-]{1,64}$", + "type": "string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinCallRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "tweet.read", + "users.read", + "dm.write" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Join a call", + "tags": [ + "Calls" + ] + } + }, + "/2/calls/{id}/leave": { + "post": { + "description": "Ends the authenticated user's media session in the call (its WebSocket receives session.ended with reason left) and removes the user from the call.", + "operationId": "leaveCall", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "pattern": "^[A-Za-z0-9_-]{1,64}$", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveCallResponse" + } + } + }, + "description": "The request has succeeded." + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The request has failed." + } + }, + "security": [ + { + "OAuth2UserToken": [ + "users.read", + "tweet.read", + "dm.write" + ] + }, + { + "UserToken": [] + } + ], + "summary": "Leave a call", + "tags": [ + "Calls" + ] + } + } + }, + "servers": [ + { + "description": "X API", + "url": "https://api.x.com" + } + ], + "tags": [ + { + "name": "Calls" + } + ] +} diff --git a/xcalls/quickstart.mdx b/xcalls/quickstart.mdx new file mode 100644 index 000000000..5d34d76ba --- /dev/null +++ b/xcalls/quickstart.mdx @@ -0,0 +1,115 @@ +--- +title: Quickstart +sidebarTitle: Quickstart +description: "Create an X call, join it as your bot, and connect a voice agent to the media WebSocket that streams the call's audio." +keywords: ["X Calls API quickstart", "create a call", "join a call", "media WebSocket", "voice agent", "PCM16"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +This walkthrough starts a call hosted by your bot, puts your agent in it and leaves again. You need an X app and the bot account your agent speaks as. + + + + Every endpoint needs a **user-context** token for the account your agent speaks as; app-only bearer tokens are rejected. With OAuth 2.0 (Authorization Code with PKCE), request `tweet.read`, `users.read`, `dm.read` and `dm.write`, which cover every Calls endpoint. See [Authentication](/xcalls/authentication) for the per-endpoint scopes and OAuth 1.0a. + + + ```bash + curl -X POST "https://api.x.com/2/calls" \ + -H "Authorization: Bearer $USER_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title": "Support line", "join_policy": "open"}' + ``` + + The call starts empty. Share its `join_url` with the people you want in it. + + ```json + { + "data": { + "id": "t69-8sb-6z5", + "title": "Support line", + "join_policy": "open", + "host_id": "2099230323825426442", + "participant_count": 0, + "created_at": "2026-09-24T02:10:00.000Z", + "join_url": "https://call.x.com/t69-8sb-6z5", + "viewer_may_join": true + } + } + ``` + + To ring someone instead, pass the `conversation_id` of your bot's conversation with them. See [Calling people](/xcalls/calling-people). + + + Choose the sample rate your agent works at. It applies in both directions. + + ```bash + curl -X POST "https://api.x.com/2/calls/t69-8sb-6z5/join" \ + -H "Authorization: Bearer $USER_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"media": {"type": "websocket", "audio": {"encoding": "pcm16", "sample_rate": 16000}}}' + ``` + + ```json + { + "data": { + "session_id": "cs_4f1c2a9e0b7d3a51", + "media_type": "websocket", + "media_url": "wss://api.x.com/2/calls/media", + "media_token": "eyJhbGciOiJIUzI1NiJ9…", + "expires_at": "2026-09-24T02:12:00.000Z", + "admission": "immediate", + "call": { "id": "t69-8sb-6z5", "join_policy": "open" } + } + } + ``` + + Open the media WebSocket before `expires_at`. The `media_token` works once. + + + Open `media_url` with the media token. Binary frames carry audio (PCM16 little-endian mono at your sample rate); text frames carry JSON events from X and commands to X. + + ```javascript + const ws = new WebSocket(`${session.media_url}?token=${encodeURIComponent(session.media_token)}`); + ws.binaryType = "arraybuffer"; + const send = (command) => ws.send(JSON.stringify(command)); + + ws.onmessage = ({ data }) => { + if (data instanceof ArrayBuffer) { + // 20 ms of everyone else in the call: feed it to your model. + return; + } + const event = JSON.parse(data); + switch (event.type) { + case "session.joined": + // Your agent is in the call. Send its voice as binary frames of any size. + break; + case "speech.started": + send({ type: "audio.clear" }); // someone started talking: stop your agent + break; + case "session.ended": + ws.close(); + break; + } + }; + ``` + + The session starts with `session.created`, then `session.joined` once your agent is in the call. See [Media WebSocket](/xcalls/media-websocket) for every event and command, and [Example agent](/xcalls/example-agent) for a complete program. + + + Send `{"type": "leave"}` on the WebSocket, call `POST /2/calls/:id/leave`, or close the socket. Your WebSocket receives `session.ended` with the reason. + + + +## Next steps + + + + Session lifecycle, events, commands and turn-taking. + + + Put calls on the calendar and find the ones to join. + + diff --git a/xcalls/scheduled-calls.mdx b/xcalls/scheduled-calls.mdx new file mode 100644 index 000000000..d482042ba --- /dev/null +++ b/xcalls/scheduled-calls.mdx @@ -0,0 +1,184 @@ +--- +title: Scheduled calls +sidebarTitle: Scheduled calls +description: "Put X calls on your user's X Calls calendar, list the calls on their calendar, and move, cancel or answer them, including single occurrences of recurring calls." +keywords: ["scheduled calls", "X Calls calendar", "schedule a call", "recurring call", "RRULE", "occurrence_start_time", "rsvp"] +--- + + + The X Calls API is in development. It isn't generally available yet, and endpoints and payloads may still change. + + +A scheduled call is an X call on the X Calls calendar: a time, a title, the people invited, and optionally a recurrence. Your user is its organizer. Scheduling is limited to the accounts that can schedule calls in the X app; other accounts get a `403`. + +Scheduling creates the call right away, so its `join_url` can be shared at once, and your agent [joins](/xcalls/api-reference/join-a-call) it with `call_id` when it's time. Invitees see the call on their X calendar and get an email unless `notify_invitees` is `false`. + + + A scheduled call's `id` is not its `call_id`. Change, cancel and answer a scheduled call with its `id`; join the call with its `call_id`. + + +## List scheduled calls + +`GET /2/calls/scheduled` returns the X calls on your user's calendar, earliest first: events on their X Calls calendar, and events on the Google or Outlook calendars they linked to X whose conference is an X call. A meeting assistant uses it to know which calls to join, and when. Calls still running at `start_time` are included. + +| Query parameter | Type | Description | +|:----------------|:-----|:------------| +| `start_time` | RFC 3339, optional | Calls still running at this time or starting after it. Defaults to now. | +| `end_time` | RFC 3339, optional | Calls starting before this time. Defaults to 24 hours after `start_time`; at most 7 days after it. | +| `max_results` | 1–100, optional | The most calls to return. Defaults to 20. | + +```bash +curl "https://api.x.com/2/calls/scheduled?max_results=5" \ + -H "Authorization: Bearer $USER_ACCESS_TOKEN" +``` + +```json +{ + "data": [ + { + "id": "1840000000000000000", + "call_id": "t69-8sb-6z5", + "join_url": "https://call.x.com/t69-8sb-6z5", + "title": "Standup", + "start_time": "2026-09-30T17:50:00.000Z", + "end_time": "2026-09-30T18:05:00.000Z", + "occurrence_start_time": "2026-09-30T17:50:00.000Z", + "recurrence": "FREQ=DAILY", + "organizer_id": "2244994945", + "invitees": [{ "id": "783214", "response": "accepted" }], + "calendar": "x", + "status": "scheduled" + }, + { + "call_id": "abc-def-ghi", + "join_url": "https://call.x.com/abc-def-ghi", + "title": "Weekly sync", + "start_time": "2026-09-30T19:00:00.000Z", + "end_time": "2026-09-30T19:30:00.000Z", + "calendar": "google", + "status": "scheduled" + } + ], + "meta": { "result_count": 2 } +} +``` + +Each item is one occurrence. `calendar` says where it lives: + +- `x`: an X Calls event, with the full [scheduled call object](#the-scheduled-call-object). Change it with its `id`. +- `google` or `outlook`: an event on a calendar your user linked to X. It has no `id` and is changed at the provider. + +A meeting on both X Calls and a linked calendar is listed once, as `x`. A linked calendar that can't be read right now (its provider is down, or its events are still loading) leaves out only its own calls; ask again shortly to see them. With no calls in the window, `data` is left out and `result_count` is 0. + +## Schedule a call + +`POST /2/calls/scheduled` + +| Field | Type | Description | +|:------|:-----|:------------| +| `start_time` | RFC 3339, required | When the call starts, with seconds and a zone (`2026-10-12T18:00:00Z`); not in the past. | +| `duration_minutes` | 1–44640, optional | How long the call lasts. Defaults to 30. | +| `title` | string, optional | At most 100 characters. | +| `description` | string, optional | At most 1000 characters. | +| `invitee_ids` | array of user ids, optional | At most 100 people. | +| `recurrence` | string, optional | An RFC 5545 RRULE, such as `FREQ=WEEKLY;BYDAY=MO`; the `RRULE:` prefix is optional. | +| `time_zone` | string, optional | IANA time zone the recurrence follows, such as `America/Los_Angeles`, so a weekly call keeps its local hour across daylight saving. | +| `notify_invitees` | boolean, optional | Whether invitees get an email. Defaults to `true`. | + +```bash +curl -X POST "https://api.x.com/2/calls/scheduled" \ + -H "Authorization: Bearer $USER_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"start_time": "2026-10-12T18:00:00Z", "title": "Weekly sync", + "invitee_ids": ["783214"], "recurrence": "FREQ=WEEKLY;BYDAY=MO", + "time_zone": "America/Los_Angeles"}' +``` + +`201 Created`: + +```json +{ + "data": { + "id": "1840000000000000000", + "call_id": "t69-8sb-6z5", + "join_url": "https://call.x.com/t69-8sb-6z5", + "title": "Weekly sync", + "start_time": "2026-10-12T18:00:00.000Z", + "end_time": "2026-10-12T18:30:00.000Z", + "occurrence_start_time": "2026-10-12T18:00:00.000Z", + "recurrence": "FREQ=WEEKLY;BYDAY=MO", + "time_zone": "America/Los_Angeles", + "organizer_id": "2244994945", + "invitees": [{ "id": "783214", "response": "none" }], + "calendar": "x", + "status": "scheduled" + } +} +``` + +## Get a scheduled call + +`GET /2/calls/scheduled/:id` returns a scheduled call your user organizes or is invited to, at its occurrence running now or its next one within a year. An unknown or past call, or one your user isn't part of, is a `200` with the problem under `errors`, as with other lookups. Changes can take a few seconds to show here. + +## Change a scheduled call + +`PUT /2/calls/scheduled/:id` moves, renames or re-invites a call your user organizes. Send only what changes: `start_time`, `duration_minutes`, `title`, `description`, `invitee_ids` (the whole new list; it can't be emptied), `recurrence` and `time_zone`, plus `notify_invitees`. The response is `{"data": {"updated": true}}`; read the call back with GET. + +```bash +curl -X PUT "https://api.x.com/2/calls/scheduled/1840000000000000000" \ + -H "Authorization: Bearer $USER_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"start_time": "2026-10-12T19:00:00Z"}' +``` + +## Cancel a scheduled call + +`DELETE /2/calls/scheduled/:id` cancels a call your user organizes and takes it off invitees' calendars. Query parameters: `scope`, `occurrence_start_time` (see [Recurring calls](#recurring-calls)) and `notify_invitees`. The response is `{"data": {"cancelled": true}}`, also when it was already cancelled. + +## Answer an invitation + +`POST /2/calls/scheduled/:id/rsvp` + +```json +{ "response": "accepted" } +``` + +`response` is `accepted`, `declined` or `tentative`. The response echoes it: `{"data": {"response": "accepted"}}`. + +## Recurring calls + +Change, cancel and rsvp apply to every occurrence of a recurring call unless you pick some with `scope` (a body field for PUT and rsvp, a query parameter for DELETE): + +| `scope` | Applies to | +|:--------|:-----------| +| `all` | Every occurrence: the default without `occurrence_start_time`. | +| `this` | The occurrence starting at `occurrence_start_time`: the default with it. | +| `this_and_following` | That occurrence and every one after it, such as to end a series. | + +`occurrence_start_time` is the occurrence's original start, which GET and the list report as `occurrence_start_time` for recurring calls (`start_time` moves when an occurrence is moved). For example, to move only next Monday's sync: + +```json +{ + "scope": "this", + "occurrence_start_time": "2026-10-19T18:00:00Z", + "start_time": "2026-10-19T20:00:00Z" +} +``` + +A one-off call has a single occurrence, so every scope means the whole call. A recurrence can be changed but not removed; cancel the occurrences you don't want with `this_and_following`. + +## The scheduled call object + +| Field | Description | +|:------|:------------| +| `id` | The scheduled call's id: what change, cancel and rsvp take. Not set for calls on linked calendars. | +| `call_id` | The call people join; join it with [join](/xcalls/api-reference/join-a-call). Anyone with it can open the call's link. | +| `join_url` | The call's link, `https://call.x.com/{call_id}`. | +| `title`, `description` | As set. | +| `start_time`, `end_time` | This occurrence, RFC 3339. | +| `occurrence_start_time` | For a recurring call, this occurrence's original start: what `scope` `this` and `this_and_following` take. | +| `recurrence`, `time_zone` | The RRULE and zone, for a recurring call. | +| `organizer_id` | The organizer's user id. | +| `invitees` | Everyone invited except the organizer, each with their `response`: `accepted`, `declined`, `tentative` or `none` (no answer yet). Not set for calls on linked calendars. | +| `calendar` | `x` for X Calls; `google` or `outlook` for a calendar your user linked to X, listed only. | +| `status` | `scheduled`, or `cancelled` for a cancelled occurrence. | From 58f13496f6d639c12210972a5661b7b42d6b4946 Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Mon, 5 Oct 2026 22:29:41 +0000 Subject: [PATCH 2/3] docs: X Calls API lists X Calls calendar events only X Calls launches without linked Google or Outlook calendars: the list returns X Calls events only, and scheduled calls have no calendar field. --- xcalls/openapi.json | 59 ++++++++++++++++++-------------------- xcalls/scheduled-calls.mdx | 27 ++++------------- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/xcalls/openapi.json b/xcalls/openapi.json index 42088105b..6d89fe8a6 100644 --- a/xcalls/openapi.json +++ b/xcalls/openapi.json @@ -339,10 +339,6 @@ "GetScheduledCallResponseData": { "additionalProperties": false, "properties": { - "calendar": { - "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", - "type": "string" - }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -356,11 +352,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "description": "Everyone invited, except the organizer.", "items": { "$ref": "#/components/schemas/GetScheduledCallResponseDataInvitees" }, @@ -399,6 +395,9 @@ "type": "string" } }, + "required": [ + "id" + ], "type": "object" }, "GetScheduledCallResponseDataInvitees": { @@ -696,10 +695,6 @@ "ListScheduledCallsResponseData": { "additionalProperties": false, "properties": { - "calendar": { - "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", - "type": "string" - }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -713,11 +708,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "description": "Everyone invited, except the organizer.", "items": { "$ref": "#/components/schemas/ListScheduledCallsResponseDataInvitees" }, @@ -756,6 +751,9 @@ "type": "string" } }, + "required": [ + "id" + ], "type": "object" }, "ListScheduledCallsResponseDataInvitees": { @@ -1135,10 +1133,6 @@ "ScheduleCallResponseData": { "additionalProperties": false, "properties": { - "calendar": { - "description": "Where the call lives: x (X Calls), or google or outlook for a calendar the user linked to X, whose calls are changed there.", - "type": "string" - }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -1152,11 +1146,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer. Not set for linked calendars.", + "description": "Everyone invited, except the organizer.", "items": { "$ref": "#/components/schemas/ScheduleCallResponseDataInvitees" }, @@ -1195,6 +1189,9 @@ "type": "string" } }, + "required": [ + "id" + ], "type": "object" }, "ScheduleCallResponseDataInvitees": { @@ -1456,7 +1453,7 @@ }, "/2/calls/scheduled": { "get": { - "description": "Returns the X calls on the authenticated user's calendar, earliest first: events on their X Calls calendar, and events on the Google or Outlook calendars they linked to X whose conference is an X call. Calls still running at start_time are included. The window defaults to the next 24 hours and may span at most 7 days.\n\nWhen both are provided, `start_time` must be earlier than `end_time`.", + "description": "Returns the scheduled calls on the authenticated user's X Calls calendar that they organize or are invited to, earliest first. Calls still running at start_time are included. The window defaults to the next 24 hours and may span at most 7 days.\n\nWhen both are provided, `start_time` must be earlier than `end_time`.", "operationId": "listScheduledCalls", "parameters": [ { @@ -1525,9 +1522,9 @@ "security": [ { "OAuth2UserToken": [ + "users.read", "dm.read", - "tweet.read", - "users.read" + "tweet.read" ] }, { @@ -1582,9 +1579,9 @@ "security": [ { "OAuth2UserToken": [ + "users.read", "dm.write", - "tweet.read", - "users.read" + "tweet.read" ] }, { @@ -1679,8 +1676,8 @@ "security": [ { "OAuth2UserToken": [ - "tweet.read", "dm.write", + "tweet.read", "users.read" ] }, @@ -1807,9 +1804,9 @@ "security": [ { "OAuth2UserToken": [ + "users.read", "tweet.read", - "dm.write", - "users.read" + "dm.write" ] }, { @@ -1940,8 +1937,8 @@ { "OAuth2UserToken": [ "dm.read", - "tweet.read", - "users.read" + "users.read", + "tweet.read" ] }, { @@ -2010,9 +2007,9 @@ "security": [ { "OAuth2UserToken": [ + "dm.write", "tweet.read", - "users.read", - "dm.write" + "users.read" ] }, { @@ -2071,8 +2068,8 @@ "security": [ { "OAuth2UserToken": [ - "users.read", "tweet.read", + "users.read", "dm.write" ] }, diff --git a/xcalls/scheduled-calls.mdx b/xcalls/scheduled-calls.mdx index d482042ba..0f0ab132b 100644 --- a/xcalls/scheduled-calls.mdx +++ b/xcalls/scheduled-calls.mdx @@ -19,7 +19,7 @@ Scheduling creates the call right away, so its `join_url` can be shared at once, ## List scheduled calls -`GET /2/calls/scheduled` returns the X calls on your user's calendar, earliest first: events on their X Calls calendar, and events on the Google or Outlook calendars they linked to X whose conference is an X call. A meeting assistant uses it to know which calls to join, and when. Calls still running at `start_time` are included. +`GET /2/calls/scheduled` returns the scheduled calls on your user's X Calls calendar that they organize or are invited to, earliest first. A meeting assistant uses it to know which calls to join, and when. Calls still running at `start_time` are included. | Query parameter | Type | Description | |:----------------|:-----|:------------| @@ -46,29 +46,14 @@ curl "https://api.x.com/2/calls/scheduled?max_results=5" \ "recurrence": "FREQ=DAILY", "organizer_id": "2244994945", "invitees": [{ "id": "783214", "response": "accepted" }], - "calendar": "x", - "status": "scheduled" - }, - { - "call_id": "abc-def-ghi", - "join_url": "https://call.x.com/abc-def-ghi", - "title": "Weekly sync", - "start_time": "2026-09-30T19:00:00.000Z", - "end_time": "2026-09-30T19:30:00.000Z", - "calendar": "google", "status": "scheduled" } ], - "meta": { "result_count": 2 } + "meta": { "result_count": 1 } } ``` -Each item is one occurrence. `calendar` says where it lives: - -- `x`: an X Calls event, with the full [scheduled call object](#the-scheduled-call-object). Change it with its `id`. -- `google` or `outlook`: an event on a calendar your user linked to X. It has no `id` and is changed at the provider. - -A meeting on both X Calls and a linked calendar is listed once, as `x`. A linked calendar that can't be read right now (its provider is down, or its events are still loading) leaves out only its own calls; ask again shortly to see them. With no calls in the window, `data` is left out and `result_count` is 0. +Each item is one occurrence, as a [scheduled call](#the-scheduled-call-object). With no calls in the window, `data` is left out and `result_count` is 0. ## Schedule a call @@ -110,7 +95,6 @@ curl -X POST "https://api.x.com/2/calls/scheduled" \ "time_zone": "America/Los_Angeles", "organizer_id": "2244994945", "invitees": [{ "id": "783214", "response": "none" }], - "calendar": "x", "status": "scheduled" } } @@ -171,7 +155,7 @@ A one-off call has a single occurrence, so every scope means the whole call. A r | Field | Description | |:------|:------------| -| `id` | The scheduled call's id: what change, cancel and rsvp take. Not set for calls on linked calendars. | +| `id` | The scheduled call's id: what change, cancel and rsvp take. | | `call_id` | The call people join; join it with [join](/xcalls/api-reference/join-a-call). Anyone with it can open the call's link. | | `join_url` | The call's link, `https://call.x.com/{call_id}`. | | `title`, `description` | As set. | @@ -179,6 +163,5 @@ A one-off call has a single occurrence, so every scope means the whole call. A r | `occurrence_start_time` | For a recurring call, this occurrence's original start: what `scope` `this` and `this_and_following` take. | | `recurrence`, `time_zone` | The RRULE and zone, for a recurring call. | | `organizer_id` | The organizer's user id. | -| `invitees` | Everyone invited except the organizer, each with their `response`: `accepted`, `declined`, `tentative` or `none` (no answer yet). Not set for calls on linked calendars. | -| `calendar` | `x` for X Calls; `google` or `outlook` for a calendar your user linked to X, listed only. | +| `invitees` | Everyone invited except the organizer, each with their `response`: `accepted`, `declined`, `tentative` or `none` (no answer yet). | | `status` | `scheduled`, or `cancelled` for a cancelled occurrence. | From b8b777f4fe53abd1c1c437650e47de6a3dcbd702 Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Tue, 6 Oct 2026 00:04:10 +0000 Subject: [PATCH 3/3] docs: X Calls API lists linked Microsoft Outlook calendars too GET /2/calls/scheduled lists events on linked Outlook calendars that meet in an X call (calendar: "outlook", no id) next to X Calls events (calendar: "x"). --- xcalls/openapi.json | 71 ++++++++++++++++++++------------------ xcalls/scheduled-calls.mdx | 27 ++++++++++++--- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/xcalls/openapi.json b/xcalls/openapi.json index 6d89fe8a6..de7654bf6 100644 --- a/xcalls/openapi.json +++ b/xcalls/openapi.json @@ -339,6 +339,10 @@ "GetScheduledCallResponseData": { "additionalProperties": false, "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or outlook for a Microsoft Outlook calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -352,11 +356,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer.", + "description": "Everyone invited, except the organizer. Not set for linked calendars.", "items": { "$ref": "#/components/schemas/GetScheduledCallResponseDataInvitees" }, @@ -395,9 +399,6 @@ "type": "string" } }, - "required": [ - "id" - ], "type": "object" }, "GetScheduledCallResponseDataInvitees": { @@ -695,6 +696,10 @@ "ListScheduledCallsResponseData": { "additionalProperties": false, "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or outlook for a Microsoft Outlook calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -708,11 +713,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer.", + "description": "Everyone invited, except the organizer. Not set for linked calendars.", "items": { "$ref": "#/components/schemas/ListScheduledCallsResponseDataInvitees" }, @@ -751,9 +756,6 @@ "type": "string" } }, - "required": [ - "id" - ], "type": "object" }, "ListScheduledCallsResponseDataInvitees": { @@ -1133,6 +1135,10 @@ "ScheduleCallResponseData": { "additionalProperties": false, "properties": { + "calendar": { + "description": "Where the call lives: x (X Calls), or outlook for a Microsoft Outlook calendar the user linked to X, whose calls are changed there.", + "type": "string" + }, "call_id": { "description": "The X call people join; also its join secret.", "type": "string" @@ -1146,11 +1152,11 @@ "type": "string" }, "id": { - "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take.", + "description": "Unique identifier of the scheduled call: what PUT, DELETE and rsvp take. Calls on linked calendars have none.", "type": "string" }, "invitees": { - "description": "Everyone invited, except the organizer.", + "description": "Everyone invited, except the organizer. Not set for linked calendars.", "items": { "$ref": "#/components/schemas/ScheduleCallResponseDataInvitees" }, @@ -1189,9 +1195,6 @@ "type": "string" } }, - "required": [ - "id" - ], "type": "object" }, "ScheduleCallResponseDataInvitees": { @@ -1436,8 +1439,8 @@ "security": [ { "OAuth2UserToken": [ - "users.read", "dm.write", + "users.read", "tweet.read" ] }, @@ -1453,7 +1456,7 @@ }, "/2/calls/scheduled": { "get": { - "description": "Returns the scheduled calls on the authenticated user's X Calls calendar that they organize or are invited to, earliest first. Calls still running at start_time are included. The window defaults to the next 24 hours and may span at most 7 days.\n\nWhen both are provided, `start_time` must be earlier than `end_time`.", + "description": "Returns the X calls on the authenticated user's calendar, earliest first: events on their X Calls calendar, and events on the Microsoft Outlook calendars they linked to X whose conference is an X call. Calls still running at start_time are included. The window defaults to the next 24 hours and may span at most 7 days.\n\nWhen both are provided, `start_time` must be earlier than `end_time`.", "operationId": "listScheduledCalls", "parameters": [ { @@ -1522,9 +1525,9 @@ "security": [ { "OAuth2UserToken": [ + "tweet.read", "users.read", - "dm.read", - "tweet.read" + "dm.read" ] }, { @@ -1580,8 +1583,8 @@ { "OAuth2UserToken": [ "users.read", - "dm.write", - "tweet.read" + "tweet.read", + "dm.write" ] }, { @@ -1676,8 +1679,8 @@ "security": [ { "OAuth2UserToken": [ - "dm.write", "tweet.read", + "dm.write", "users.read" ] }, @@ -1736,8 +1739,8 @@ { "OAuth2UserToken": [ "dm.read", - "users.read", - "tweet.read" + "tweet.read", + "users.read" ] }, { @@ -1804,9 +1807,9 @@ "security": [ { "OAuth2UserToken": [ - "users.read", + "dm.write", "tweet.read", - "dm.write" + "users.read" ] }, { @@ -1876,8 +1879,8 @@ { "OAuth2UserToken": [ "tweet.read", - "dm.write", - "users.read" + "users.read", + "dm.write" ] }, { @@ -1936,9 +1939,9 @@ "security": [ { "OAuth2UserToken": [ + "tweet.read", "dm.read", - "users.read", - "tweet.read" + "users.read" ] }, { @@ -2007,9 +2010,9 @@ "security": [ { "OAuth2UserToken": [ - "dm.write", + "users.read", "tweet.read", - "users.read" + "dm.write" ] }, { @@ -2068,9 +2071,9 @@ "security": [ { "OAuth2UserToken": [ + "dm.write", "tweet.read", - "users.read", - "dm.write" + "users.read" ] }, { diff --git a/xcalls/scheduled-calls.mdx b/xcalls/scheduled-calls.mdx index 0f0ab132b..a2714cdd4 100644 --- a/xcalls/scheduled-calls.mdx +++ b/xcalls/scheduled-calls.mdx @@ -19,7 +19,7 @@ Scheduling creates the call right away, so its `join_url` can be shared at once, ## List scheduled calls -`GET /2/calls/scheduled` returns the scheduled calls on your user's X Calls calendar that they organize or are invited to, earliest first. A meeting assistant uses it to know which calls to join, and when. Calls still running at `start_time` are included. +`GET /2/calls/scheduled` returns the X calls on your user's calendar, earliest first: events on their X Calls calendar, and events on the Microsoft Outlook calendars they linked to X whose conference is an X call. A meeting assistant uses it to know which calls to join, and when. Calls still running at `start_time` are included. | Query parameter | Type | Description | |:----------------|:-----|:------------| @@ -46,14 +46,29 @@ curl "https://api.x.com/2/calls/scheduled?max_results=5" \ "recurrence": "FREQ=DAILY", "organizer_id": "2244994945", "invitees": [{ "id": "783214", "response": "accepted" }], + "calendar": "x", + "status": "scheduled" + }, + { + "call_id": "abc-def-ghi", + "join_url": "https://call.x.com/abc-def-ghi", + "title": "Weekly sync", + "start_time": "2026-09-30T19:00:00.000Z", + "end_time": "2026-09-30T19:30:00.000Z", + "calendar": "outlook", "status": "scheduled" } ], - "meta": { "result_count": 1 } + "meta": { "result_count": 2 } } ``` -Each item is one occurrence, as a [scheduled call](#the-scheduled-call-object). With no calls in the window, `data` is left out and `result_count` is 0. +Each item is one occurrence. `calendar` says where it lives: + +- `x`: an X Calls event, with the full [scheduled call object](#the-scheduled-call-object). Change it with its `id`. +- `outlook`: an event on a Microsoft Outlook calendar your user linked to X. It has no `id` and is changed in Outlook. + +A meeting on both X Calls and a linked calendar is listed once, as `x`. A linked calendar that can't be read right now (Outlook is down, or its events are still loading) leaves out only its own calls; ask again shortly to see them. With no calls in the window, `data` is left out and `result_count` is 0. ## Schedule a call @@ -95,6 +110,7 @@ curl -X POST "https://api.x.com/2/calls/scheduled" \ "time_zone": "America/Los_Angeles", "organizer_id": "2244994945", "invitees": [{ "id": "783214", "response": "none" }], + "calendar": "x", "status": "scheduled" } } @@ -155,7 +171,7 @@ A one-off call has a single occurrence, so every scope means the whole call. A r | Field | Description | |:------|:------------| -| `id` | The scheduled call's id: what change, cancel and rsvp take. | +| `id` | The scheduled call's id: what change, cancel and rsvp take. Not set for calls on linked Outlook calendars. | | `call_id` | The call people join; join it with [join](/xcalls/api-reference/join-a-call). Anyone with it can open the call's link. | | `join_url` | The call's link, `https://call.x.com/{call_id}`. | | `title`, `description` | As set. | @@ -163,5 +179,6 @@ A one-off call has a single occurrence, so every scope means the whole call. A r | `occurrence_start_time` | For a recurring call, this occurrence's original start: what `scope` `this` and `this_and_following` take. | | `recurrence`, `time_zone` | The RRULE and zone, for a recurring call. | | `organizer_id` | The organizer's user id. | -| `invitees` | Everyone invited except the organizer, each with their `response`: `accepted`, `declined`, `tentative` or `none` (no answer yet). | +| `invitees` | Everyone invited except the organizer, each with their `response`: `accepted`, `declined`, `tentative` or `none` (no answer yet). Not set for calls on linked Outlook calendars. | +| `calendar` | `x` for X Calls; `outlook` for a Microsoft Outlook calendar your user linked to X, listed only. | | `status` | `scheduled`, or `cancelled` for a cancelled occurrence. |