# Freesbe for developers > Freesbe: Pass it on. Chat, calls and meetings, with AI built in. It is made for support and sales teams first: channels, DMs, threads, huddles and video calls, scheduled meetings with guest links, booking pages, a phone line for every person, recordings with AI notes, AI summaries and translations, a punch clock, webhooks and an HTTP API. It runs in the browser and as a desktop app for macOS, Windows and Linux. Free: up to 10 people; Business is $4 per person a month; Corporate is $7 per person a month. This file is for AI assistants and coding agents helping someone build on Freesbe: incoming and outgoing webhooks, apps (bots with scopes, slash commands, buttons, dialogs and events), the REST API, the realtime WebSocket, the MCP server for AI agents, Sign in with Freesbe (OpenID Connect) and the partner program. Examples use made-up companies. ## Where to connect - REST API: `https://api.freesbe.net` - Token header: `Authorization: Bearer ` - Realtime WebSocket: `wss://api.freesbe.net/api/workspaces/{w}/rt` - MCP server: `https://api.freesbe.net/mcp` - OAuth authorization server: `https://id.freesbe.net` - Incoming webhook URL: `https://api.freesbe.net/hooks/custom//` - Outgoing webhook signature: `Freesbe-Signature: t=,v1=` - OpenAPI: `https://freesbe.net/docs/api/openapi.json` More: the developers page https://freesbe.net/developers/, the partner program https://freesbe.net/partners/, every guide https://freesbe.net/llms.txt. --- # Apps Source: https://freesbe.net/docs/apps/ > Connect your own tools to Freesbe with apps. Each app gets a bot that posts as itself and a token that can only do what its scopes allow. Apps take Slack app manifests. An app is an integration your workspace makes for itself: an order system that posts new orders, a script that files tickets, a bot you built for Slack. Each app gets two things: - **A bot.** It shows up in the people list with an **App** badge and posts the app's messages under its own name, so nobody's account is borrowed. Add it to a channel like a person. - **A bot token.** Your tool sends it with each call to the [API](https://freesbe.net/docs/api/#apps). It can only do what the app's scopes allow, and it keeps working when the workspace requires [single sign-on](https://freesbe.net/docs/single-sign-on/) or [multi-factor authentication](https://freesbe.net/docs/multi-factor-authentication/), so it doubles as an API key. ![A new order card from the Order desk app in #warehouse, with its App badge](https://freesbe.net/docs/img/app-message.webp) Apps belong to one workspace. Only owners and admins can make and change them, in **Workspace settings → Integrations & apps → Apps**. ![Workspace settings, Apps: Order desk and Stand-up, each with its scopes and token](https://freesbe.net/docs/img/admin-apps.webp) ## Make an app 1. Open **Workspace settings → Integrations & apps → Apps** and click **Create app**. 2. Give it a name and, if you like, a description. Its bot posts under the name. 3. Tick the scopes it needs. Start small; you can add more later. 4. Click **Create app**. ![Create app: a name, a description and the scopes to tick](https://freesbe.net/docs/img/admin-app-new.webp) Freesbe shows the app's **bot token** once. Copy it into your tool's settings or secrets now; if you lose it, make a new one. ![The new app's bot token, shown once, with a command to try it](https://freesbe.net/docs/img/admin-app-token.webp) Try it from a terminal: it answers with the app's name, its bot and its scopes. ```sh curl -H "Authorization: Bearer fsb-bot-…" https://api.freesbe.net/api/apps/me ``` ## Give your app an icon An app shows its first letter until you give it an icon. The icon is its bot's picture too, so it shows next to the app's messages, on its profile card and in the people list. 1. Open the **⋯** menu next to the app and click **Edit**. 2. Click **Upload icon** and pick a PNG, JPEG, GIF or WebP. A square picture works best; you can zoom and choose the part to show. ![Editing Order desk: its parcel icon at the top, with Change icon and Remove](https://freesbe.net/docs/img/admin-app-icon.webp) **Remove** goes back to the letter. Your tool can also set its own icon with its bot token (`PUT /api/apps/me/icon`, see the [API](https://freesbe.net/docs/api/#apps)). Freesbe's built-in integrations, like GitHub, HubSpot, Zendesk or Zabbix, and the Zapier, Make, n8n and Zoho Flow apps show their own logo on their own: there's nothing to upload. ![A Zabbix alert in #ops-alerts with the Zabbix logo, and the Zabbix app's card showing the same logo](https://freesbe.net/docs/img/app-bot-logo.webp) ## Bring an app from Slack If you built an app for Slack, paste its manifest instead of picking scopes: 1. In Slack's app settings, open **App Manifest** and copy it (YAML or JSON). 2. In Freesbe, click **Create app**, switch to **Manifest** and paste it. 3. Click **Create app**. ![Create app with a Slack app manifest pasted in YAML](https://freesbe.net/docs/img/admin-app-manifest.webp) Freesbe reads the app's name, description, colour, bot name, bot scopes, [event subscriptions](#get-events), [slash commands](#slash-commands) and [message shortcuts](#message-actions). Slack splits scopes and events for public channels, private channels and DMs (`groups:history`, `message.im`); Freesbe has one for each, so those map across. Scopes and events Freesbe doesn't have yet are listed and left out, and user scopes and global shortcuts are ignored: the app acts as its bot only. Its interactivity request URL is where [buttons](#buttons) and [dialogs](#dialogs) are sent. ## Put the bot to work A new bot isn't in any channel yet. - **Public channels:** give the app the `channels:join` scope and let it join, or add it from the channel's member list. - **Private channels and DMs:** add the bot from the channel's member list, like a person. Then your tool posts with `POST /api/channels/{channel}/messages`. Besides text, apps can send a card as Slack Block Kit `blocks` or `attachments`, with headers, facts, images and link buttons; the `body` is what notifications and search show. ```sh curl https://api.freesbe.net/api/channels//messages \ -H "Authorization: Bearer fsb-bot-…" \ -H "Content-Type: application/json" \ -d '{ "body": "New order #4471 from Acme Corp", "blocks": [ { "type": "header", "text": { "type": "plain_text", "text": "New order #4471" } } ] }' ``` ## Get events Freesbe can tell your app when things happen: a message in a channel its bot is in, someone mentioning the app, a reaction, a new channel, someone joining the workspace, a huddle starting, a meeting being scheduled, a recording that's ready. 1. Open the app's **⋯** menu and click **Edit** (or set it up while you make the app). 2. Under **Events**, enter the **Request URL** on your server and tick the events you want. Ticking an event ticks the scope it needs. 3. Click **Save changes**. ![Editing an app: the request URL and the events it gets, each with the scope it needs](https://freesbe.net/docs/img/admin-app-events.webp) When you save, Freesbe checks the URL the way Slack does: it sends a `url_verification` request with a `challenge`, and your server answers with the challenge. If it doesn't, the app isn't saved and Freesbe says what went wrong. Apps built with Slack's SDKs (Bolt) do this already. Each event then arrives as a `POST` in Slack's Events API envelope (`"type": "event_callback"`), with the event inside `event`. The app only hears what its bot could see: messages and reactions in channels and DMs it is in, changes to public channels, the people in the workspace, and meetings it is part of. It never hears about what its own bot did, so it can't talk to itself. ### Check the signature Every event is signed with the app's **signing secret**: open the app's **⋯** menu and click **Show signing secret**. Freesbe sends two signatures over the same body: - `X-Slack-Signature` and `X-Slack-Request-Timestamp`, the way Slack signs, so Slack's SDKs check it for you. - `Freesbe-Signature`, the same way as [outgoing webhooks](https://freesbe.net/docs/api-and-webhooks/#check-the-signature). Answer with any 2xx within 10 seconds. Failures are retried for about 13 hours. Click **Deliveries** next to the app to see what was sent and how your server answered. ![Order desk's recent deliveries: message and reaction events, delivered](https://freesbe.net/docs/img/admin-app-deliveries.webp) The [API reference](https://freesbe.net/docs/api/#app-events) lists every event, what it carries and the scope it needs. ## Slash commands An app can add slash commands, such as `/order 4512`, that anyone in the workspace can type at the start of a message. Typing `/` shows the commands the workspace's apps add, with what to type after each one. ![Typing / in the message box: Order desk's /order and /stock commands](https://freesbe.net/docs/img/app-command-suggest.webp) Slash commands are set in the app's manifest, the same way as on Slack, under `features.slash_commands`. Each one has a `command`, the `url` Freesbe sends it to, a `description` and a `usage_hint`: ```yaml features: slash_commands: - command: /order url: https://orders.northwind.example/freesbe/commands description: Look up an order usage_hint: "[order number]" ``` A command can belong to one app only. When someone runs it, Freesbe `POST`s Slack's form to the command's URL (`command`, `text`, `user_id`, `channel_id`, `response_url` and the rest), signed like [events](#check-the-signature). Answer within 3 seconds: - **Plain text, or JSON with `text` or `blocks`:** only the person who ran the command sees the answer, marked **Only visible to you**, until they dismiss it or reload. - **JSON with `"response_type": "in_channel"`:** the app's bot posts the answer in the channel for everyone. - **An empty 200:** nothing shows yet. Post the answer later to the `response_url`, the same way, up to 30 minutes later. ![Order desk's answer to /stock assam, only visible to the person who asked](https://freesbe.net/docs/img/app-command-reply.webp) The message isn't posted: the command goes to the app instead. A message that starts with `/` but isn't a command, such as a file path, is sent as usual. ## Message actions An app can add actions to every message's **⋯** menu, such as **Create order** or **File a ticket**. Picking one sends the message to the app, which can answer through its `response_url`. ![A message's ⋯ menu with Order desk's Create order action](https://freesbe.net/docs/img/app-message-action.webp) Message actions are Slack's message shortcuts, set under `features.shortcuts` with `type: message`. They go to the app's interactivity request URL as Slack's `message_action` payload, with the message, the channel and the person: ```yaml features: shortcuts: - name: Create order type: message callback_id: create_order description: Make a wholesale order from this message settings: interactivity: is_enabled: true request_url: https://orders.northwind.example/freesbe/actions ``` Each app's commands and actions are listed on its card in **Workspace settings → Integrations & apps → Apps**. Turning an app off hides them until it's back on. ## Buttons An app's message can have buttons that do something, such as **Mark packed**, as well as buttons that open a link. Pressing one tells the app, which can change the message, answer, or open a dialog. ![Order desk's new order card with Mark packed and Open order buttons](https://freesbe.net/docs/img/app-buttons.webp) They're Block Kit buttons with an `action_id` and no `url`, in an `actions` block or as a section's accessory. A press goes to the app's interactivity request URL as Slack's `block_actions` payload, with the button's `action_id`, `block_id` and `value`, the message, the person, a `trigger_id` and a `response_url`. Post to the `response_url` with `"replace_original": true` to change the message, say to show who packed the order, or with `"delete_original": true` to remove it. Buttons work the same on answers only one person sees. Only apps' buttons do something: in incoming webhook cards, a button without a link is left out. ## Dialogs An app can open a dialog for the person who ran its command, picked its message action or pressed its button, to ask for the details it needs. When they press the dialog's send button, Freesbe checks what's needed is filled in and sends it to the app. ![A New order dialog from Order desk asking for the customer, tea and number of cases](https://freesbe.net/docs/img/app-dialog.webp) Dialogs are Slack's modals. Within 30 seconds of the `trigger_id`, send the modal to `POST /api/apps/views` with the bot token: ```json { "triggerId": "", "view": { "type": "modal", "callback_id": "new_order", "title": { "type": "plain_text", "text": "New order" }, "submit": { "type": "plain_text", "text": "Create order" }, "blocks": [ { "type": "input", "block_id": "cases", "label": { "type": "plain_text", "text": "Cases" }, "element": { "type": "number_input", "action_id": "cases", "is_decimal_allowed": false } } ] } } ``` The dialog opens only where the person pressed or typed, not on their other devices. It can have sections, headers, context, dividers, images and buttons, and these inputs: `plain_text_input` (with `multiline` for longer text), `email_text_input`, `url_text_input`, `number_input`, `datepicker`, `static_select`, `radio_buttons`, `checkboxes` and `users_select`. Sending the dialog reaches the app as Slack's `view_submission`, with the answers in `view.state.values`. Answer within 3 seconds with nothing to close it, or with a `response_action`: - **`errors`:** show messages under the inputs, by `block_id`, such as `{ "response_action": "errors", "errors": { "cases": "Only 12 cases left" } }`. - **`update`:** show another `view` in its place. - **`push`:** show another `view` on top. Closing it goes back to the one under it. Dialogs stack three deep. - **`clear`:** close every dialog. The app can also change an open dialog with `PUT /api/apps/views/{id}` (Slack's `views.update`), and add one on top with `POST /api/apps/views/push` and a `trigger_id` from a button inside it. With `notify_on_close: true`, the app hears `view_closed` when the person closes it. ## Scopes Each scope opens a few API endpoints; the [API reference](https://freesbe.net/docs/api/#reference) shows the scope each endpoint needs. A call outside the app's scopes answers `403 missing_scope`, and calls apps can't make at all (sign-in, your account, calls, AI and the punch clock) answer `403 not_allowed_token_type`. | Scope | Lets the app | | ------------------- | ---------------------------------------------------------------- | | `team:read` | See the workspace's name and settings | | `users:read` | See the people in the workspace | | `users:write` | Set the app's own status | | `channels:read` | See channels and who is in them | | `channels:join` | Join and leave public channels | | `channels:manage` | Create channels, rename them, archive them and add people | | `channels:history` | Read messages in channels the app is in | | `chat:write` | Send messages as the app, schedule them, and edit or delete them | | `im:write` | Start direct messages with people | | `reactions:write` | Add and remove emoji reactions | | `pins:write` | Pin and unpin messages | | `bookmarks:write` | Add, change and remove the links kept at the top of a channel | | `files:read` | Open files shared in channels the app is in | | `files:write` | Upload files and attach them to its messages | | `search:read` | Search messages in channels the app is in | | `todos:read` | See the app's own to-dos | | `todos:write` | Make, change and delete the app's own to-dos | | `meetings:read` | See the workspace's meetings | | `meetings:write` | Schedule meetings and change the ones the app made | | `recordings:read` | Open huddle and meeting recordings the app can see | | `app_mentions:read` | Hear about messages that mention the app | The bot is a member of the workspace, never an admin, and it only sees what a member in the same channels would: private channels and DMs only once it's been added. ## Change, pause or delete an app With many apps, search them in the [Admin Center](https://freesbe.net/docs/admin-center/) by name, description or scope. To let your team sign in to one of your tools with their Freesbe account, see [Sign in with Freesbe](https://freesbe.net/docs/sign-in-with-freesbe/): it has its own page, under **Security & compliance**. Open the **⋯** menu next to the app: - **Edit** changes its icon, name, description or scopes, or its whole manifest. A new scope works on the next call; a removed one stops working at once. - **Make a new token** replaces the bot token. The old one stops working right away, so update your tool with the new one. Do this if a token may have leaked. - **Turn off** pauses the app: its token answers `401` until you turn it back on. - **Delete** removes the app and its token. Its bot leaves the workspace, but the messages it sent stay, under its name. Each app shows the end of its current token and when it was last used, so you can spot a token that's no longer needed. ## Zapier, Make, n8n and Zoho Flow To connect Freesbe to Zapier, Make, n8n or Zoho Flow, you don't need to make an app yourself: open **Workspace settings → Integrations & apps → Integrations → Automation** and click **Connect**. See [Zapier, Make, n8n and Zoho Flow](https://freesbe.net/docs/automation/). Their triggers use REST hooks, which any app can make with its bot token (`POST /api/apps/hooks`), to send one event to its own URL. ## Apps and webhooks [Webhooks](https://freesbe.net/docs/api-and-webhooks/) are still the quickest way to post into one channel, or to send a workspace's events to your server. Use an app when your tool needs to post in several channels, read messages, react, hear about what happens in the channels its bot is in, or act as a bot people can add and DM. --- # API and webhooks Source: https://freesbe.net/docs/api-and-webhooks/ > Post messages from other tools with incoming webhooks, get events with signed outgoing webhooks, and script Freesbe with the REST API. There are three ways to connect Freesbe to other tools: | You want to… | Use | | ------------------------------------------------------- | ----------------------------------------- | | Post alerts, deploys or form submissions into a channel | An [incoming webhook](#incoming-webhooks) | | Run a bot that posts, reads and reacts in many channels | An [app](https://freesbe.net/docs/apps/) | | Tell your own server when something happens in Freesbe | An [outgoing webhook](#outgoing-webhooks) | | Read and write anything a person can | The [REST API](#the-rest-api) | | Follow everything as it happens, DMs included | The [realtime WebSocket](#live-events) | The [API reference](https://freesbe.net/docs/api/) lists every endpoint, grouped like the app: messages, channels and DMs, people and invites, guests, meetings, booking pages, calendar free/busy, the punch clock, workspace settings, sign-in security and more. Each one says who may call it (anyone, members, channel members or admins) and has an example request and answer. ## Incoming webhooks An incoming webhook gives you a secret URL that posts into one channel. It accepts the same JSON as Slack's incoming webhooks, so a Slack integration only needs the new URL. In the [Admin Center](https://freesbe.net/docs/admin-center/), search your webhooks by name, address, channel or who added them. ![Workspace settings, Webhooks: an incoming webhook that posts to #engineering](https://freesbe.net/docs/img/admin-webhooks.webp) 1. An admin opens **Workspace settings → Integrations & apps → Webhooks → Incoming** and clicks **Add incoming webhook**. 2. Give it a name, such as "Deploy notifications", and pick the channel it posts to. 3. Copy the URL. It's shown once; if you lose it, use **Make a new URL**. Then post to it: ```sh curl -X POST -H 'Content-Type: application/json' \ -d '{"text": "Deploy finished :rocket:", "username": "CI"}' \ https://api.freesbe.net/hooks/custom// ``` Freesbe reads `text`, `blocks`, `attachments`, `username`, `icon_emoji` and `icon_url`, from a JSON body or a form with a `payload` field, and answers `ok` like Slack. Messages show under the webhook's name, or the `username` you send. Each of the `attachments` shows like it does in Slack: its text, fields and pictures beside a bar in the attachment's `color`, with no box around it, so alerts from monitoring tools stay easy to scan. ### Cards `blocks` in Slack's Block Kit format show as a card: headers, sections with fields and a small picture beside them, context lines, dividers, images and buttons that open a link. As in Slack, `text` is what notifications and search use; leave it out and Freesbe writes it from the blocks. Interactive elements without a link, such as menus or buttons that call back a Slack app, are left out. The same URL also takes the cards tools send to Microsoft Teams, so a tool set up for a Teams channel can post to Freesbe instead: an Adaptive Card (on its own, or in the `attachments` of a `"type": "message"` payload, as Teams' Workflows webhooks expect) or an older Office 365 connector `MessageCard`. Freesbe shows their text, facts, images and **Open URL** buttons. ![A low stock card from a webhook in #engineering, with Reorder and Open inventory buttons](https://freesbe.net/docs/img/webhook-post.webp) The URL holds the webhook's ID and a secret token, and nothing about your workspace. URLs made before October 2026, `https://api.freesbe.net/hooks/` or `https://app.freesbe.net/hooks/`, keep working. ## Outgoing webhooks An outgoing webhook sends a signed `POST` to your server for the events you choose. 1. An admin opens **Workspace settings → Integrations & apps → Webhooks → Outgoing** and clicks **Add outgoing webhook**. 2. Enter a name and the **Payload URL**. 3. Leave **Channel** on **All public channels**, or pick one channel to only get its events. 4. Pick the events and click **Create webhook**. Copy the signing secret; it's shown once. There are 30 events, in five groups: - **Messages:** new, edited and deleted messages, files shared, and reactions added or removed. - **Channels:** channels made, renamed, given a new topic, archived or unarchived, and people joining or leaving them. - **People:** someone joining the workspace, their profile or role changing, being deactivated, and status changes. - **Huddles and meetings:** huddles starting and ending and people joining or leaving them; meetings scheduled, changed or canceled; a guest waiting to be let in; bookings through a booking page; recordings and transcripts that are ready. - **Punch clock:** someone clocking in or out. Channel events come from public channels, or from the one channel you picked. The workspace's own events (people, meetings, bookings and the punch clock) go to webhooks on **All public channels**. **Send test** sends a `webhook.test` event. The [API reference](https://freesbe.net/docs/api/#outgoing) lists each event's name and what it carries. ![The outgoing webhook form, with its events in groups](https://freesbe.net/docs/img/admin-webhook-events.webp) To get events in your own app instead, with Slack's event format, see [Apps](https://freesbe.net/docs/apps/#get-events). ### Check the signature Each request carries `Freesbe-Event`, `Freesbe-Delivery` and `Freesbe-Signature: t=,v1=`. `v1` is the HMAC-SHA256 of `.` under the webhook's secret. In Node.js: ```js import { createHmac, timingSafeEqual } from "node:crypto"; function verify(secret, header, rawBody) { const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("="))); const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; return ( fresh && v1?.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) ); } ``` ### Retries A delivery that fails, or gets a 5xx, 408 or 429 answer, is retried 8 more times, from 30 seconds up to 8 hours apart. Any other 4xx stops it. **Deliveries** on the webhook's card shows the last 14 days of attempts. ## The REST API The API lives at `https://api.freesbe.net`. It takes and returns JSON, and every request carries a bearer token. ### Get a token For an integration, [make an app](https://freesbe.net/docs/apps/): its bot token posts as the app, only does what its scopes allow, doesn't expire and isn't held to single sign-on or MFA. Send it as `Authorization: Bearer fsb-bot-…`. A script can also act as a person. Sign in with their account: ```sh curl -X POST https://api.freesbe.net/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email": "bot@example.com", "password": "…", "wantToken": true}' ``` The answer includes a `token`. Send it as `Authorization: Bearer `. It lasts 30 days from the last time it was used, so a script that runs at least monthly stays signed in. Signing out with `POST /api/auth/logout`, or changing the account's password, ends it. If the workspace requires [single sign-on](https://freesbe.net/docs/single-sign-on/), password tokens can't use it. If it requires [multi-factor authentication](https://freesbe.net/docs/multi-factor-authentication/), set up an authenticator app for the script's account, and after signing in send a code from it: ```sh curl -X POST https://api.freesbe.net/api/me/mfa/totp \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"code": "123456"}' ``` Until then, workspace requests answer 403 `mfa_required`. When the workspace asks again after a while, do the same. ### Post a message ```sh curl -X POST https://api.freesbe.net/api/channels/$CHANNEL/messages \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"body": "Release *2.4* is out :tada:"}' ``` Message text uses Slack-style formatting: `*bold*`, `_italic_`, `` `code` ``, `` and `:emoji:` shortcodes. Freesbe also shows `__underline__`, and lines starting with `• ` or `- ` (bullets) or `1. ` (numbers) as lists, nested four spaces per level. Slack shows those as the plain text they are, so the same message reads well in both. Add `threadRootId` to reply in a thread. Find workspace and channel IDs with `GET /api/workspaces` and `GET /api/workspaces/{w}/channels`, or copy the workspace ID from the [Admin Center](https://freesbe.net/docs/admin-center/), under **Workspace → General → Details**. ### What a script can do Anything the account could do in the app. A member account can post, edit and delete its own messages, reply in threads, react, upload files, open DMs, create and join channels, set its status, search and schedule meetings. An admin account can also invite people, change roles, deactivate people, change workspace settings and manage webhooks. A few things need the app itself, such as adding a passkey or joining a call; the reference marks them **App only** and lists [what isn't in the API yet](https://freesbe.net/docs/api/#gaps). ### Live events To react to messages in DMs and private channels, presence, statuses or huddles, open the workspace's WebSocket at `wss://api.freesbe.net/api/workspaces/{w}/rt` with the same bearer token. See [Realtime events](https://freesbe.net/docs/api/#realtime) for the frames and the full list of events. ### From a browser `api.freesbe.net` only accepts bearer tokens, never the app's sign-in cookie, so any website can call it (CORS is open). Keep tokens on a server if the page is public. ### Errors Errors come back as `{ "error": { "code": "forbidden", "message": "Only admins can do that" } }` with a matching HTTP status. See [Errors and limits](https://freesbe.net/docs/api/#errors) in the reference. ## For AI assistants and API tools Building with an AI assistant or a coding agent? Give it [freesbe.net/llms-developers.txt](https://freesbe.net/llms-developers.txt): one plain-text file with where to connect, the developer guides (apps, webhooks, Sign in with Freesbe, AI agents, the partner program) and the whole API reference, every endpoint with its example. For example, paste: ```text Read https://freesbe.net/llms-developers.txt, then help me build a Freesbe integration that posts new orders to #orders ``` To load the API into an API client, or generate an SDK in any language, use the OpenAPI 3.1 document at [freesbe.net/docs/api/openapi.json](https://freesbe.net/docs/api/openapi.json). Both are made from the [API reference](https://freesbe.net/docs/api/) each time the site is published, so they never fall behind it. The [Developers](https://freesbe.net/developers/) page has every address on one card, with a copy button for each. ![The Start here card on the Developers page: the API, token header, WebSocket, MCP server, OAuth, webhook URL, signature header and OpenAPI, each with a copy button](https://freesbe.net/docs/img/developers-start.webp) ![The For your AI assistant section: the llms files, a prompt to paste and the MCP config for a code editor](https://freesbe.net/docs/img/developers-assistant.webp) --- # Sign in with Freesbe Source: https://freesbe.net/docs/sign-in-with-freesbe/ > Let your team sign in to your own tools (a dashboard, a wiki, an internal admin) with their Freesbe account. Standard OpenID Connect, with your workspace's sign-in rules and phone approval built in. Sign in with Freesbe lets the people in your workspace sign in to your other tools with their Freesbe account: an internal dashboard, a wiki, a support tool, anything that supports OpenID Connect (often listed as "OIDC" or "generic OAuth" in its settings). It's on every plan. Because the sign-in happens on Freesbe, everything you set up for Freesbe applies there too: [a second step](https://freesbe.net/docs/multi-factor-authentication/), [single sign-on](https://freesbe.net/docs/single-sign-on/), and [approving sign-ins on your phone](https://freesbe.net/docs/sign-in-and-security/#approve-sign-ins-on-your-phone). When someone leaves your workspace, they can't sign in to those tools with Freesbe any more. ## Set up a tool Owners and admins set it up in the [Admin Center](https://freesbe.net/docs/admin-center/), under **Security → Sign-in & access → Sign in with Freesbe**. It lists every tool your team signs in to with Freesbe, who can sign in to each, how many people have, and when someone last did. ![Admin Center, Security, Sign-in & access, Sign in with Freesbe: Northwind Dashboard, Stock admin and Team wiki, with their return addresses and options](https://freesbe.net/docs/img/admin-center-sign-in-with-freesbe.webp) 1. Click **Add a tool** and give it a name. Each tool is one of your workspace's [apps](https://freesbe.net/docs/apps/), and its name and colour are what people see when they sign in. To use an app you already have, choose **An app you have** instead. ![Add a tool: a new app called Support desk](https://freesbe.net/docs/img/sign-in-with-freesbe-add.webp) 2. Under **Return addresses**, paste the address your tool asks you for (often called the redirect URI or callback URL), such as `https://dashboard.northwind.example/auth/callback`. Add one per line if the tool has several. 3. Click **Turn on**. To change a tool later, click **Settings** next to it. ![Sign in with Freesbe for the Northwind Dashboard app: issuer, client ID, client secret, return address and options](https://freesbe.net/docs/img/sign-in-with-freesbe-settings.webp) Freesbe shows three things to put in your tool's settings: - **Issuer**: `https://id.freesbe.net`. Most tools only need this and find the rest themselves at `https://id.freesbe.net/.well-known/openid-configuration`. - **Client ID**: the app's id. - **Client secret**: shown once, right after you turn it on. Copy it into your tool then. If you lose it, click **Make a new secret**; the old one stops working at once. ### Options - **Who can sign in**: everyone in the workspace except guests, or admins only. - **Confirm it's you at every sign-in**: on for every tool unless you turn it off. Each time someone signs in to the tool, they confirm it's them right then with a passkey or their phone (picking the number shown on screen, then their fingerprint or face check), even if they're already signed in to Freesbe in that browser. Someone who just did, in the last 5 minutes, isn't asked twice. People without a passkey or a phone set up use their authenticator app, or an emailed code. Turn it off only for tools anyone in the workspace could safely see, like a wiki. - **Allow it for everyone**: skips the screen that asks each person to allow the app, for tools everyone's expected to use. - **Post sign-ins to**: a channel where the tool's bot posts each time someone signs in to it, such as "Maya Chen (maya@northwind.example) signed in to Stock admin with Freesbe." Handy for tools only a few people should use. Choose **Don't post** to stop. ![Sign in with Freesbe for Stock admin: admins only, a second step, and sign-ins posted to #leadership](https://freesbe.net/docs/img/sign-in-with-freesbe-notify.webp) - **It's a phone or browser-only app**: for tools that can't keep a secret, like a phone app or a single-page web app. They use PKCE instead of a client secret, and may return to their own app link (such as `net.northwind.dashboard:/callback`). To stop it, click **Settings** next to the tool and then **Turn off**. Turning the whole app off in **Integrations & apps → Apps**, or deleting it, stops it too. ## What people see When someone signs in to your tool, it sends them to Freesbe. If they aren't signed in to Freesbe in that browser, they sign in as usual, with any way your workspace allows, including [just their phone](https://freesbe.net/docs/sign-in-and-security/#sign-in-with-just-your-phone). Then Freesbe asks them to confirm it's them, even if they were already signed in. With a phone set up to [approve sign-ins](https://freesbe.net/docs/sign-in-and-security/#approve-sign-ins-on-your-phone), their phone asks "Is this you signing in?": they tap the number shown on the computer, then use their fingerprint or face. They can use a passkey instead. ![Confirm it's you: Northwind Dashboard asks each time you sign in. A notification was sent to the phone, with the number 47 to tap there](https://freesbe.net/docs/img/sign-in-with-freesbe-confirm.webp) ![The phone asking Is this you signing in?, with Chrome on Windows in Singapore and the numbers 31, 47 and 82](https://freesbe.net/docs/img/mobile-approve-sign-in.webp) The first time, Freesbe also asks them to allow the tool: ![Sign in to Northwind Dashboard with Freesbe: it will see your name, your email address, and that you're in Northwind Traders and your role there](https://freesbe.net/docs/img/sign-in-with-freesbe-allow.webp) Click **Allow** and Freesbe sends them back to the tool, signed in. Next time they only confirm it's them (unless you turned that off for the tool), then go straight back. The tool learns only what the screen lists: their name, email address, and their role in your workspace. It can't read their messages or post as them. ### Apps you sign in to **Preferences → My account → Sessions and apps → Apps you sign in to with Freesbe** lists every tool you've allowed and when you last signed in to each. Click **Remove** to take it back: the tool asks you to allow it again next time. To sign out of a tool, sign out in the tool itself. ![Preferences, My account, Sessions and apps: Apps you sign in to with Freesbe, with Northwind Dashboard and a Remove button](https://freesbe.net/docs/img/prefs-connected-apps.webp) ## For developers Sign in with Freesbe is standard [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) with the authorization code flow. Any OpenID Connect library works; point it at the issuer. | | | | --------- | --------------------------------------------------------------------------------------------------------------------------- | | Issuer | `https://id.freesbe.net` | | Discovery | `https://id.freesbe.net/.well-known/openid-configuration` | | Authorize | `https://id.freesbe.net/authorize` | | Token | `https://id.freesbe.net/token` (`client_secret_basic`, `client_secret_post`, or PKCE alone for phone and browser-only apps) | | User info | `https://id.freesbe.net/userinfo` | | Keys | `https://id.freesbe.net/jwks` | | Scopes | `openid` (required), `profile`, `email` | | ID tokens | ES256, valid for an hour | PKCE (`S256`) works for every app and is required for phone and browser-only apps. A code works once, for a minute. `prompt=none` isn't supported: Freesbe always shows its own page. The ID token and `/userinfo` carry: - `sub`: the person's Freesbe id. It never changes, even if their email does. - `email` and `email_verified`, with the `email` scope; `name`, with `profile`. - `workspace_id` and `workspace_role` (`owner`, `admin` or `member`). - `amr`: how they signed in: `pwd` (password), `otp` (an emailed code), `hwk` and `user` (a passkey, or approval on their phone), `fed` (Google or Microsoft), and `mfa` once they've passed a second step. Check for `mfa` if your tool needs it, and leave **Confirm it's you at every sign-in** on. - `auth_time`: when they last proved it's them, which with **Confirm it's you at every sign-in** is a moment ago. Your tool can refuse an older one. And the `nonce` you sent. Signing keys rotate every 90 days; fetch them from the keys address rather than copying one. --- # Build an integration for Freesbe Source: https://freesbe.net/docs/build-an-integration/ > The partner program for software companies. Build an integration with Freesbe's apps, API, webhooks, MCP server or Sign in with Freesbe, apply on freesbe.net/partners/, and get a page on freesbe.net with a link back to you. If you make software that a team would want next to its chat, calls or customer inbox, you can build an integration with Freesbe and have it listed on freesbe.net. Apply on [freesbe.net/partners/](https://freesbe.net/partners/), tell us what you want to build, and once it works we write a page for it under [Integrations](https://freesbe.net/integrations/), in its category next to the ones we built, with a link to your site and your setup guide. The program is free, and you don't need a Freesbe account to apply. ![The partner program page on freesbe.net: what partners get, four ways to build, and the application form](https://freesbe.net/docs/img/partners-page.webp) ## What you can build with Partners build on the same interfaces customers use for their own tools, so anything you build works in every workspace. | Build with | Good for | Guide | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | | An app | A bot with its own name and icon that posts, reads and reacts; slash commands, buttons, dialogs and events. Takes a Slack app manifest as it is. | [Apps](https://freesbe.net/docs/apps/) | | The API and webhooks | Posting into channels from your product, syncing, and getting signed events on your server | [API and webhooks](https://freesbe.net/docs/api-and-webhooks/) | | The MCP server | An AI agent that works in Freesbe as the person who signed it in | [AI agents](https://freesbe.net/docs/ai-agents/#for-developers) | | Sign in with Freesbe | Letting a workspace's people sign in to your product with their Freesbe account | [Sign in with Freesbe](https://freesbe.net/docs/sign-in-with-freesbe/#for-developers) | Start in a free workspace: [sign up](https://app.freesbe.net/signup), make an app in **Workspace settings → Integrations & apps → Apps**, and try your integration there. If you need a paid feature to test with, say so in your application. ## Apply 1. Open [freesbe.net/partners/](https://freesbe.net/partners/#apply). 2. Fill in your company, your website, and your name and work email. 3. Name the integration and pick **What kind of integration** it is (CRM, support, finance, monitoring, AI agents and the other categories of Workspace settings → Integrations, or Something else) and **How it connects**. 4. Say in a sentence or two what it does for Freesbe customers, and how far along it is. An idea is fine. 5. Add a link to your docs if you have them, agree to the [partner program terms](https://freesbe.net/legal/partner-program/), and click **Send application**. ![The application form: company, website, contact, the integration's name, kind and how it connects](https://freesbe.net/docs/img/partners-apply.webp) You get an email saying we have it. Apply once for each integration. ## What happens next A person on our team reads every application, usually within five business days, and emails you at each step. Reply to any of those emails with questions or anything you want to add. | Status | What it means | | --------- | -------------------------------------------------------------------------------------- | | In review | We're looking at it, and may ask questions or ask to try it. | | Approved | It's in. Send us your setup guide and a square logo by replying to the email. | | Listed | Your page is live on freesbe.net. We show it to you before it goes live. | | Declined | We can't take it right now. The email says why; you can apply again once that changes. | What we look at: - It does what your application says. - It asks only for the scopes it needs, and keeps customers' data to what it needs. - Your setup guide works, step by step, for someone who has never seen your product. - Your listing and your guide use your own product's name, not Freesbe's, for what you built. ## Your page on freesbe.net A listed integration gets a card on [Integrations](https://freesbe.net/integrations/) marked with your company's name, and a page of its own that says what it does, how to set it up, and that it's built and supported by you, with a link to your site. Customers set it up from your guide: they create the app from your manifest, paste a webhook address, or connect your AI agent over MCP. ![A partner's page under Integrations: Built and supported by Acme Corp, a Freesbe partner](https://freesbe.net/docs/img/partners-listing.webp) ## The Works with Freesbe badge Once you're listed, put the badge on your site, linking to your page on freesbe.net. Use the light one on light pages and the dark one on dark pages, at 40 pixels high or more, and don't change its colours. ```html Works with Freesbe ``` The dark badge is `works-with-freesbe-dark.svg`. Both are on [freesbe.net/partners/](https://freesbe.net/partners/#badge). ## Change or leave To change your listing, such as a new name, logo or guide, reply to any of our emails or write to partners@freesbe.net. To leave the program, tell us the same way and we take your page down. The [partner program terms](https://freesbe.net/legal/partner-program/) have the details. --- # AI agents Source: https://freesbe.net/docs/ai-agents/ > Connect Claude, ChatGPT, your code editor or any other AI tool to Freesbe. It acts for you, as itself, with only what you allow, and admins decide who can connect what. AI agents are the AI tools people already use, connected to Freesbe: Claude, ChatGPT, a code editor such as Cursor or VS Code, or an agent your own team built. Once connected, an agent can work in your workspace for you: find out who is around, read what you can read, post as itself, and (soon) ask people questions and wait for their answers. It's on every plan. Three things stay true for every agent: - **It acts for the person who connected it.** An agent sees only what that person can see, and never more than they allowed when they connected it. - **It's visible.** An agent shows up in the member list and in messages as its own bot, with an **AI agent** badge and the name of the tool it is. Where admins let agents post as the person, each message still says which agent sent it. - **Admins are in charge.** Owners and admins choose who can connect agents, approve or block each one, limit what it may do, and see everything it did. Freesbe's agent support uses the open [Model Context Protocol](https://modelcontextprotocol.io/) (MCP), so any tool that can add an MCP server or "connector" works, with no app to install. ## Connect an agent In your AI tool, add Freesbe as a connector or MCP server with this address: ``` https://api.freesbe.net/mcp ``` Where to put it depends on the tool. In Claude it's **Settings → Connectors → Add custom connector**; in ChatGPT, **Settings → Connectors**; in most code editors, an `mcp.json` file. The tool then opens a page in your browser: ![Connect Claude to Freesbe: pick a workspace, see what it will be able to do, and allow it](https://freesbe.net/docs/img/connect-agent.webp) 1. If you aren't signed in to Freesbe in that browser, sign in as usual. Your workspace's sign-in rules apply, including a [second step](https://freesbe.net/docs/multi-factor-authentication/) and [single sign-on](https://freesbe.net/docs/single-sign-on/). 2. Pick the workspace the agent should work in. One connection is for one workspace; connect it again for another. 3. Read what it will be able to do. Anything crossed out isn't allowed in that workspace, so the agent won't get it. 4. Click **Allow**. You go back to the tool, connected. If the workspace's admins want to approve new agents first, the button says **Ask an admin** instead. Add a note if you like, and connect again once an admin has approved it (you'll see the agent as **Waiting** in your preferences until then). Guests can't connect agents. ### Your connected agents **Preferences → My account → Sessions and apps → AI agents** lists every agent you've connected, in which workspace, and when it last did something. Click **Disconnect** and it stops at once; connect it again from the tool whenever you like. ![Preferences, My account, Sessions and apps: AI agents, with Claude and Cursor connected to Northwind Traders](https://freesbe.net/docs/img/prefs-connected-agents.webp) An agent keeps working for 30 days after it was last used, then it asks you to connect again. ## What an agent can do When a tool connects, it says what it wants to do, and the connect page lists it in plain words. These are the permissions an agent can ask for: | Permission | What it means | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | See your channels and who is in them | The names and members of the channels you're in | | Read messages in public channels you're in | Messages, threads and files in public channels | | Read messages in your private channels | Off unless admins allow it | | Read your direct messages | Off unless admins allow it | | Search messages and files you can see | Search, with the same results you'd get | | Send messages in channels and threads | Posts as the agent, in channels you're in | | Send direct messages | Direct messages from the agent, to people you can message | | Add emoji reactions | | | See the people in the workspace | Names, titles and roles | | See who's around, their local time and working hours | Status, [availability](https://freesbe.net/docs/status-and-availability/) and time zones | | See when people are free or busy | Free/busy from [calendars](https://freesbe.net/docs/status-and-availability/#connect-your-calendar) | | See your meetings · Schedule meetings and share booking pages | [Meetings](https://freesbe.net/docs/meetings/) and [booking pages](https://freesbe.net/docs/bookings/) | | Read transcripts and notes of meetings you were in | [Recordings and notes](https://freesbe.net/docs/recordings-and-notes/) of meetings you attended | | See your to-dos · Add and complete your to-dos | Your [to-dos](https://freesbe.net/docs/getting-around/#to-dos) | | Ask people questions for you and get their answers | Direct messages from the agent asking a teammate, with their answers back ([below](#ask-people)) | ### Tools An agent only sees the tools its permissions allow, and only what you could see yourself: a channel you aren't in, or a private one without the private permission, doesn't exist for it. | Tool | What it does | | --------------------- | ------------------------------------------------------------------------------------------------------------------ | | **search** | Searches the messages you can see, with the same filters as the search box (`from:`, `in:`, `after:`) | | **list_channels** | Your channels, or every channel you could join | | **read_channel** | The latest messages in a channel or direct message, and older ones page by page | | **read_thread** | A message and its replies | | **send_message** | Posts in a channel or a thread | | **send_dm** | Sends a direct message to one person or a few | | **add_reaction** | Adds an emoji reaction to a message | | **find_people** | Finds people by name, email or title | | **check_presence** | Whether people are around, their status, do not disturb, calendar and local time | | **find_free_time** | Times everyone is free, within their working hours and calendars | | **ask** | Asks someone a question for you, in a direct message ([below](#ask-people)) | | **get_answer** | The answer, or where the question is | | **list_questions** | The questions it asked for you | | **cancel_question** | Withdraws a question that's still waiting | | **call_phone_number** | Calls a number for you: your Freesbe phone rings first ([click-to-call](https://freesbe.net/docs/phone/#click-to-call-from-your-crm)) | | **get_connection** | Who the agent works for, in which workspace, and what it may do | Every message it reads comes with a link back to it in Freesbe, so the agent can point you to its sources. Everything an agent posts is marked as its own: it posts as the agent, its entry in the member list says **AI agent**, and each message says who it's for. ![A message in #spring-launch posted by Claude, with "Sent by Claude, an AI agent, for Maya Chen" under it](https://freesbe.net/docs/img/agent-message.webp) If your admins turned on **Agents post as the person who connected them**, the agent's messages show your name and picture instead, with **via Claude, an AI agent** under each one. You can edit or delete them like your own messages, but the line saying an agent sent them stays. Reactions are still the agent's own. ![A message in #spring-launch under Maya Chen's name, with "via Claude, an AI agent" under it](https://freesbe.net/docs/img/agent-message-as-person.webp) Agents treat what they read in messages as information, never as instructions, and post only what you asked for. Rate limits are per connection, so one busy agent can't slow the workspace down. ### Ask people An agent can ask a teammate something for you, like "When will the Acme Corp order ship?", and wait for the answer. The question arrives as a direct message from the agent, saying who it's asking for, with the right way to answer: a date, a date and time, choices, **Yes** or **No**, a number, or a few words, and a note if they want to add one. ![A direct message from Claude asking Maya Chen when the Acme Corp order will ship, for James Okafor, with a date picker, a note, Send answer, Not me and Can't say yet](https://freesbe.net/docs/img/agent-question.webp) - **Working hours.** A question waits until it's a weekday between 9:00 and 17:00 where the person is, and until their Do Not Disturb ends. The agent can skip the wait for something urgent. - **Not me.** The person can pass it on, naming who might know. It goes to them, or to the next person the agent named, or comes back to the agent as unanswered. - **Can't say yet.** They're asked again the next working morning. - **Reminders and deadlines.** The agent can ask for up to two reminders, and a deadline. When the deadline passes the question moves on to the next person the agent named, with as long to answer, or stops waiting. - **You hear back.** When someone answers, passes or doesn't answer in time, the agent tells you in your direct message with it, so you have the answer even if the agent isn't running. Agents can ask members, not guests. One agent can have three questions waiting on the same person at a time, and a connection can ask 30 a day. Admins can take the permission away from an agent in **Workspace settings → Integrations & apps → AI agents → Permissions**. ## For admins Owners and admins manage agents in **Workspace settings → Integrations & apps → AI agents**. ![Workspace settings, AI agents: who can connect, the agents people connected with Approve, Block and Permissions, and the activity log](https://freesbe.net/docs/img/admin-ai-agents.webp) ### Who can connect AI agents - **Nobody**: agents can't connect to this workspace, and the ones already connected stop working until you turn it back on. - **Agents an admin approved**: the first person to connect a new agent asks you to approve it. You see the request on this page, with their note. Once approved, anyone can connect that agent. Admins' own connections are approved as they make them. - **Anyone** (the default): anyone but guests can connect any agent. Each one shows up on this page as soon as someone connects it, where you can limit or block it. **Let agents read private channels and direct messages** is off by default. When it's on, an agent can ask for those permissions, and still only reads the private conversations of the person who connected it. Turning it off takes those permissions away from every connection at once. **Agents post as the person who connected them** is off by default, so agents post as their own bot with "Sent by Claude, an AI agent, for Maya Chen" under each message. On, what an agent posts shows the person's name and picture, with **via Claude, an AI agent** under it. That line can't be removed, even when the person edits the message, so everyone can still tell a person didn't type it. It applies to everything agents post from then on; messages already sent stay as they are. Agents' reactions and the questions they [ask people](#ask-people) stay the agent's own. ![Workspace settings, AI agents: who can connect, private reading off and Agents post as the person who connected them on](https://freesbe.net/docs/img/admin-ai-agents-rules.webp) ### Each agent Every agent anyone connected is listed with how many people connected it and when it last did something: - **Approve** or **Allow** lets it in; **Block** stops every connection to it at once, and nobody can connect it until you allow it again. - **Permissions** sets the most it may do in this workspace. People who connect it get what they asked for, within that. Unticking a permission takes it away from everyone who already connected the agent. - **Remove** deletes the agent from the workspace and all its connections. Its messages stay, as a removed member's do. If someone connects it again later, it comes back as a new agent, subject to your approval setting. ### Activity The bottom of the page shows what agents did, newest first: who connected or disconnected what, approvals and blocks, and every tool call with whether it worked. It stays for 90 days. ## For developers If an AI assistant is helping you build, give it [freesbe.net/llms-developers.txt](https://freesbe.net/llms-developers.txt): this section, the other developer guides and the whole API reference in one file. Freesbe's MCP server speaks the current [MCP specification](https://modelcontextprotocol.io/specification/latest) over streamable HTTP, and uses OAuth 2.1 exactly as the specification's authorization section describes, so a standard MCP client connects with no Freesbe-specific code. | | | | ----------------------------- | ---------------------------------------------------------------------- | | MCP server | `POST https://api.freesbe.net/mcp` (JSON responses; no SSE stream yet) | | Protected resource metadata | `https://api.freesbe.net/.well-known/oauth-protected-resource/mcp` | | Authorization server | `https://id.freesbe.net` | | Authorization server metadata | `https://id.freesbe.net/.well-known/oauth-authorization-server` | | Authorize | `https://id.freesbe.net/authorize` | | Token | `https://id.freesbe.net/token` | | Registration | `https://id.freesbe.net/register` | | Revocation | `https://id.freesbe.net/revoke` | How a client identifies itself: - **Client ID metadata documents** (preferred): use an `https` URL you control as the `client_id`. It serves a JSON document with `client_id` (the same URL), `client_name`, `redirect_uris`, and optionally `client_uri` and `logo_uri`. Freesbe fetches it when a person connects and shows the name and logo on the connect page. `token_endpoint_auth_method` must be `none`. - **Dynamic registration** (fallback): `POST /register` with `client_name` and `redirect_uris` returns a `client_id`. Only public clients are registered. Every client is a public client using PKCE (`S256`), and must send the `resource` parameter (`https://api.freesbe.net/mcp`) on the authorize and token requests. Access tokens last an hour; the refresh token works once and comes back renewed, for 30 days. A request without a valid token gets `401` with a `WWW-Authenticate` header pointing at the resource metadata, as the specification requires; a tool the connection lacks the scope for gets `403` with `error="insufficient_scope"`. Scopes are the permissions above, by their short names (`channels:read`, `channels:history`, `groups:history`, `im:history`, `search:read`, `chat:write`, `im:write`, `reactions:write`, `users:read`, `presence:read`, `calendar:read`, `meetings:read`, `meetings:write`, `recordings:read`, `todos:read`, `todos:write`, `questions:write`, `phone:call`). Ask for what you need; a client that asks for nothing gets everything but the two private ones and `phone:call`, which places calls on your workspace's phone bill. The settings pages' own endpoints are in the [API reference](https://freesbe.net/docs/api/#ref-ai-agents). --- # Zapier, Make, n8n and Zoho Flow Source: https://freesbe.net/docs/automation/ > Start Zaps, Make scenarios, n8n workflows and Zoho Flow flows when something happens in Freesbe, and let them post messages, react and make channels. Freesbe works with the big automation tools, so you can connect it to thousands of other apps without writing code: - **[Zapier](https://zapier.com)**: Zaps, with Freesbe as a trigger or an action. - **[Make](https://www.make.com)**: scenarios on Make's visual canvas. - **[n8n](https://n8n.io)**: workflows on n8n's cloud or your own server. - **[Zoho Flow](https://www.zoho.com/flow/)**: flows between Freesbe and Zoho's own apps, like Zoho CRM, Zoho Books and Zoho People, and hundreds of others. Some flows you can build: post every new Acme Corp deal from your CRM into #sales, file a ticket when someone mentions the bot with a problem, add new teammates to your HR system the day they join, or send the notes of every recorded meeting to a shared folder. ![Workspace settings, Integrations, Automation: Zapier and n8n connected, with the triggers their flows are listening for](https://freesbe.net/docs/img/admin-automation.webp) ## Connect a tool Only owners and admins can connect a tool. 1. Open **Workspace settings → Integrations & apps → Integrations → Automation**. 2. Click **Connect** next to Zapier, Make, n8n or Zoho Flow. Freesbe shows an **API key**, once. 3. In the tool, add Freesbe and paste the key when it asks for it: - **Zapier:** pick Freesbe as a trigger or action app, and click **Sign in**. - **Make:** add a Freesbe module, then **Create a connection**. - **n8n:** install the `n8n-nodes-freesbe` community node (self-hosted n8n: **Settings → Community nodes → Install**), then add a **Freesbe API** credential. - **Zoho Flow:** add a Freesbe trigger or action to a flow, then **Connect** and paste the key. Each tool connects through an [app](https://freesbe.net/docs/apps/) of its own, with a bot named after the tool. The bot posts the tool's messages, so nobody's account is borrowed, and it keeps working when the workspace requires [single sign-on](https://freesbe.net/docs/single-sign-on/) or [multi-factor authentication](https://freesbe.net/docs/multi-factor-authentication/). You'll see it in **Workspace settings → Integrations & apps → Apps** too. If the key leaks, or you lost it before pasting it, click **New API key**. The old one stops working right away, so paste the new one into the tool's connection. ## Add the bot to channels The bot only hears what's said in channels it's in, the way a person would. - **Public channels:** pick the channel in a trigger or an action, and the bot joins it by itself. - **Private channels and DMs:** add the bot from the channel's member list first, like a person. ## Triggers A trigger starts a flow when something happens in Freesbe. Freesbe sends the event to the tool the moment it happens, so flows start right away instead of checking every few minutes. | Trigger | Starts the flow when | | -------------------------- | ----------------------------------------------------------------------- | | **New message** | Someone posts in a channel or DM the bot is in | | **New mention of the bot** | Someone @mentions the bot, such as `@Zapier file this` | | **New reaction** | Someone adds an emoji reaction to a message the bot can see | | **New file** | Someone shares a file in a channel the bot is in | | **New channel** | Someone makes a public channel | | **New person** | Someone joins the workspace | | **Meeting scheduled** | A meeting the bot is invited to is scheduled | | **Recording ready** | A huddle or meeting recording is ready, where the bot is or was invited | Make's **Watch events**, n8n's **Freesbe Trigger** and Zoho Flow's **Something happens in Freesbe** can start on any of the [app events](https://freesbe.net/docs/api/#app-events) too, such as an edited message, a huddle starting or a transcript being ready. Message triggers can be limited to one channel; leave the channel empty for every channel the bot is in. The bot never hears its own messages, so a flow that posts back can't start itself again. Each trigger gives the flow plain fields to use in later steps: the message's **text**, a **link** to it, the **channel's name**, **who** sent it, and the **Reply to** ID for answering in its thread. Email addresses are only included when the app may see the people in the workspace, as the Automation apps can. ### See what's listening Turning a flow on makes a trigger in Freesbe, and turning it off removes it. **Triggers listening**, under the tools, lists each one: the event, the tool, the channel and since when. To stop one, click its bin. Turn the flow off in the tool too, or it makes the trigger again the next time it's turned on. Turning off or deleting the tool's app in **Workspace settings → Integrations & apps → Apps** stops all of its triggers. ## Actions Actions are what a flow does in Freesbe, as the tool's bot: - **Send a message** in a channel, with Slack-style formatting. Give it a **Reply to** ID to answer in a thread, or [Block Kit](https://freesbe.net/docs/apps/#put-the-bot-to-work) blocks to post a card with headers, facts and link buttons. - **Send a direct message** to someone, picked by name or found by email. - **Add a reaction** to a message, such as ✅ when a ticket is closed. - **Create a channel**, public or private, with a topic and people in it. And searches, to find what a later step needs: - **Find a person** by email address. - **Find a channel** by name. - **Find messages** that match some words, in the channels the bot is in. Make also has **Make an API call**, for anything else in the [API](https://freesbe.net/docs/api/) the app may do. In Zoho Flow, the searches are actions too (**Find person**, **Find channel** and **Search messages**), and Zoho Flow asks for a channel's or person's ID, which a search gives you. ## What a tool's app can do The app Freesbe makes for each tool can see the workspace and its people, see and join channels, make channels, read and send messages in the channels it's in, start DMs, react, open files, search, and hear about meetings and recordings it's part of. It's a member of the workspace, never an admin, and only sees what a member in the same channels would. ## For developers The apps are built on [REST hooks](https://freesbe.net/docs/api/#apps): a bot token asks Freesbe to send one event to a URL with `POST /api/apps/hooks`, and deletes the hook with `DELETE /api/apps/hooks/{id}`. Any tool, or your own server, can use them the same way. Each delivery is signed with the app's signing secret, like the app's [events](https://freesbe.net/docs/apps/#check-the-signature), and answering `410 Gone` deletes the hook. --- # Freesbe API reference Source: https://freesbe.net/docs/api/ (OpenAPI: https://freesbe.net/docs/api/openapi.json) Every endpoint is at https://api.freesbe.net, takes and returns JSON, and needs `Authorization: Bearer ` unless it says "No token". `{w}` is a workspace id. Errors answer `{ "error": { "code": "…", "message": "…" } }`. ## Who can call what - No token: Anyone can call it, without a token. - Signed in: Any valid token. It isn't tied to a workspace, so workspace sign-in rules don't apply. - Members: Anyone in the workspace, guests included. Guests only see their own channels and the people in them. - Members, not guests: Owners, admins and members. Guests get 403 forbidden. - Channel members: People who can see the channel: anyone in the workspace for a public channel, members for a private channel or DM. - Creator or admins: The person who created the channel, or a workspace owner or admin. - Organizer or admins: The person who scheduled the meeting, or a workspace owner or admin. - Its owner: Only the person it belongs to, such as the maker of a booking page. - Admins: Workspace owners and admins. - App only: Needs the Freesbe app or a browser (a passkey prompt, a sign-in redirect, a call). Listed so you know it exists; scripts can't use it. - App token: Only an app's bot token (see Apps). People's tokens get 403 forbidden. ## Errors | Status | Code | Meaning | | --- | --- | --- | | 400 | `bad_request` | The body isn't JSON, or a parameter is malformed. | | 400 | `invalid` | A field failed validation. `message` says which. | | 401 | `unauthorized` | No token, or it expired or was signed out. | | 403 | `forbidden` | You're signed in but not allowed, for example not an admin. | | 403 | `sso_required` | The workspace requires Google or Microsoft sign-in, and this token signed in another way. | | 403 | `mfa_required` | The workspace requires multi-factor authentication, and this token hasn't verified a second step (lately). Verify with `POST /api/me/mfa/totp`. | | 403 | `missing_scope` | An app's bot token called an endpoint its scopes don't cover. `message` names the scope to add to the app's manifest. | | 403 | `not_allowed_token_type` | An app's bot token called an endpoint apps can't use, such as sign-in or your account. | | 403 | `file_blocked` | The workspace's file type rules don't let this person share this file, by its extension or because its contents are a program under another name. See File types in the guides. | | 403 | `unclaimed` | The email belongs to an account imported from Slack that nobody has taken over yet. `message` says what happens next. | | 404 | `not_found` | It doesn't exist, or you can't see it. Private channels you're not in answer 404 too. | | 410 | `not_found` | A meeting link whose meeting was canceled or has ended. | | 409 | `conflict` | It clashes with something, such as a channel name in use. | | 413 | `too_large` | The body or file is too big, or the workspace is out of storage. | | 429 | `rate_limited` | Slow down and try again in a minute. | | 502 | `upstream` | A service Freesbe depends on failed. | | 503 | `unavailable` | That feature is turned off on this server. | | 500 | `internal` | Something broke on our side. | ## App scopes An app's bot token can only call endpoints its scopes cover; each endpoint below names the scope it needs. - `team:read`: See the workspace's name and settings - `users:read`: See the people in the workspace - `users:write`: Set the app's own status - `channels:read`: See channels and who is in them - `channels:join`: Join and leave public channels - `channels:manage`: Create channels, rename them, archive them and add people - `channels:history`: Read messages in channels the app is in - `chat:write`: Send messages as the app, schedule them, and edit or delete them - `im:write`: Start direct messages with people - `reactions:write`: Add and remove emoji reactions - `pins:write`: Pin and unpin messages - `bookmarks:write`: Add, change and remove the links kept at the top of a channel - `files:read`: Open files shared in channels the app is in - `files:write`: Upload files and attach them to its messages - `search:read`: Search messages in channels the app is in - `todos:read`: See the app's own to-dos - `todos:write`: Make, change and delete the app's own to-dos - `meetings:read`: See the workspace's meetings - `meetings:write`: Schedule meetings and change the ones the app made - `recordings:read`: Open huddle and meeting recordings the app can see - `phone:call`: Start phone calls: ring someone's phone, then dial the number - `app_mentions:read`: Hear about messages that mention the app ## Events (outgoing webhooks and apps) | Event | `data` holds | App scope | | --- | --- | --- | | `message.created` | `message`, `channel` | `channels:history` | | `message.updated` | `message`, `channel` | `channels:history` | | `message.deleted` | `channelId`, `messageId`, `threadRootId`, `channel` | `channels:history` | | `file.shared` | `message` (with its `files`), `channel` | `files:read` | | `app.mentioned` | `message`, `channel`. Apps only: a message that mentions the app's bot. | `app_mentions:read` | | `reaction.added` | `channelId`, `messageId`, `emoji`, `userId`, `channel` | `channels:history` | | `reaction.removed` | `channelId`, `messageId`, `emoji`, `userId`, `channel` | `channels:history` | | `channel.created` | `channel`, `userId` (who made it) | `channels:read` | | `channel.renamed` | `channel`, `userId`, `previousName` | `channels:read` | | `channel.updated` | `channel`, `userId`. The topic changed. | `channels:read` | | `channel.archived` | `channel`, `userId` | `channels:read` | | `channel.unarchived` | `channel`, `userId` | `channels:read` | | `member.joined` | `channel`, `userId` | `channels:read` | | `member.left` | `channel`, `userId` | `channels:read` | | `user.joined` | `member`: someone joined the workspace | `users:read` | | `user.updated` | `member`: their name, title, picture or role changed | `users:read` | | `user.deactivated` | `member` | `users:read` | | `user.status_changed` | `status` | `users:read` | | `huddle.started` | `huddleId`, `channel`, `userId` (who started it), `startedAt` | `channels:read` | | `huddle.joined` | `huddleId`, `channel`, `userId`, `startedAt` | `channels:read` | | `huddle.left` | `huddleId`, `channel`, `userId`, `startedAt` | `channels:read` | | `huddle.ended` | `huddleId`, `channel`, `startedAt`, `endedAt`, `participantIds` | `channels:read` | | `meeting.scheduled` | `meeting`: `id`, `code`, `title`, `startsAt`, `durationMin`, `timeZone`, `organizerId`, `inviteeUserIds` | `meetings:read` | | `meeting.updated` | `meeting` | `meetings:read` | | `meeting.canceled` | `meeting` | `meetings:read` | | `meeting.guest_waiting` | `meeting`, `guestName`: someone knocked in the waiting room | `meetings:read` | | `booking.created` | `booking`: `meetingId`, `bookingLinkId`, `userId`, `name`, `email`, `note`, `answers` (each `questionId`, `kind`, `label` and `answer`), `startsAt`, `durationMin` | Webhooks only | | `recording.ready` | `recording`: `id`, `channelId`, `meetingId`, `startedAt`, `durationMs`; and `channel` | `recordings:read` | | `transcript.ready` | `recording`, `channel` | `recordings:read` | | `time.clocked_in` | `userId`, `at` | Webhooks only | | `time.clocked_out` | `userId`, `at` | Webhooks only | | `phone.call_ended` | `call`: `id`, `userId`, `outcome` (`answered`, `busy`, `no_answer`, `rejected`, `unreachable` or `failed`), `seconds`, `answeredAt`, `endedAt`, `leadId`, `vars`. A click-to-call is over. | `phone:call` | | `webhook.test` | `text`. Sent by the test endpoint and the Send test button. Webhooks only. | Webhooks only | ## Account and profile Who you are, across every workspace. These work with any token, even in a workspace that requires single sign-on or multi-factor authentication. ### GET /api/meta Who: No token. The server's name and version, the sign-up policy (`first`, `invite` or `open`), the single sign-on providers it has, whether it can send email, and whether the mobile and desktop apps talk to it (`phoneApps`, false on test copies, so a phone can't approve sign-ins there). Answer: ```json { "name": "Freesbe", "version": "…", "signup": "invite", "sso": ["google", "microsoft", "apple"], "email": true, "turnstileSiteKey": null, "phoneApps": true } ``` ### GET /api/health Who: No token. Answers `{ "ok": true }` while the API is up. ### POST /api/auth/signup Who: No token. Create an account. Needs an `inviteToken` unless sign-up is open; the invite also joins its workspace. `acceptTerms: true` records that the person agreed to the current Terms of Service and Privacy Policy; without it the apps ask them to agree before they go on. Answers 201 like login. 10 sign-ups a minute from one IP address. Request: ```json { "email": "deploy-bot@northwind.example", "name": "Deploy bot", "password": "at least 8 characters", "timezone": "Europe/London", "inviteToken": "q7Zr…", "acceptTerms": true, "wantToken": true } ``` ### POST /api/auth/login Who: No token. Sign in with an email and password. Send `wantToken: true` to get a token in the answer. 10 tries a minute per address and per email. Request: ```json { "email": "deploy-bot@northwind.example", "password": "…", "wantToken": true } ``` Answer: ```json { "user": { … }, "workspaces": [ … ], "canCreateWorkspaces": false, "importedHistory": false, "authMethod": "password", "mfaVerifiedAt": null, "emailVerified": true, "token": "kX3q…" } ``` ### POST /api/auth/logout Who: Signed in. End the session the token belongs to. Answers 204. ### GET /api/me Who: Signed in. The signed-in person, their workspaces, how this session signed in and whether it has verified a second step. Answer: ```json { "user": { "id": "01JB8Q3V7RZ4T5K2M9XWD6HY1C", "email": "dana@northwind.example", "name": "Dana Whitfield", "avatarUrl": "/api/avatars/01JB8Q3V7RZ4T5K2M9XWD6HY1C?v=01JB9…", "timezone": "Europe/London" }, "workspaces": [{ "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", … }], "canCreateWorkspaces": true, "importedHistory": false, "authMethod": "password", "mfaVerifiedAt": null, "emailVerified": true, "lobbyEmail": true } ``` ### PATCH /api/me Who: Signed in. Change your `name` (up to 80 characters) or `timezone` (an IANA zone), set your `workingHours` (seven days, Sunday first, each a list of `[start, end]` minutes after midnight in your time zone; `null` goes back to Monday to Friday 9 to 5), or turn off the email about guests waiting in your meetings' lobbies (`lobbyEmail`). Answers like `GET /api/me`. Request: ```json { "name": "Dana Whitfield", "timezone": "America/New_York" } ``` ### GET /api/me/unread Who: Signed in. What waits for you in each of your workspaces: `unread` when a channel or DM you're in (and haven't muted) has messages you haven't read, and `attention`, the mentions of you in channels plus unread direct messages; `inbox`, the open customer conversations that are yours or nobody's; and `latestCustomer`, the newest customer message among them that should alert you (null in a muted inbox channel or while your notifications there are paused). The apps show it on the workspace icons and alert on new customer messages. Answer: ```json { "workspaces": [{ "workspaceId": "01J…", "unread": true, "attention": 2, "inbox": 1, "latestCustomer": { "conversationId": "01J…", "at": 1793900000000, "from": "Jordan Fields", "source": "Website" } }] } ``` ### PUT /api/me/password Who: Signed in. Change your password, or set one on an account made with Google or Microsoft (leave out `currentPassword`). Signs out every other session. Answers 204. Request: ```json { "currentPassword": "…", "newPassword": "…" } ``` ### PUT /api/me/avatar Who: Signed in. Set your picture: the raw PNG, JPEG, GIF or WebP bytes with their `Content-Type`, up to 2 MB. Answers like `GET /api/me`. Request: ```json curl -X PUT $API/api/me/avatar \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: image/png' --data-binary @dana.png ``` ### DELETE /api/me/avatar Who: Signed in. Remove your picture, so your initials show. Answers like `GET /api/me`. ### GET /api/me/avatar/{provider} Who: Signed in. The photo Google or Microsoft (`google` or `microsoft`) shared the last time you signed in with it, as image bytes, kept even when you use another picture. Answers 404 when there's none. To use it, upload it with `PUT /api/me/avatar`. ### POST /api/me/photo-handoffs Who: Signed in. A one-time link for taking your picture with your phone, shown as a QR code. `url` opens a page in the phone's browser that sends one photo back. It works once, for 10 minutes. Nothing changes until you upload the picture with `PUT /api/me/avatar`. Answer: ```json { "code": "kq3V…", "url": "https://app.freesbe.net/snap#kq3V…", "expiresAt": 1761462600000 } ``` ### GET /api/me/photo-handoffs/{code} Who: Signed in. Whether the phone sent the photo: `waiting`, `ready`, or `gone` (expired, cancelled or already picked up). Answer: ```json { "state": "ready" } ``` ### POST /api/me/photo-handoffs/{code}/photo Who: Signed in. The phone's photo as raw image bytes, once. The link is used up. ### DELETE /api/me/photo-handoffs/{code} Who: Signed in. Cancel the link, and drop a photo the phone already sent. Answers 204. ### GET /api/photo-handoffs/{code} Who: No token. For the phone's page: the first name of the person whose picture it takes. 404 once the link expired or was used. Answer: ```json { "name": "Dana", "expiresAt": 1761462600000 } ``` ### PUT /api/photo-handoffs/{code} Who: No token. For the phone's page: the photo, as raw JPEG, PNG or WebP bytes with their `Content-Type`, up to 8 MB. One per link. Answers 204. ### GET /api/avatars/{userId} Who: Signed in. Someone's picture, if you share a workspace with them. `avatarUrl` on people points here. ### GET /api/me/sign-in Who: Signed in. How you can sign in: whether you have a password, your linked Google and Microsoft accounts, your passkeys, and whether an authenticator app is set up. Answer: ```json { "hasPassword": true, "identities": [{ "provider": "google", "email": "dana@northwind.example", "createdAt": …, "lastUsedAt": … }], "passkeys": [{ "id": "01JB…", "name": "MacBook", "backedUp": true, "createdAt": …, "lastUsedAt": … }], "totp": true } ``` ### DELETE /api/me/identities/{provider} Who: Signed in. Unlink your `google` or `microsoft` account. Answers 204. ### POST /api/me/claims Who: Signed in. When `importedHistory` is true, emails you a link for each Slack account imported under your address, to add its history to your account. Answers 204. ## Email sign-in and your address These need a server that can send email (`email: true` in `/api/meta`), and answer 503 `unavailable` otherwise. ### POST /api/auth/email-code Who: No token. Email a one-time sign-in code. Answers 204 whether or not the address has an account, so it doesn't reveal who does. 10 codes a day per address. Request: ```json { "email": "dana@northwind.example" } ``` ### POST /api/auth/email-code/verify Who: No token. Sign in with the emailed code. Answers like login. Request: ```json { "email": "dana@northwind.example", "code": "482913", "wantToken": true } ``` ### POST /api/auth/verify-email Who: No token. Verify an address from the link in a verification email (its `token`). Answers `{ "email": "…" }`. ### POST /api/me/email/verification Who: Signed in. Email yourself a new verification code and link. Answers 204. ### POST /api/me/email/verify Who: Signed in. Verify your address with the emailed `code`. Answers like `GET /api/me`. Request: ```json { "code": "482913" } ``` ### PUT /api/me/email Who: Signed in. Start changing your address: emails a code to the new one. Needs `currentPassword` when the account has a password. Answers 204. Request: ```json { "email": "dana.whitfield@northwind.example", "currentPassword": "…" } ``` ### POST /api/me/email/confirm Who: Signed in. Finish the change with the `code` sent to the new address. Answers like `GET /api/me`. Request: ```json { "code": "482913" } ``` ## Passkeys, Google and Microsoft These follow the browser's WebAuthn and OAuth flows, so they're for the Freesbe apps. You can list, rename and remove passkeys from a script. ### POST /api/auth/passkey/options Who: App only. WebAuthn options for `navigator.credentials.get()`. ### POST /api/auth/passkey/login Who: App only. Sign in with the passkey's assertion (`response`). Answers like login. ### GET /api/me/passkeys Who: Signed in. Your passkeys: `id`, `name`, `backedUp`, `createdAt`, `lastUsedAt`. ### POST /api/me/passkeys/options Who: App only. WebAuthn options for `navigator.credentials.create()`. Refused with a token: add passkeys from the app. ### POST /api/me/passkeys Who: App only. Save a new passkey from the browser's `response`, with an optional `name`. ### PATCH /api/me/passkeys/{id} Who: Signed in. Rename a passkey. Request: ```json { "name": "Work laptop" } ``` ### DELETE /api/me/passkeys/{id} Who: Signed in. Remove a passkey. Answers 204. ### GET /api/auth/oauth/{provider}/start Who: App only. Open in the browser to sign in with `google`, `microsoft` or `apple`. `?next=/path` returns there, `?invite=` signs up through an invite and `?link=1` links the account instead. ### GET /api/auth/oauth/{provider}/callback Who: App only. Where Google and Microsoft send people back. ### POST /api/auth/oauth/apple/callback Who: App only. Where Apple posts its answer (a form with `code`, `state` and, the first time, `user`). Answers 303 to the GET callback with the same fields. ### POST /api/auth/handoff Who: App only. The desktop app's sign-in through the system browser: a one-time code for a PKCE `challenge`. ### POST /api/auth/handoff/redeem Who: App only. The desktop app trades the `code` and its `verifier` for a session. ## Sessions and multi-factor authentication In a workspace that requires multi-factor authentication, its endpoints answer 403 `mfa_required` until the token verifies a second step. A script verifies with an authenticator app: set one up once with `/api/me/totp`, then send a code to `/api/me/mfa/totp` after each sign-in, and again when the workspace asks. ### GET /api/me/sessions Who: Signed in. Everywhere you're signed in: device, app, sign-in method, address and rough location, when it started and was last used. `current` marks this one. Answer: ```json { "sessions": [ { "id": "…", "current": true, "method": "password", "createdAt": …, "lastActiveAt": …, "ip": "203.0.113.24", "location": "London, United Kingdom", … } ] } ``` ### DELETE /api/me/sessions Who: Signed in. Sign out every other session, keeping this one. Answers 204. ### DELETE /api/me/sessions/{id} Who: Signed in. Sign out one session. Answers 204. ### GET /api/me/alerts Who: Signed in. Which of your devices alert you for new messages and reminders (Preferences → Notifications → Alert me on). `alertOn` is `using` (the device you're using, the default), `all` (every device) or `chosen` (only the devices whose `alerts` switch is on). `devices` lists everywhere you're signed in, each a session, with the name its app reported (`name`, or null). Calls ring on every device whatever this says. Answer: ```json { "alertOn": "chosen", "devices": [ { "id": "…", "current": true, "name": "Maya's MacBook Air", "client": "desktop", "os": "macOS", "browser": null, "lastActiveAt": …, "alerts": true } ] } ``` ### PUT /api/me/alerts Who: Signed in. Change any of them: `alertOn`, `devices` (session ids to true or false) and `deviceName`, this device's own name (up to 60 characters; null forgets it). Your open apps follow straight away. Answers like `GET /api/me/alerts`. Request: ```json { "alertOn": "chosen", "devices": { "…": false } } ``` ### POST /api/me/terms Who: Signed in. Agree to the current Terms of Service and Privacy Policy. Send the `version` you were shown; an older one answers 409. Signed-in browser sessions only. `GET /api/me` has `termsVersion`, the version you last agreed to, or null. Answers like `GET /api/me`. Request: ```json { "version": "2026-10-28" } ``` ### GET /api/me/security-activity Who: Signed in. Your security activity, newest first, 50 at a time: each sign-in, second step, sign-out and change to how you sign in (`event`: `sign_in`, `sign_in.failed`, `mfa.verified`, `mfa.failed`, `sign_out`, `session.ended`, `sessions.ended`, `password.set`, `password.changed`, `email.changed`, `authenticator.added`, `authenticator.removed`, `passkey.added`, `passkey.removed`, `sso.linked`, `sso.unlinked`, `signed_out_by_admin`, `sign_in.denied` when you said a New sign-in alert wasn't you, `sign_in.confirmed` when you said it was, `sign_in.approved` and `sign_in.rejected` when your phone answered a sign-in or verification request), with what happened (`detail`: the sign-in `method`, `newDevice` and `newCountry` on a sign-in that sent a New sign-in alert, a passkey's `name`, the `provider`, and on phone answers the `approver` phone, the `kind` of request, the `device` and `location` that asked, and `wrongNumber` or `reported`), the browser or app, the IP address and where it was. Narrow it with `?event=`, `?from=` and `?to=` (times in milliseconds) and `?limit=` (up to 5000). When `more` is true, pass the last event's `id` as `?before=` for the next page. Events are kept for a year. Answer: ```json { "events": [ { "id": "01JB8ZK2W1E9R4M3T6Y7U8I9OP", "at": 1792729800000, "userId": "01JB8ZH7L9M1N3P5Q7R9S1T3UV", "event": "sign_in", "detail": { "method": "passkey" }, "ip": "203.0.113.24", "country": "GB", "location": "London, United Kingdom", "client": "web", "browser": "Chrome", "os": "Windows" } ], "more": false } ``` ### GET /api/me/deletion Who: Signed in. Whether your account is being deleted (`pending`, with the purge date), what stops you deleting it (`blockers`: workspaces you own, `owner` with other people in them or `alone`), and the workspaces you'd leave. Answer: ```json { "pending": null, "blockers": [], "workspaces": [{ "id": "01JB…", "name": "Northwind Traders" }] } ``` ### POST /api/me/deletion Who: Signed in. Deletes your account in 30 days: you leave every workspace and every session, passkey and app or agent grant ends now. Signed-in browser sessions only, with a second factor verified in the last 15 minutes (`mfa_required`). Answers 409 while you own a workspace. Until the purge, signing in works but every other endpoint answers 403 `account_deleting`. Request: ```json { "reason": "Moving to another team" } ``` Answer: ```json { "purgeAt": 1795322400000 } ``` ### DELETE /api/me/deletion Who: Signed in. Keeps your account, if it's still waiting to be deleted. Workspaces' admins reactivate you. Answers 204. ### GET /api/me/mfa Who: Signed in. When this session last verified a second step, and which ways it can: `phone` (a phone set up to approve sign-ins), `passkey`, `totp`, `email`, `password`. `setup` means none yet: setting up a passkey or authenticator app counts. Answer: ```json { "verifiedAt": null, "phone": false, "passkey": false, "totp": true, "email": true, "password": false, "setup": false } ``` ### POST /api/me/mfa/totp Who: Signed in. Verify with a `code` from your authenticator app. Each code works once. Answers like `GET /api/me`. Request: ```json { "code": "123456" } ``` ### POST /api/me/mfa/email Who: Signed in. Email a code to verify with. Not after signing in with an emailed code. Answers 204. ### POST /api/me/mfa/email/verify Who: Signed in. Verify with the emailed `code`. Answers like `GET /api/me`. ### POST /api/me/mfa/password Who: Signed in. Verify with your `password`, only after signing in with an emailed code. Answers like `GET /api/me`. ### POST /api/me/mfa/passkey/options Who: App only. WebAuthn options to verify with one of your passkeys. ### POST /api/me/mfa/passkey Who: App only. Verify with the passkey's assertion. ### POST /api/me/mfa/phone Who: Signed in. Ask your phones to approve this session's second step. Show the `number`: the phone asks for it among three. Lasts two minutes; at most two can wait at once, and none for 15 minutes after the phone reports one that wasn't you. Answer: ```json { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "number": 47, "expiresAt": 1792771320000, "phones": 1 } ``` ### POST /api/me/mfa/phone/{id} Who: Signed in. Whether the phone answered: `pending`, `denied`, `expired`, or `approved` with `me` (like `GET /api/me`), which verifies this session once. `DELETE` stops waiting. Answer: ```json { "status": "pending" } ``` ### POST /api/auth/phone Who: No token. Sign in with no password: ask that person's phones to approve. Show the `number` and keep the `secret` for the checks. Answers the same whether or not the address has an account or a phone, so it doesn't reveal who does. `DELETE` with `{ id, secret }` stops waiting. Request: ```json { "email": "dana@northwind.example" } ``` Answer: ```json { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "number": 63, "expiresAt": 1792771320000 } ``` ### POST /api/auth/phone/check Who: No token. Whether the phone answered: `pending` or `denied`, or `approved` with `auth` (like login), which signs in once. Counts as the second step. Stop asking at `expiresAt`. Request: ```json { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "wantToken": true } ``` Answer: ```json { "status": "pending" } ``` ### GET /api/me/approver-devices Who: Signed in. Your phones that approve sign-ins. `DELETE /api/me/approver-devices/{id}` removes one (from another device, it needs a second step verified in the last hour, like other sign-in methods). Phones are set up in the mobile app. Answer: ```json { "devices": [{ "id": "01JC8Z…", "name": "iPhone", "platform": "ios", "createdAt": 1792540320000, "lastUsedAt": 1792760520000, "current": false }] } ``` ### POST /api/me/mfa/launch Who: App only. The app calls this each time it opens. For workspaces that ask each time Freesbe opens, it forgets this session's verification. ### POST /api/me/totp Who: Signed in. Start setting up an authenticator app: a new Base32 `secret` and an `otpauth://` `uri` for a QR code. Answer: ```json { "secret": "JBSWY3DPEHPK3PXP…", "uri": "otpauth://totp/Freesbe:deploy-bot%40northwind.example?secret=…" } ``` ### PUT /api/me/totp Who: Signed in. Turn the authenticator app on with a first `code` from it. This counts as verifying. Answers like `GET /api/me`. Request: ```json { "code": "123456" } ``` ### DELETE /api/me/totp Who: Signed in. Remove the authenticator app. Answers 204. ## Workspaces and settings ### GET /api/workspaces Who: Signed in. The workspaces you belong to, with your `role` in each. ### POST /api/workspaces Who: Signed in. Create a workspace and become its owner, when `canCreateWorkspaces` is true. `region` is where its data is stored: `eu` or `us` (guaranteed to stay in the region), or `apac` or `oc` (Asia-Pacific or Oceania, placed close to the team without that guarantee; see Data residency in the guides). It can't be changed afterwards; without it, the region closest to where the request comes from is used. The workspace's `region` is null for workspaces made before regions. Answers 201. Request: ```json { "name": "Northwind Traders", "region": "eu" } ``` Answer: ```json { "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "identityProvider": { "id": "nwOkta7Rk2Qm9Xc4Lp8Vt", "preset": "okta" }, "requireMfa": true, "mfaReverifyHours": 168, "adminCenterMfa": "strong", "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "membersCanLeave": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } ``` ### GET /api/workspaces/{w} Who: Members. App scope: `team:read`. One workspace. Answer: ```json { "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "identityProvider": { "id": "nwOkta7Rk2Qm9Xc4Lp8Vt", "preset": "okta" }, "requireMfa": true, "mfaReverifyHours": 168, "adminCenterMfa": "strong", "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "membersCanLeave": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } ``` ### PATCH /api/workspaces/{w} Who: Admins. Change the `name`, require single sign-on (`requireSso`: `google`, `microsoft`, `any`, `sso:` for the workspace's own identity provider once its test sign-in went through, or null; the provider itself is set up in the Admin Center, not through the API), require multi-factor authentication (`requireMfa`, and `mfaReverifyHours`: null, 0, 12, 24 or 168), choose what opening the Admin Center takes (`adminCenterMfa`: `strong`, the default, for a passkey, a phone approval or an authenticator app code, or `any` second step; signed-in sessions only, never a token, and `any` needs a strong step verified in the last 15 minutes, `strong_mfa_required` otherwise), allow huddle and meeting `recordings`, give them `transcripts` and AI notes, transcribe every huddle (`autoTranscribe`), share meetings' notes outside the workspace by a private link (`guestTranscripts`, with `guestTranscriptEmails` to email meeting guests theirs automatically, `guestTranscriptDays`: the longest a link works, 1, 3, 7, 14 or 30, and `guestTranscriptVideo` to let links play the recording; turning these on asks people with an approving phone again), and delete recordings after `recordingRetentionDays` (30, 90, 365 or null), choose who can start and stop recordings (`recordingControl`: `everyone`, `starter`, the default, or `hosts`), and turn AI summaries and translations on or off (`ai`). `lobbyRemindMin` (0, 1, 2, 3, 5 or 10) and `lobbyEmailMin` (0, 5, 10, 15 or 30) set when a meeting's hosts get a louder alert and an email about a guest waiting in its lobby; 0 is off. `lobbyMusic` picks what guests hear while they wait in a meeting's lobby: `lofi` (the default), `drift`, `sunny`, `classic`, `bossa` or `off`. `workflowBuilders` is `members` or `admins`: who can make workflows. `editWindowMin` (null for any time, 0 for never, 5, 60, 1440 or 10080) is how many minutes people can edit a message after sending it, `membersCanDelete` lets members and guests delete their own messages, and `adminsCanDelete` lets owners and admins delete anyone's message in a channel. `membersCanLeave` (on by default) lets members and admins leave the workspace on their own; guests always can. `readReceipts` is where people see who read their messages: `off` (the default), `small` (DMs and channels of up to 20 people) or `all` (bigger channels show a count, not names); `readReceiptsOptOut` lets each person turn their own off. `fileRules` sets which files `members` and `guests` can share: each has a `preset` (`any`, `safe` or `only`), `kinds` for `only`, and `allow` and `block` extension lists (lists and `only` need Corporate or Enterprise; see File types in the guides). Your own session must meet a new sign-in rule first, so you can't lock yourself out. Request: ```json { "requireMfa": true, "mfaReverifyHours": 168, "recordingRetentionDays": 90 } ``` Answer: ```json { "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "identityProvider": { "id": "nwOkta7Rk2Qm9Xc4Lp8Vt", "preset": "okta" }, "requireMfa": true, "mfaReverifyHours": 168, "adminCenterMfa": "strong", "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "membersCanLeave": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } ``` ### PUT /api/workspaces/{w}/read-receipts Who: Members. Turn your own read receipts on or off with `show`. Off works both ways: others don't see how far you read, and you don't see how far they did. Refused with 403 when admins turned `readReceiptsOptOut` off. Answers with the workspace, whose `showReadReceipts` is your choice. Request: ```json { "show": false } ``` Answer: ```json { "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "identityProvider": { "id": "nwOkta7Rk2Qm9Xc4Lp8Vt", "preset": "okta" }, "requireMfa": true, "mfaReverifyHours": 168, "adminCenterMfa": "strong", "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "membersCanLeave": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } ``` ### GET /api/workspaces/{w}/message-deletions Who: Admins. The last 100 messages owners and admins deleted for someone else, newest first: the channel, whose message it was (`authorId`, or `authorName` for an app), who deleted it and when, whether it was in a thread and how many files it had. What it said isn't kept. An admin's `DELETE /api/messages/{m}/files/{f}` on someone else's message is listed too, with `fileName` set to the file's name (the message itself stays); `fileName` is null when the whole message was deleted. Answer: ```json { "deletions": [ { "id": "01JB8ZK2W1E9R4M3T6Y7U8I9OP", "channelId": "01JB8ZJ4Q6S0D2F4G6H8J0K2LM", "channelName": "general", "messageId": "01JB8ZK0A2B4C6D8E0F2G4H6JK", "authorId": "01JB8ZH9N1P3Q5R7S9T1V3W5XY", "authorName": "Tom Becker", "deletedBy": "01JB8ZH7L9M1N3P5Q7R9S1T3UV", "deletedAt": 1792729800000, "inThread": false, "fileCount": 0, "fileName": null } ] } ``` ### GET /api/workspaces/{w}/remote-control/sessions Who: Admins. The remote control log: the last 100 requests to control someone's shared screen, newest first. `kind` is `huddle` or `meeting` (a support session, where `sharerName` is the name the customer gave). `startedAt` is null when the request was declined or never answered, and `endReason` is `declined`, `expired`, `stopped`, `left`, `share_stopped` or `turned_off`. Answer: ```json { "sessions": [ { "id": "01JC4T…", "kind": "huddle", "channelId": "01JB8S…", "viewerId": "01JB8Q…", "sharerId": "01JB8R…", "sharerName": null, "requestedAt": 1792729800000, "startedAt": 1792729806000, "endedAt": 1792730400000, "endedBy": "01JB8R…", "endReason": "stopped" } ] } ``` ### GET /api/workspaces/{w}/exports Who: Admins. The workspace's last 20 data exports, newest first: what each covers (`scope` `public` or `all`, `includeFiles`, `from`, `to`), its `status` (`queued`, `running`, `ready`, `failed` or `expired`), progress, and its zips (`parts`) once ready. `canExportAll` says whether you can include private channels and DMs, and `allNeeds` why not (`owner` or `plan`). Answer: ```json { "exports": [ { "id": "01JC5E…", "scope": "public", "includeFiles": true, "from": null, "to": null, "status": "ready", "createdBy": "01JB8Q…", "createdAt": 1792729800000, "finishedAt": 1792730100000, "expiresAt": 1793334900000, "error": null, "progress": { "channels": 84, "channelsDone": 84, "messages": 41250, "files": 312 }, "parts": [ { "n": 1, "size": 734003200 } ] } ], "canExportAll": false, "allNeeds": "plan" } ``` ### POST /api/workspaces/{w}/exports Who: Admins. Start an export in Slack's export format. `scope` `all` (private channels and DMs too) is for owners on plans that include it, and emails every owner. One export runs at a time (409) and a workspace can start one a day (429). `from` and `to` limit it to messages in that time. Request: ```json { "scope": "public", "includeFiles": true, "from": null } ``` Answer: ```json { "export": { "id": "01JC5E…", "status": "queued", … } } ``` ### POST /api/workspaces/{w}/exports/{id}/parts/{n}/link Who: Admins. Only for the admin who started the export: a download link for zip `n` of a ready export, working for 15 minutes without signing in. Needs a second factor verified in the last 15 minutes: `mfa_required` otherwise. Each link is recorded in the audit log. Answer: ```json { "url": "https://app.freesbe.net/api/export-downloads/kQ3…", "expiresAt": 1792730700000 } ``` ### GET /api/workspaces/{w}/audit Who: Admins. The audit log, newest first, 50 at a time: what owners and admins changed (`action`: `settings.changed`, `member.role`, `member.deactivated`, `member.reactivated`, `invite.created`, `invite.emailed`, `invite.revoked`, `webhook.created`, `webhook.deleted`, `app.created`, `app.deleted`), who did it (`actorId`), to whom (`targetId`), what changed (`detail`) and the IP address. When `more` is true, pass the last entry's `id` as `?before=` for the next page. Entries are kept for a year. Answer: ```json { "entries": [ { "id": "01JB8ZK2W1E9R4M3T6Y7U8I9OP", "at": 1792729800000, "actorId": "01JB8ZH7L9M1N3P5Q7R9S1T3UV", "action": "member.role", "targetId": "01JB8ZH9N1P3Q5R7S9T1V3W5XY", "detail": { "role": "admin", "from": "member" }, "ip": "203.0.113.24" } ], "more": false } ``` ### GET /api/workspaces/{w}/security-activity Who: Admins. Members' security activity, newest first, 50 at a time: their sign-ins, second steps, sign-outs and changes to how they sign in, only from when they joined the workspace. The same events and filters as `GET /api/me/security-activity`, plus `?user=` for one member. Answer: ```json { "events": [{ "id": "…", "at": 1792729800000, "userId": "…", "event": "sign_in.failed", "detail": { "method": "password" }, "ip": "203.0.113.24", "country": "GB", … }], "more": false } ``` ### PUT /api/workspaces/{w}/logo Who: Admins. Set the logo: raw PNG, JPEG, GIF or WebP bytes with their `Content-Type`, up to 2 MB. ### DELETE /api/workspaces/{w}/logo Who: Admins. Remove the logo, so the workspace's initials show. ### GET /api/workspace-logos/{w}/{version} Who: No token. The logo image. `logoUrl` points here; the version can't be guessed, so only people who saw the URL can fetch it. ### GET /api/workspaces/{w}/boot Who: Members. Everything the app needs to start in one call: you, the workspace, members, your channels and DMs with unread counts, statuses, presence, live huddles, the event cursor `seq` for the WebSocket, and which features are on. ### GET /api/workspaces/{w}/cache-key Who: Members. The key the app encrypts its local cache of the workspace with. It changes when you lose access. ### GET /api/workspaces/{w}/allowance Who: Admins. The usage period's video, and storage, against the workspace's limits (`period`: its first day `start`, last day `end`, and `resets`, the billing day allowances start again, as UTC dates), and why cameras and screen sharing are paused, if they are (`paused`, `cap`, `allowance` or `budget`, the workspace's spending limit). Answer: ```json { "period": { "start": "2026-11-14", "end": "2026-12-13", "resets": "2026-12-14" }, "videoGb": 41.2, "videoGbLimit": 200, "storageGb": 3.8, "storageGbLimit": 50, "videoPaused": null } ``` ### GET /api/workspaces/{w}/billing Who: Admins. The workspace's plan and subscription, add-ons (`addOns`) and the usage period's use past the plan's allowances (`usage`, charged when the period ends; `usagePeriod` has its `start`, `end` and `resets` days, from the workspace's billing day): `status` (`none`, `trialing`, `active`, `past_due`, `canceled` or `comped`), yearly or monthly `interval`, `seats` (active members, not guests) and `seatsBilled`, what the workspace gets (`limits` and `features`, its plan's with any changes Freesbe made for it), the plans on sale (`plans`, from the pricing catalog), when the trial or period ends, and up to 24 invoices, newest first. Amounts are in cents. `enforced` says whether the plan's limits apply yet. `history` is the same as `GET /api/workspaces/{w}/billing/history`. Answer: ```json { "plan": "business", "planName": "Business", "status": "active", "interval": "year", "seats": 8, "seatsBilled": 8, "limits": { "members": 500, "historyDays": null, ... }, "features": ["groupVideo", ...], "plans": [{ "id": "free", "name": "Free", "prices": {}, ... }, ...], "trialEndsAt": null, "periodEndsAt": 1803945600000, "canceling": false, "pastDueSince": null, "invoices": [{ "id": "in_1", "number": "NWT-0001", "status": "paid", "total": 38400, "tax": 0, "currency": "usd", ... }], "paymentsOn": true, "enforced": false, "addOns": [{ "id": "phone", "name": "Phone system", "per": "seat", "units": null, "prices": { "month": 1000, "year": 9600 }, "quantity": 1, "billedQuantity": 8, "needs": [], ... }], "usage": [{ "id": "phone_minutes", "name": "Extra phone minutes", "unit": "minutes", "used": 8420, "included": 8000, "extra": 420, "price": 2, "per": 1, "amount": 840 }], "usagePeriod": { "start": "2026-11-14", "end": "2026-12-13", "resets": "2026-12-14" }, "history": { "hidden": null, "upcoming": null } } ``` ### GET /api/workspaces/{w}/billing/history Who: Admins. Messages the plan's history hides (Free shows the last 90 days; older ones are kept, never deleted). `hidden`: hidden now, those from before `before`. `upcoming`: when older messages will start being hidden (`at`), why (`canceling`, `unpaid`, `trial` or `starting`) and which (`before`). Each is null unless there are messages it applies to. Answer: ```json { "hidden": null, "upcoming": { "at": 1803945600000, "reason": "canceling", "days": 90, "before": 1796169600000 } } ``` ### PUT /api/workspaces/{w}/billing/add-ons/{addOn} Who: Admins. Adds an add-on to the workspace's subscription, changes how many (for add-ons bought by the unit, like call center agents), or removes it with `quantity` 0. Add-ons charged per person or counted by Freesbe (phone numbers) take 1 or 0. 409 without a paid plan, when it needs another add-on first, or when the card is declined. Returns the Billing page's data. Request: ```json { "quantity": 3 } ``` Answer: ```json { "plan": "business", "addOns": [...], ... } ``` ### POST /api/workspaces/{w}/billing/checkout Who: Admins. Where to send an admin to pay for `plan` (a plan in the catalog that's bought by card, like `business`) yearly or monthly (`interval`: `year` or `month`): Stripe Checkout, or, when the workspace already pays, Stripe's page to confirm the switch. `returnPath` is the app page to come back to. 503 until payments are set up on the server. Request: ```json { "plan": "business", "interval": "year", "returnPath": "/northwind/c/general" } ``` Answer: ```json { "url": "https://checkout.stripe.com/c/pay/cs_…" } ``` ### POST /api/workspaces/{w}/billing/portal Who: Admins. A link to Stripe's billing portal for the workspace: card, billing details, invoices and cancelling. 409 when the workspace has never paid. Request: ```json { "returnPath": "/northwind/c/general" } ``` Answer: ```json { "url": "https://billing.stripe.com/p/session/…" } ``` ### GET /api/catalog Who: No token. The published pricing catalog: every plan with its prices in cents per person (`month`, `year`), limits (null is no limit) and features, plus add-ons and metered prices. Cached for 5 minutes. Answer: ```json { "catalog": { "version": 3, "currency": "usd", "defaultPlan": "free", "plans": [{ "id": "business", "name": "Business", "prices": { "month": 500, "year": 4800 }, "limits": { "members": 500, ... }, "features": ["groupVideo", ...], ... }, ...], "addOns": [], "meters": [...] } } ``` ### GET /api/internal/status-signals Who: No token. For [status.freesbe.net](https://status.freesbe.net/) only: the counts it turns into the AI, email, webhook and phone rates, to a caller whose `X-Status-Secret` header matches the status page's shared secret. Anyone else gets 401. What the status page itself shows is at [status.freesbe.net/api/summary.json](https://status.freesbe.net/api/summary.json) and `/api/status.json`, open to everyone. Answer: ```json { "at": 1762156800000, "ai": { "failed": 1, "total": 840 }, "email": { "failed": 0, "total": 312 }, "webhooks": { "failed": 2, "total": 1460 }, "phone": { "failed": 0, "total": 212 } } ``` ## People and roles ### GET /api/workspaces/{w}/members Who: Members. App scope: `users:read`. Everyone in the workspace, deactivated people included. Guests get the people they share channels with. On a phone system from Freesbe, `extension` is the number colleagues dial to reach them. Answer: ```json { "members": [ { "userId": "01JB8R…", "name": "Priya Raman", "role": "member", "title": "Support lead", "extension": "102", … } ] } ``` ### PATCH /api/workspaces/{w}/members/{userId} Who: Members. Anyone can set their own `title`. Admins change anyone's title, make someone an `admin` or `member` (a guest made a member joins the whole workspace), and set `deactivated`. People admins change titles and `deactivated` for members and guests, never admins. The owner's role can't change this way (see `/owner`), and nobody can deactivate themselves. Admins (and people admins, for members and guests) can also set someone's account `name` and `email`, only when their address and the new one are at one of the workspace's verified email domains, and never their own or the owner's; the old address is told about a new one. Changing a role, `deactivated` or `email` needs a recent second step. Request: ```json { "role": "admin" } ``` Answer: ```json { "member": { "userId": "01JB8R…", "name": "Priya Raman", "email": "priya@northwind.example", "avatarUrl": null, "timezone": "Asia/Singapore", "role": "member", "title": "Support lead", "deactivated": false, "pending": false, "company": null } } ``` ### PUT /api/workspaces/{w}/members/{userId}/admin-roles Who: Admins. Gives a member limited admin roles, replacing what they had: `billing`, `phone`, `people` and `helpdesk`, each opening its own part of the Admin Center. An empty list takes them away. Members only (owners and admins can do it all); giving roles needs Corporate or Enterprise. Signed-in browser sessions only. The member's `adminRoles` lists them, and the workspace's `adminRoles` says which ones work for you now. Request: ```json { "roles": ["phone", "billing"] } ``` Answer: ```json { "member": { "userId": "01JB8R…", "name": "Priya Raman", "email": "priya@northwind.example", "avatarUrl": null, "timezone": "Asia/Singapore", "role": "member", "title": "Support lead", "deactivated": false, "pending": false, "company": null } } ``` ### GET /api/workspaces/{w}/members/{userId}/sessions Who: Admins. Where a member is signed in, for signing out a lost or stolen phone or computer: each session's device (`client`: `web`, `desktop`, `mobile` or `script`, with `browser` and `os`), versions (`appVersion` for the Freesbe apps, `browserVersion`, `osVersion`), sign-in method, IP address and rough location, when it started and was last used. Owners, admins, and members with the People or Helpdesk admin role (members and guests only). `signOut` says what you can do: `now`, `ask` (the owner: a second admin approves) or `no`. For the owner, `request` is the open request to sign them out, if any, and `approvers` counts the admins who could approve one. Answer: ```json { "sessions": [ { "id": "…", "client": "mobile", "os": "iOS", "appVersion": "1.0.0", "osVersion": "18.1", "method": "passkey", "createdAt": …, "lastActiveAt": …, "ip": "192.0.2.88", "location": "Lisbon, Portugal", … } ], "signOut": "now", "request": null, "approvers": 0 } ``` ### DELETE /api/workspaces/{w}/members/{userId}/sessions Who: Admins. Signs a member out everywhere: every session ends in every workspace they use, their phones stop getting notifications, and this workspace's calendar add-ins sign out. They can sign in again. Not yourself, and not an owner of this or any other workspace (see `sign-out-requests`). They get an email, their security activity shows it, and the audit log records it. Answers 204. ### DELETE /api/workspaces/{w}/members/{userId}/sessions/{id} Who: Admins. Signs one of a member's sessions out, like the one above. Answers 204. ### POST /api/workspaces/{w}/members/{userId}/sign-out-requests Who: Admins. Asks to sign the owner out of one session (`sessionId`) or every one (`sessionId: null`), with a `reason` the approver sees. No admin signs the owner out alone: another admin approves within 24 hours, and the other admins get an email. One open request at a time (409). Request: ```json { "sessionId": null, "reason": "Maya's phone was stolen" } ``` Answer: ```json { "request": { "id": "…", "userId": "…", "sessionId": null, "requestedBy": "…", "reason": "…", "createdAt": …, "expiresAt": … } } ``` ### POST /api/workspaces/{w}/sign-out-requests/{id} Who: Admins. Approves a request to sign the owner out, which happens at once. Neither the admin who asked nor the owner can approve it. Answers 204. ### DELETE /api/workspaces/{w}/sign-out-requests/{id} Who: Admins. Declines a request to sign the owner out, or withdraws your own. Answers 204. ### POST /api/workspaces/{w}/owner Who: Its owner. Hands the workspace to another admin or member who has signed in: they become the owner and you become an admin. Needs a second factor verified in the last 15 minutes (`mfa_required`). Answers 204. Request: ```json { "userId": "01JB8S…" } ``` ### POST /api/workspaces/{w}/leave Who: Members. Leaves the workspace. Your messages stay, you lose access, and a new invite brings you back. The owner hands the workspace over first (`/owner`). Members and admins can't leave while the workspace turned off `membersCanLeave`; guests always can. Answers 204. ### GET /api/workspaces/{w}/closure Who: Admins. Whether the workspace is being closed (`closure`: `stage` `closing` until `closesAt`, then `closed` and read-only until `purgeAt`), when the paid period ends, and the add-ons and phone system closing would end. Answer: ```json { "closure": null, "periodEnd": 1795322400000, "addOns": ["Phone system"], "phoneSystem": true } ``` ### POST /api/workspaces/{w}/closure Who: Its owner. Closes the workspace: the plan stops renewing and it goes read-only when the paid period ends (at once on Free), then everything in it is deleted 30 days later. `name` must be the workspace's name. Signed-in browser sessions only, with a second factor verified in the last 15 minutes (`mfa_required`). While closed, changes answer 403 `workspace_closed`; reading, exports, the billing portal and marking read still work. A workspace Freesbe's team holds answers the same way with 403 `workspace_on_hold` (waiting for documents) or `workspace_suspended`; under review only invites, phone numbers and customer inbox changes answer 403 `workspace_under_review` (see the account status guide). Request: ```json { "name": "Northwind Traders", "reason": "Moving to another tool" } ``` Answer: ```json { "closure": { "stage": "closing", "requestedAt": 1794112800000, "closesAt": 1795322400000, "closedAt": null, "purgeAt": null, "by": "01JB8R…" }, "periodEnd": 1795322400000, "addOns": [], "phoneSystem": false } ``` ### DELETE /api/workspaces/{w}/closure Who: Its owner. Reopens a closing or closed workspace until its deletion starts. In the paid period the plan renews again; once closed it's back on Free. Needs a second factor verified in the last 15 minutes. Answers 204. ### GET /api/workspaces/{w}/profiles Who: Members. App scope: `users:read`. Profile fields and everyone's values, by user id: department, location, manager (a user id), phone, pronouns and the workspace's own fields. Dates are YYYY-MM-DD. Guests get only fields not hidden from guests, for the people they share channels with. Answer: ```json { "fields": [ { "id": "01JC2…", "preset": "department", "label": null, "kind": "text", "options": [], "locked": true, "hiddenFromGuests": true } ], "profiles": { "01JB8R…": { "01JC2…": "Engineering" } } } ``` ### GET /api/workspaces/{w}/profile-fields Who: Members. App scope: `users:read`. The workspace's profile fields, in the order profiles show them. `preset` is `pronouns`, `department`, `location`, `manager`, `phone` or null for a field admins added; `kind` is `text`, `link`, `date`, `person`, `options` or `phone`. ### PUT /api/workspaces/{w}/profile-fields Who: Admins. Saves every field, in order: keep a field by its `id`, add one without an `id`, and leave one out to delete it with its values. Set `label`, `options` (for `options` fields), `locked` (only admins fill it in) and `hiddenFromGuests`. Adding your own fields and locking fields need Business, Corporate or Enterprise. Request: ```json { "fields": [ { "id": "01JC2…", "kind": "text", "locked": true }, { "kind": "options", "label": "Region", "options": ["EMEA", "Americas"] } ] } ``` ### GET /api/workspaces/{w}/teams Who: Members. App scope: `users:read`. The workspace's teams, by name: who's on each one (`memberIds`, active people only) and the channels that keep it in them that you can see (`channelIds`). Guests get only the teams they're on. Answer: ```json { "teams": [ { "id": "01JD4…", "name": "Sales", "description": "Account managers and sales leads", "memberIds": ["01JB8R…"], "channelIds": ["01JB9C…"], "createdAt": 1793952000000 } ] } ``` ### POST /api/workspaces/{w}/teams Who: Admins. Makes a team: a `name` no other team has, an optional `description`, and the people on it (`memberIds`, guests too). Admins and people admins. Teams need Business, Corporate or Enterprise. Request: ```json { "name": "Sales", "description": "Account managers and sales leads", "memberIds": ["01JB8R…", "01JB8S…"] } ``` Answer: ```json { "team": { "id": "01JD4…", "name": "Sales", "description": "Account managers and sales leads", "memberIds": ["01JB8R…", "01JB8S…"], "channelIds": ["01JB9C…"], "createdAt": 1793952000000 } } ``` ### PATCH /api/workspaces/{w}/teams/{id} Who: Admins. Changes a team's `name`, `description` or the people on it: `memberIds` replaces the list. People put on the team join every channel that keeps it; people taken off leave the channels they're in only because of this team (not ones they joined themselves, were added to by hand, or have through another team). Deactivated people stay on it. Request: ```json { "memberIds": ["01JB8R…", "01JB8S…", "01JB8T…"] } ``` Answer: ```json { "team": { "id": "01JD4…", "name": "Sales", "description": "Account managers and sales leads", "memberIds": ["01JB8R…", "01JB8S…"], "channelIds": ["01JB9C…"], "createdAt": 1793952000000 } } ``` ### DELETE /api/workspaces/{w}/teams/{id} Who: Admins. Deletes a team. Everyone stays in the channels it put them in; those channels stop keeping it. Answers 204. ### PUT /api/workspaces/{w}/members/{userId}/profile Who: Members. Sets someone's values by field id; null or an empty string clears one. Anyone can set their own fields that aren't locked; admins set anyone's. Answers with all of that person's values. Request: ```json { "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } } ``` Answer: ```json { "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } } ``` ### POST /api/workspaces/{w}/profiles/bulk Who: Admins. Sets values for up to 500 people at once, as `people: [{ userId, values }]`. Nothing is saved if any value doesn't fit its field. Request: ```json { "people": [ { "userId": "01JB8R…", "values": { "01JC2…": "Engineering" } } ] } ``` Answer: ```json { "updated": 1 } ``` ## Invites ### GET /api/workspaces/{w}/invites Who: Admins. Open invite links and emailed invites, guest invites included. ### POST /api/workspaces/{w}/invites Who: Admins. Create an invite link to share: `role` (`member` or `admin`), `expiresInDays` (default 7, at most 30), `maxUses` (null for unlimited). The `token` and `url` are only in this answer. Request: ```json { "role": "member", "expiresInDays": 7, "maxUses": 10 } ``` Answer: ```json { "invite": { "id": "01JBD2…", "token": "q7Zr…", "url": "https://app.freesbe.net/invite/q7Zr…", "role": "member", "channelIds": [], "createdBy": "01JB8Q…", "createdAt": 1761200000000, "expiresAt": 1761804800000, "maxUses": 10, "uses": 0, "sentTo": null, "needsApproval": false } } ``` ### POST /api/workspaces/{w}/invites/email Who: Admins. Email up to 50 people their own single-use invite, with an optional `role`. Says who was sent one, who's already in, and who couldn't be emailed or was held back by the email limits. Request: ```json { "emails": ["sam@acme.example", "priya@northwind.example"], "role": "member" } ``` Answer: ```json { "sent": ["sam@acme.example"], "skipped": ["priya@northwind.example"], "failed": [], "limited": [] } ``` ### GET /api/workspaces/{w}/invites/teammates?q={q} Who: Admins. Up to 8 people, by name, who share another workspace with you and aren't in this one or invited to it yet, matching `q` (part of a name or email address). Only full members of workspaces where you're a full member are listed, never guests. Each comes with the other workspaces you share. Apps can't call this. Answer: ```json { "teammates": [ { "userId": "01JB8R…", "name": "Priya Shah", "email": "priya@northwind.example", "avatarUrl": null, "workspaces": ["Northwind Labs"] } ] } ``` ### DELETE /api/workspaces/{w}/invites/{id} Who: Admins. Revoke an invite. Answers 204. ### GET /api/invites/{token} Who: No token. What an invite link shows: the workspace, who sent it, whether it still works, and for guest invites the channels and whether someone has to let the guest in. Answer: ```json { "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "logoUrl": null }, "invitedBy": "Dana Whitfield", "valid": true, "guest": false, "channels": [], "needsApproval": false } ``` ### POST /api/invites/{token}/accept Who: Signed in. Join the workspace with an invite. A guest invite that needs approval answers 202 `{ "workspace": null, "pending": true }` until someone in the channel lets you in. ## Email domains ### GET /api/workspaces/{w}/email-domains Who: Admins. The workspace's email domains, each with the TXT record that verifies it, and the people waiting for an admin to let them in. ### POST /api/workspaces/{w}/email-domains Who: Admins. Add a domain people at your company have addresses at. `role` is `member` or `admin`; `approval` makes an admin let each person in (always on for admins). Answers 201 with the domain and its TXT record. Request: ```json { "domain": "northwind.example", "role": "member", "approval": false } ``` ### POST /api/workspaces/{w}/email-domains/{id}/check Who: Admins. Look for the domain's TXT record now instead of waiting for the next check. ### GET /api/workspaces/{w}/email-domains/{id}/dns Who: Admins. Where the domain's DNS is managed and how to add its record there: the `zone`, its `nameServers`, the `provider` when Freesbe knows it (like `namecheap`) and its `providerName`, the record's `host` relative to the zone, and `oneClick`, a link to the provider's Domain Connect page that adds the record, when the provider supports it. Answer: ```json { "dns": { "zone": "northwind.example", "nameServers": ["dns1.registrar-servers.com", "dns2.registrar-servers.com"], "provider": "namecheap", "providerName": "Namecheap", "host": "_freesbe-verification", "oneClick": null } } ``` ### DELETE /api/workspaces/{w}/email-domains/{id} Who: Admins. Remove a domain. People who already joined stay. ## Guests Guests are people from outside the organization who join specific channels only. Anyone in the channel who isn't a guest can invite them. ### POST /api/channels/{c}/guest-invites Who: Channel members. An invite link that makes people guests in this channel. `approval` (default true) means someone in the channel lets each guest in. Also `expiresInDays` and `maxUses`. Not for DMs, and not for guests. Request: ```json { "approval": true, "expiresInDays": 14, "maxUses": null } ``` Answer: ```json { "invite": { "role": "guest", "channelIds": ["01JB8S…"], "needsApproval": true, "url": "https://app.freesbe.net/invite/…", … } } ``` ### POST /api/channels/{c}/guest-invites/email Who: Channel members. Email up to 50 people their own guest invite to the channel, with `approval`. Answers like the workspace email invites. Request: ```json { "emails": ["jordan@acme.example"], "approval": false } ``` ### GET /api/invites/{token}/request Who: Signed in. Where your request to join through a guest invite stands: `none`, `pending`, `approved` (with the `workspace`) or `declined`. ### POST /api/guest-requests/{id}/approve Who: Channel members. Let a waiting guest in. The id is the `requestId` of the `guest_request` system message posted in the channel. Answers 204, or 409 when someone already answered. ### POST /api/guest-requests/{id}/decline Who: Channel members. Turn a waiting guest away. Answers 204. ## Channels Channel names are lowercase letters, numbers, hyphens and underscores, up to 80 characters; Freesbe tidies what you send (`Q3 Launch!` becomes `q3-launch`). Private channels you're not in answer 404, as if they didn't exist. ### GET /api/workspaces/{w}/channels Who: Members. App scope: `channels:read`. Every public channel, plus your private channels and DMs, each with your unread state. Guests get only their own. Answer: ```json { "channels": [ { "id": "01JB8S…", "kind": "public", "name": "deploys", "unreadCount": 3, … } ] } ``` ### POST /api/workspaces/{w}/channels Who: Members, not guests. App scope: `channels:manage`. Create a channel: `name`, `kind` (`public` or `private`), an optional `topic` and the `memberIds` to add besides you. Answers 201, or 409 when the name is taken. Request: ```json { "name": "acme-rollout", "kind": "private", "topic": "Acme Corp onboarding", "memberIds": ["01JB8R…"] } ``` Answer: ```json { "channel": { "id": "01JB8S…", "workspaceId": "01JB8Q…", "kind": "public", "name": "deploys", "topic": "Release notes and rollbacks", "isGeneral": false, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "archivedAt": null, "lastMessageId": "01JBC4…", "lastMessageAt": 1761210000000, "memberCount": 14, "isMember": true, "lastReadId": "01JBC1…", "unread": true, "unreadCount": 3, "mentionCount": 0, "muted": false, "notify": "default", "sound": null } } ``` ### GET /api/channels/{c} Who: Channel members. App scope: `channels:read`. One channel, with your unread state and notification settings. Answer: ```json { "channel": { "id": "01JB8S…", "workspaceId": "01JB8Q…", "kind": "public", "name": "deploys", "topic": "Release notes and rollbacks", "isGeneral": false, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "archivedAt": null, "lastMessageId": "01JBC4…", "lastMessageAt": 1761210000000, "memberCount": 14, "isMember": true, "lastReadId": "01JBC1…", "unread": true, "unreadCount": 3, "mentionCount": 0, "muted": false, "notify": "default", "sound": null } } ``` ### PATCH /api/channels/{c} Who: Channel members. App scope: `channels:manage`. Change the `topic` (channel members, up to 250 characters) or the `name` (its creator or admins). Posts a note in the channel. Request: ```json { "topic": "Release notes, rollbacks and on-call" } ``` ### POST /api/channels/{c}/join Who: Channel members. App scope: `channels:join`. Join a public channel. ### POST /api/channels/{c}/leave Who: Channel members. App scope: `channels:join`. Leave a channel. Not DMs, and not the general channel. Answers 204. ### POST /api/channels/{c}/archive Who: Creator or admins. App scope: `channels:manage`. Archive a channel: it becomes read-only. Not the general channel. ### POST /api/channels/{c}/unarchive Who: Creator or admins. App scope: `channels:manage`. Reopen an archived channel. ### PUT /api/channels/{c}/auto-join Who: Creator or admins. App scope: `channels:manage`. Keep the whole workspace in the channel: everyone who isn't in it yet joins now, and people who join the workspace later are added on their own. `"members"` leaves guests out, `"everyone"` adds them too, `null` stops adding people (nobody is removed). The channel then carries `autoJoin`. Not the general channel, which already has everyone. People can still leave. Posts one note in the channel. Request: ```json { "autoJoin": "members" } ``` Answer: ```json { "channel": { "id": "01JB8S…", "workspaceId": "01JB8Q…", "kind": "public", "name": "deploys", "topic": "Release notes and rollbacks", "isGeneral": false, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "archivedAt": null, "lastMessageId": "01JBC4…", "lastMessageAt": 1761210000000, "memberCount": 14, "isMember": true, "lastReadId": "01JBC1…", "unread": true, "unreadCount": 3, "mentionCount": 0, "muted": false, "notify": "default", "sound": null } } ``` ### PUT /api/channels/{c}/teams/{teamId} Who: Creator or admins. App scope: `channels:manage`. Keeps a team in the channel: everyone on it who isn't in the channel yet joins now, and people put on the team later join too. The channel then lists it in `teamIds`. Not the general channel. People can still leave. Posts one note in the channel. Answer: ```json { "channel": { "id": "01JB8S…", "workspaceId": "01JB8Q…", "kind": "public", "name": "deploys", "topic": "Release notes and rollbacks", "isGeneral": false, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "archivedAt": null, "lastMessageId": "01JBC4…", "lastMessageAt": 1761210000000, "memberCount": 14, "isMember": true, "lastReadId": "01JBC1…", "unread": true, "unreadCount": 3, "mentionCount": 0, "muted": false, "notify": "default", "sound": null } } ``` ### DELETE /api/channels/{c}/teams/{teamId} Who: Creator or admins. App scope: `channels:manage`. Stops keeping the team in the channel. Nobody is removed; people it put there stay as if someone had added them. Answer: ```json { "channel": { "id": "01JB8S…", "workspaceId": "01JB8Q…", "kind": "public", "name": "deploys", "topic": "Release notes and rollbacks", "isGeneral": false, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "archivedAt": null, "lastMessageId": "01JBC4…", "lastMessageAt": 1761210000000, "memberCount": 14, "isMember": true, "lastReadId": "01JBC1…", "unread": true, "unreadCount": 3, "mentionCount": 0, "muted": false, "notify": "default", "sound": null } } ``` ### GET /api/channels/{c}/members Who: Channel members. App scope: `channels:read`. The user ids of everyone in the channel. Answer: ```json { "userIds": ["01JB8Q…", "01JB8R…"] } ``` ### POST /api/channels/{c}/members Who: Channel members. App scope: `channels:manage`. Add people (`userIds`, up to 500) to a channel you're in. Not DMs; guests can't add people. Request: ```json { "userIds": ["01JB8T…"] } ``` ### DELETE /api/channels/{c}/members/{userId} Who: Creator or admins. App scope: `channels:manage`. Remove someone from the channel. Answers 204. ### PUT /api/channels/{c}/prefs Who: Channel members. Your own settings for the channel: `muted`, `notify` (`default`, `all`, `mentions` or `none`) and the notification `sound` (null for your default). Request: ```json { "muted": false, "notify": "mentions", "sound": "marimba" } ``` ### PUT /api/channels/{c}/section Who: Channel members. Put the conversation in one of your sidebar sections, or back under Channels or Direct messages with null. It comes back as `sectionId` on the channel. Answers 204. Apps can't use this. Request: ```json { "sectionId": "01JC2…" } ``` ### POST /api/channels/{c}/read Who: Channel members. Mark the channel read up to `lastReadId`, the newest message you've seen. It only moves forward, and your other devices clear the same unread state. Answers 204. Request: ```json { "lastReadId": "01JBC4…" } ``` ### POST /api/channels/{c}/unread Who: Channel members. Mark the channel unread from `messageId` on (a message in the channel, not a thread reply): the read cursor moves back to just before it. Answers with the channel and its new `unreadCount` and `mentionCount`, and your other devices get a `read.updated` event with `counts`. Read receipts don't move back. Apps can't use this. Request: ```json { "messageId": "01JBC4…" } ``` Answer: ```json { "channel": { "id": "01JBC2…", "lastReadId": "01JBC3…", "unread": true, "unreadCount": 3, "mentionCount": 1, … } } ``` ### GET /api/channels/{c}/receipts Who: Channel members. Read receipts: how far the others in the conversation have read. `mode` is `names`, `count` (a big channel: show how many, not who) or `off` (the workspace doesn't show receipts here, you turned yours off, or you aren't a member), with an empty `readers` list. A message was seen by everyone whose `lastReadId` is the same or newer. People who turned their receipts off aren't listed. Changes arrive as `receipt.updated` events. Answer: ```json { "mode": "names", "readers": [ { "userId": "01JB8R…", "lastReadId": "01JBC4…" }, { "userId": "01JB8S…", "lastReadId": null } ] } ``` ## Direct messages A DM is a channel with `kind: "dm"` and no name. Read and post in it with the channel and message endpoints. ### POST /api/workspaces/{w}/dms Who: Members. App scope: `im:write`. Find or open the DM with exactly these people (`userIds`, up to 8 besides you). An empty list opens your own notes. Guests can only message people they share a channel with. Request: ```json { "userIds": ["01JB8R…", "01JB8T…"] } ``` Answer: ```json { "channel": { "id": "01JBD9…", "kind": "dm", "name": null, "memberIds": ["01JB8Q…", "01JB8R…", "01JB8T…"], … } } ``` ## Messages and threads Text is Slack-style mrkdwn, up to 40,000 characters. Mention people with `<@userId>` and the whole channel with ``. Archived channels are read-only. ### GET /api/channels/{c}/messages Who: Channel members. App scope: `channels:history`. A page of top-level messages, oldest first: the newest 50 by default. Page with `?before=`, `?after=` or `?from=`, jump with `?around=`, and set `?limit=` from 1 to 100. `hasMore` says there are more in that direction. On a plan with limited history (Free: 90 days), older messages are left out, here and everywhere else (search, threads, single messages answer 404): the page that reaches them has `historyHidden` (`days`, and `before`, the time messages before it are hidden) and `hasMore` false. Answer: ```json { "messages": [ { "id": "01JBC4…", "body": "Release *2.4* is out :tada:", … } ], "hasMore": true } ``` ### POST /api/channels/{c}/messages Who: Channel members. App scope: `chat:write`. Post a message, or a thread reply with `threadRootId`. Attach up to 10 uploaded `fileIds`. Sending the same `clientMsgId` again returns the first message instead of posting twice. In a [customer inbox](#ref-inbox) conversation's thread the reply goes to the customer, unless `note` is true. You have to be in the channel. Apps can also send a card as Slack `blocks` or `attachments`, with `body` as its notification text. A message a person sent from the phone app or a browser on a phone or tablet comes back with `sentFrom: "mobile"`, here and wherever messages are listed; it's absent otherwise. Answers 201. Request: ```json { "body": "Rollback done, <@01JB8R…> can you confirm?", "threadRootId": "01JBC4…", "clientMsgId": "deploy-8812", "fileIds": ["01JBC9…"] } ``` Answer: ```json { "message": { "id": "01JBC4…", "channelId": "01JB8S…", "kind": "user", "userId": "01JB8Q…", "botName": null, "botIcon": null, "body": "Release *2.4* is out :tada:", "attachments": [], "files": [], "system": null, "threadRootId": null, "replyCount": 0, "lastReplyAt": null, "replyUserIds": [], "reactions": [{ "emoji": "🎉", "userIds": ["01JB8R…"] }], "clientMsgId": "deploy-8812", "createdAt": 1761210000000, "editedAt": null, "deletedAt": null } } ``` ### GET /api/messages/{m} Who: Channel members. App scope: `channels:history`. One message, with its reactions, files and thread counts. Answer: ```json { "message": { "id": "01JBC4…", "channelId": "01JB8S…", "kind": "user", "userId": "01JB8Q…", "botName": null, "botIcon": null, "body": "Release *2.4* is out :tada:", "attachments": [], "files": [], "system": null, "threadRootId": null, "replyCount": 0, "lastReplyAt": null, "replyUserIds": [], "reactions": [{ "emoji": "🎉", "userIds": ["01JB8R…"] }], "clientMsgId": "deploy-8812", "createdAt": 1761210000000, "editedAt": null, "deletedAt": null } } ``` ### PATCH /api/messages/{m} Who: Channel members. App scope: `chat:write`. Edit your own message's `body`, within the workspace's `editWindowMin`. Sets `editedAt`. Request: ```json { "body": "Release *2.4.1* is out :tada:" } ``` ### DELETE /api/messages/{m} Who: Channel members. App scope: `chat:write`. Delete your own message (unless the workspace turned off `membersCanDelete`; owners and admins always can), or, as an owner or admin, anyone's message in a channel you can see (not in DMs, and not when `adminsCanDelete` is off). A thread's first message stays as a placeholder while it has replies, and a message an admin deleted stays as a placeholder with `deletedByAdmin: true`; admin deletions are logged. Answers 204. ### DELETE /api/messages/{m}/files/{f} Who: Channel members. App scope: `chat:write`. Delete one file from a message, for whoever may delete the message (the same rules as `DELETE /api/messages/{m}`). The rest of the message stays and everyone gets `message.updated`; a message left with no text, files, attachments or blocks is deleted as `DELETE /api/messages/{m}` would. An owner or admin deleting a file from someone else's message is logged in `GET /api/workspaces/{w}/message-deletions`. Answers 204. ### GET /api/messages/{m}/replies Who: Channel members. App scope: `channels:history`. A thread: its first message and up to 1,000 replies, oldest first, and `lastReadId`, how far you've read in it (replies after it are new; null if you never opened it). Pass `?channel=` with the thread's channel id when you know it: the answer comes back faster, and a thread that isn't in that channel is a 404. Answer: ```json { "root": { "id": "01JBC4…", "replyCount": 2, … }, "replies": [ { … }, { … } ], "lastReadId": "01JBC5…" } ``` ### POST /api/messages/{m}/thread-read Who: Channel members. Mark the thread that `m` starts read up to `lastReadId`, its newest reply you've seen (or `m` itself). It only moves forward, your other devices get a `thread.read` event, and your phones take the thread's notification away. Answers 204. Apps can't use this. Request: ```json { "lastReadId": "01JBC5…" } ``` ### POST /api/messages/{m}/thread-unread Who: Channel members. Mark the thread that `m` starts unread from `messageId` on (one of its replies, or `m` itself for all of them): those replies count as new again until you open the thread. Your other devices get a `thread.read` event with `unread`. Apps can't use this. Request: ```json { "messageId": "01JBC5…" } ``` Answer: ```json { "rootId": "01JBC4…", "lastReadId": "01JBC4…" } ``` ### POST /api/messages/{m}/forward Who: Channel members. App scope: `chat:write`. Forward a message you can read to a conversation you're in (`channelId`), with your own `comment` above it. The copy carries the original as an attachment with `forwarded` (`messageId`, `channelId`, `threadRootId`, `userId`, `channelName`, `createdAt`, `fileNames`); files stay in the original conversation. Forwarding a forward with no comment passes the original on. Request: ```json { "channelId": "01JB8T…", "comment": "Can we do this for Acme Corp too?" } ``` Answer: ```json { "message": { "id": "01JBD7…", "body": "Can we do this for Acme Corp too?", "attachments": [ { "authorName": "Lena Fischer", "text": "Northwind wants the blue tins", "footer": "#sales", "forwarded": { "messageId": "01JBA2…", "channelId": "01JB8S…", … } } ], … } } ``` ### POST /api/messages/{m}/notify-anyway Who: Channel members. Notify anyway: alert the other person in a 1:1 DM about your own message even though they paused notifications. Works for a day after sending and once a message; asking again does nothing. Answers 204. Not open to apps. ## Reactions ### PUT /api/messages/{m}/reactions/{emoji} Who: Channel members. App scope: `reactions:write`. React with one emoji: the character itself, URL-encoded (`%F0%9F%8E%89` for 🎉). Doing it twice is harmless. Answers 204, or 409 when the message has all the different reactions it can take. Request: ```json curl -X PUT "$API/api/messages/$MESSAGE/reactions/%F0%9F%8E%89" \ -H "Authorization: Bearer $FREESBE_TOKEN" ``` ### DELETE /api/messages/{m}/reactions/{emoji} Who: Channel members. App scope: `reactions:write`. Remove your reaction. Answers 204. ## Keyword alerts Words and phrases that alert you like a mention: a message in one of your channels with one of them notifies you, counts toward the mention badge and shows in Activity. They match whole words, ignoring case. Up to 50 per workspace, 40 characters each. Changes reach your other devices as a `keywords.changed` event. Apps can't use these. ### GET /api/workspaces/{w}/keywords Who: Members. Your keywords. Answer: ```json { "keywords": ["Northwind", "price list"] } ``` ### PUT /api/workspaces/{w}/keywords Who: Members. Replace them. Blanks and repeats are dropped. Answers the saved list. Request: ```json { "keywords": ["Northwind", "price list", "launch"] } ``` ## Link previews Freesbe adds a preview for up to three web links in each message people send: an attachment with `fromUrl` (the link), `serviceName` (the site), `title`, `titleLink`, `text` (the page's description) and a picture in `thumbUrl`, or `imageUrl` for a link to a picture. Pictures load through Freesbe, so the site doesn't see who's reading. Previews arrive a moment after the message, as a `message.updated` event. Links in code, to private addresses, and those an integration previews get none. ### DELETE /api/messages/{m}/link-preview?url={url} Who: Channel members. Remove the preview of one link from your own message, for good: editing the message doesn't bring it back. Answers 204. ### GET /api/link-previews/{id}/image Who: Members. A preview's picture, fetched by Freesbe. `thumbUrl` and `imageUrl` point here. ## Custom emoji A workspace's own pictures, written `:name:` in messages and used as reactions (`PUT /api/messages/{m}/reactions/%3Anorthwind%3A`). Names are 2 to 32 lowercase letters, numbers, `-`, `+` or `_`, and can't be a built-in emoji's name. A workspace has up to 500. Changes reach everyone as an `emoji.changed` event. ### GET /api/workspaces/{w}/emoji Who: Members. App scope: `team:read`. Every custom emoji, by name. The `url` loads the picture without signing in. Answer: ```json { "emoji": [ { "name": "northwind", "url": "/api/custom-emoji/01JB…/01JC…", "createdBy": "01JB8R…", "createdAt": 1767225600000 } ] } ``` ### POST /api/workspaces/{w}/emoji?name={name} Who: Members. Add one: raw PNG, JPEG, GIF or WebP bytes with their `Content-Type`, up to 256 KB. Guests can't. Answers 201 with `{ "emoji": { … } }`, or 409 when the name is taken. Apps can't add emoji. Request: ```json curl -X POST "$API/api/workspaces/$WORKSPACE/emoji?name=northwind" \ -H "Authorization: Bearer $FREESBE_TOKEN" -H "Content-Type: image/png" --data-binary @northwind.png ``` ### DELETE /api/workspaces/{w}/emoji/{name} Who: Members. Remove one you added; owners and admins can remove any. Reactions that used it stay and show its name. Answers 204. ### GET /api/custom-emoji/{w}/{id} Who: No token. The picture. Its id can't be guessed, and the picture behind it never changes. ## Sidebar sections Your own groups of channels and DMs in the sidebar. Only you see them; changes reach your other devices as `sidebar.sections` (the whole list) and `channel.section` events. Up to 50, with names of up to 40 characters. Apps can't use these. ### GET /api/workspaces/{w}/sidebar-sections Who: Members. Your sections, in sidebar order. Answer: ```json { "sections": [ { "id": "01JC2…", "name": "Customers", "emoji": "🤝" } ] } ``` ### POST /api/workspaces/{w}/sidebar-sections Who: Members. Add a section at the end. Answers 201 with `{ "section": { … } }`. Request: ```json { "name": "Customers", "emoji": "🤝" } ``` ### PATCH /api/workspaces/{w}/sidebar-sections/{id} Who: Members. Rename it, or change its emoji (null for none). Request: ```json { "name": "Clients" } ``` ### PUT /api/workspaces/{w}/sidebar-sections/{id}/position Who: Members. Move it, counted from 0. Answers the whole list. Request: ```json { "position": 0 } ``` ### DELETE /api/workspaces/{w}/sidebar-sections/{id} Who: Members. Delete it. Its conversations go back under Channels and Direct messages. Answers 204. ## Notification sounds The sounds are `freesbe`, `ding`, `pop`, `knock`, `bell`, `bubble`, `chord`, `marimba` and `none` (notify without a sound). Set one per conversation with the channel's `prefs`, or per person here; your other devices follow. ### GET /api/workspaces/{w}/sounds Who: Members. The sounds you picked for messages from particular people, by user id. Answer: ```json { "sounds": { "01JB8R…": "bell" } } ``` ### PUT /api/workspaces/{w}/sounds/{userId} Who: Members. Pick the sound for messages from one person, or null for your default. Answers the whole list. Request: ```json { "sound": "bell" } ``` ## Files Upload first, then attach the file to a message within a day; files that are never attached are deleted. Until then only you can download it. ### POST /api/workspaces/{w}/files?name= Who: Members. App scope: `files:write`. Upload the raw bytes with their `Content-Type` and a `Content-Length`, up to 50 MB. Add `&width=&height=` for images when you know them. Answers 201, 403 `file_blocked` when the workspace's file type rules turn the file away (checked by name and by contents), or 413 when the workspace is out of storage. Request: ```json curl -X POST "$API/api/workspaces/$WORKSPACE/files?name=q3-report.pdf" \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: application/pdf' \ --data-binary @q3-report.pdf ``` Answer: ```json { "file": { "id": "01JBC9…", "name": "q3-report.pdf", "mime": "application/pdf", "size": 482113, "url": "/api/files/01JBC9…" } } ``` ### GET /api/files/{id} Who: Channel members. App scope: `files:read`. Download a file from a channel you can see. Images, audio, video and plain text open inline; add `?download=1` to always download. Supports `Range`. ### GET /api/files/{id}/preview Who: Channel members. App scope: `files:read`. The picture of a PDF's first page, or a photo's smaller copy (at most 2048 pixels on its longest side), when the file has one: its `previewUrl`. Same access as the file. Apps need `files:read`. ### PUT /api/files/{id}/preview Who: Members. App scope: `files:write`. Add a picture of a PDF's first page to a PDF you uploaded, before you send it: the raw PNG, JPEG or WebP bytes (at most 1 MB) with its `Content-Type`, and `?width=&height=` of the page so messages keep their shape. Freesbe doesn't draw PDFs itself; the apps do this as they upload one. For a photo (JPEG, PNG, WebP or HEIC, not GIF), send a smaller copy instead, at most 2048 pixels on its longest side and 2 MB: conversations show and copy it, and the file itself stays the original for downloads. A photo keeps its own `width` and `height`. Returns the file with its `previewUrl`. Apps need `files:write`. Answer: ```json { "file": { "id": "01JBC9…", "name": "quote.pdf", "mime": "application/pdf", "size": 48211, "width": 595, "height": 842, "url": "/api/files/01JBC9…", "previewUrl": "/api/files/01JBC9…/preview" } } ``` ### GET /api/channels/{c}/files Who: Channel members. App scope: `files:read`. The files shared in a channel or DM you can see, threads included, newest first: what its Files tab shows. Filter with `kind` (`images`, `media` for video and audio, `documents`, `other`) and `q` (part of the name). Up to `limit` (default 50, at most 100) at a time; pass `next` as `before` for older ones. Files of deleted messages aren't listed. Apps need `files:read`. Answer: ```json { "files": [{ "id": "01JBC9…", "name": "spring-catalog.pdf", "mime": "application/pdf", "size": 482113, "url": "/api/files/01JBC9…", "messageId": "01JBC8…", "threadRootId": null, "uploaderId": "01JB8Q…", "createdAt": 1761238800000 }], "next": null } ``` ## Files (the file cabinet) Files people keep outside conversations, in My files or the workspace's folders, shared with people and teams. Access is `view` (open and download), `edit` (also upload, add versions, rename and move) or `full` (also delete, restore, share and change who has access). The owner of My files, admins in the workspace's folders, and whoever uploaded a file (while they can open its folder) have `full`; making a top-level workspace folder adds a `full` share for its maker. A top-level workspace folder's `everyone` (`none`, `view` or `edit`) is what every member gets in it. Members only: guests get 404. Download a file at its `file.url`, the same as any other file. ### GET /api/workspaces/{w}/cabinet Who: Members, not guests. A list from Files: `view=mine` (My files), `shared` (Shared with me), `workspace`, `recent` or `trash`; or a folder's contents with `folder=`; or folders and files whose names have `q` in them, wherever you can open them. Each entry has its `access` (`view`, `edit` or `full`), and a folder list says whether you can add to it (`canAdd`). Answer: ```json { "folder": null, "path": [], "canAdd": true, "folders": [{ "kind": "folder", "id": "01JC1F…", "name": "Taxes 2025", "place": "mine", "ownerId": "01JB8Q…", "parentId": null, "access": "full", "shared": true, "updatedAt": 1762300000000 }], "items": [{ "kind": "item", "id": "01JC1G…", "name": "notes.txt", "place": "mine", "access": "full", "versions": 1, "file": { "id": "01JC1H…", "name": "notes.txt", "mime": "text/plain", "size": 120, "url": "/api/files/01JC1H…" } }] } ``` ### POST /api/workspaces/{w}/cabinet/folders Who: Members, not guests. Make a folder: `name`, and `parentId` for a folder inside another, or `place` (`mine` or `workspace`) for one at the top. A top-level workspace folder takes `everyone` too (`edit` when left out). Needs edit on the folder it goes in. ### POST /api/workspaces/{w}/cabinet/items Who: Members, not guests. Upload a file to Files: the raw bytes with `Content-Type` and `Content-Length`, and `?name=`, plus `folder=` (or `place=` for the top of My files or the workspace's). Up to 50 MB, checked against the workspace's file type rules for members. ### POST /api/workspaces/{w}/cabinet/save Who: Members, not guests. Keep a copy of a file from a conversation you're in: `fileId`, and `folderId` (leave it out for the top of My files). ### PATCH /api/cabinet/folders/{id} Who: Members, not guests. Rename a folder (`name`) or move it (`parentId`, null for the top), with edit. Folders move within the same place: My files or the workspace's; moving to another top-level workspace folder needs full. `everyone` (`none`, `view` or `edit`) sets what every member gets in a top-level workspace folder, with full. ### DELETE /api/cabinet/folders/{id} Who: Members, not guests. Move a folder, with everything in it, to the trash for 30 days. From the trash, `?forever=1` deletes it now. Needs full. ### PATCH /api/cabinet/items/{id} Who: Members, not guests. Rename a file (`name`) or move it to another folder (`parentId`), with edit. ### DELETE /api/cabinet/items/{id} Who: Members, not guests. Move a file to the trash for 30 days; `?forever=1` from the trash deletes it and every version now. Needs full. ### POST /api/cabinet/{kind}/{id}/restore Who: Members, not guests. Take a folder (`folders`) or file (`items`) out of the trash, back where it was, or to the top when that folder is gone too. Needs full. ### GET /api/cabinet/items/{id}/versions Who: Members, not guests. A file's versions, newest first, with the current one marked. Up to 20 are kept. ### POST /api/cabinet/items/{id}/versions Who: Members, not guests. Upload a new version of a file, the same way as a new file. The name in Files stays. ### POST /api/cabinet/items/{id}/versions/{file}/restore Who: Members, not guests. Make an earlier version the current one again. ### POST /api/cabinet/items/{id}/attach Who: Members, not guests. A copy of a file to send in a message: returns the file like an upload does, for `fileIds`. ### GET /api/cabinet/{kind}/{id}/shares Who: Members, not guests. Who has access to a folder or file: shares on it and on folders above it (`inheritedFrom`), `everyone` for the workspace's (`everyoneFrom` when it's set on a folder above), and `canShare`. ### POST /api/cabinet/{kind}/{id}/shares Who: Members, not guests. Share it with a person (`userId`) or a team (`teamId`) to `view`, `edit` or `full`. Needs full. Guests can't be added. ### PATCH /api/cabinet/shares/{id} Who: Members, not guests. Change a share's `access`. Needs full. ### DELETE /api/cabinet/shares/{id} Who: Members, not guests. Stop sharing with that person or team. Needs full. ### GET /api/cabinet/{kind}/{id}/links Who: Members, not guests. Secure send: the links sending this folder or file to people outside the workspace, and this month's `usage` (`used`, `included`, `metered`, `resets`). Needs full. ### POST /api/cabinet/{kind}/{id}/links Who: Members, not guests. Secure send: send a folder or file to people outside the workspace, one link each, emailed to them. They open it with a code we email to that address. `days` (up to the workspace's longest), `allowDownload` (false to only view in the browser) and `downloadLimit` are optional. Each address counts as one share. Needs full and the Secure send add-on. Returns each link's `url`, which is shown only now. Request: ```json { "emails": ["dana@contoso.example"], "note": "Your 2025 return, ready to sign.", "days": 14, "allowDownload": true } ``` ### GET /api/workspaces/{w}/cabinet/links Who: Members, not guests. The Secure send links you sent, newest first, with what each opens, when it was first and last opened, and its downloads. Admins add `?all=1` for everyone's. ### GET /api/cabinet/links/{id}/events Who: Members, not guests. A link's activity: when it was sent, codes asked for, wrong codes, opens, and each file viewed or downloaded, with the country and device. For whoever sent it, admins, and people with full access. ### DELETE /api/cabinet/links/{id} Who: Members, not guests. Stop a Secure send link working, at once, in every browser it's open in. ### GET /api/workspaces/{w}/share-policy Who: Admins. The Secure send policy: who may send files outside (`everyone`, `admins` or `nobody`), the longest a link lasts (`maxDays`), and `blockedDomains`. ### PUT /api/workspaces/{w}/share-policy Who: Admins. Change the Secure send policy. `nobody` also stops every link already sent; a shorter `maxDays` applies to new links. Request: ```json { "who": "everyone", "maxDays": 30, "blockedDomains": ["competitor.example"] } ``` ## Status, availability and search Who is online (`presence`) comes with `/boot` and then live over the WebSocket; there's no endpoint to set it, because it follows the apps people have open. ### PUT /api/workspaces/{w}/status Who: Members. App scope: `users:write`. Set your status in a workspace. Fields you leave out stay as they are: `text` (up to 100 characters), `emoji`, `expiresAt` (when the text and emoji clear), `dndUntil` (Do Not Disturb until then; null turns it off) and `manualAway`. Request: ```json { "text": "At the Acme Corp warehouse", "emoji": "🚚", "expiresAt": 1761238800000, "dndUntil": null } ``` Answer: ```json { "status": { "userId": "01JB8Q…", "text": "At the Acme Corp warehouse", "emoji": "🚚", "expiresAt": 1761238800000, "dndUntil": null, "manualAway": false } } ``` ### GET /api/workspaces/{w}/search?q= Who: Members. App scope: `search:read`. Search the messages you can see for 2 to 200 characters, newest first, optionally in one channel with `&channelId=`. `q` can hold filters: `from:@name` (or `from:me`), `in:#channel`, `after:yyyy-mm-dd`, `before:yyyy-mm-dd`, `on:yyyy-mm-dd` and `has:file`; after and before leave out the day itself. Answers the matching `results`, the `channels` they're in, and the `filters` as understood, with any names that matched nobody in `unknown`. Answer: ```json { "results": [ { "id": "01JBC4…", "channelId": "01JB8S…", "body": "Release *2.4* is out", … } ], "channels": [ { "id": "01JB8S…", "name": "deploys", … } ], "filters": { "from": [ { "id": "01JB8Q…", "name": "Maya Chen" } ], "in": [], "after": null, "before": null, "hasFile": false, "unknown": [] } } ``` ### POST /api/workspaces/{w}/ask Who: Members. Ask Freesbe: an answer to a `question` (2 to 200 characters, filters allowed) written by AI from the messages you can see, optionally only in `channelId`, in `language` when the question doesn't make it clear. Each of the `sentences` lists the `sources` it comes from, numbered from 1 in the order of `sources`. `status` is `unknown` when the messages found don't answer it. `search` is the question as a search with the filters it understood typed out. Counts as one AI request; 403 when admins turned AI off or the month's requests are used up. Not for guests. With `Accept: text/event-stream` it streams server-sent events: `{ "type": "text", "text" }` pieces of the answer as it's written (one sentence a line, each ending with its source numbers like `[1][3]`), then `{ "type": "done", … }` with the whole answer as above, or `{ "type": "error", "message" }`. The answer is written within 30 seconds or fails with a "took too long" message. Request: ```json { "question": "When is the Northwind launch?", "language": "en" } ``` Answer: ```json { "status": "answered", "sentences": [ { "text": "The launch is on April 14.", "sources": [1] } ], "sources": [ { "id": "01JBC4…", "channelId": "01JB8S…", "body": "Yes, *April 14* still works.", … } ], "channels": [ { "id": "01JB8S…", "name": "spring-launch", … } ], "filters": { "from": [], "in": [], "after": null, "before": null, "hasFile": false, "unknown": [] }, "search": "When is the Northwind launch?" } ``` ### GET /api/workspaces/{w}/activity?before= Who: Members. Mentions of you (including `@channel` and `@here`), huddles in your DMs that you missed (system messages of type `huddle_missed`) and replies in threads you started or replied in, newest first, 50 at a time, with the `channels` they're in and `threadReads`: how far you've read in each of those threads that you've opened, by its first message's id (later replies are new). Page back with `before`, the oldest message id you have. Answer: ```json { "items": [ { "id": "01JBC9…", "channelId": "01JB8S…", "body": "<@01JB8Q…> can you approve this?", … } ], "channels": [ { "id": "01JB8S…", "name": "support", … } ], "threadReads": { "01JBC4…": "01JBC5…" } } ``` ### GET /api/workspaces/{w}/sent?before= Who: Members. Your own messages and replies, newest first, 50 at a time, in conversations you're still in, with the `channels` they're in. Page back with `before`, the oldest message id you have. The app's Drafts & sent page shows these under Sent. Answer: ```json { "items": [ { "id": "01JBD2…", "channelId": "01JB8S…", "body": "Shipped the Acme Corp order", … } ], "channels": [ { "id": "01JB8S…", "name": "sales", … } ] } ``` ### GET /api/workspaces/{w}/calls?before= Who: Members. Your calls: huddles in your DMs that have ended, newest first, 50 at a time. `direction` is `out` when you called; `answered` says whether you joined (incoming) or someone else did (outgoing). Page back with `before`, the oldest call's `startedAt`. Activity → Calls in the app lists these with your phone system calls. Not for apps. Answer: ```json { "calls": [ { "huddleId": "01JBE1…", "channelId": "01JB9D…", "callerId": "01JB8Q…", "direction": "in", "answered": false, "startedAt": 1767016320000, "durationMs": 30000, "participantIds": ["01JB8Q…"] } ], "more": false } ``` ## To-dos Each person's own to-do list in a workspace. Nobody else, admins included, can see or change it. `dueAt` is milliseconds (the apps use the end of the chosen day); `doneAt` is set when it's ticked off. ### GET /api/workspaces/{w}/todos Who: Members. App scope: `todos:read`. Your open to-dos, the soonest due first and undated ones after, then the 50 you finished most recently. Answer: ```json { "todos": [ { "id": "01JBF2…", "text": "Send Globex the revised freight quote", "notes": "Use the *new lane rates* from Lena's sheet.", "dueAt": 1761325199999, "doneAt": null, "messageId": null, "channelId": null, "createdAt": 1761238800000 } ] } ``` ### POST /api/workspaces/{w}/todos Who: Members. App scope: `todos:write`. Add a to-do: `text` (the one-line title) up to 500 characters, optional `notes` up to 10,000 characters in message formatting (mrkdwn, line breaks kept), optional `dueAt`, and optional `messageId` to make it from a message you can see. With a `messageId`, `text` can be empty and the message's text is used; a long or multi-line message also goes whole into `notes`. Up to 500 open to-dos each. Request: ```json { "text": "Approve spring catalog prices", "notes": "Check the *Acme Corp* discount first.", "dueAt": 1761325199999 } ``` Answer: ```json { "todo": { "id": "01JBF3…", "text": "Approve spring catalog prices", "dueAt": 1761325199999, "doneAt": null, … } } ``` ### POST /api/workspaces/{w}/todos/ai Who: Members. AI help for a to-do in the editor, counted like other AI requests (not for guests, and only while the workspace has AI and allowance left). `action` is `write` (notes from the title), `plan` (numbered steps, plus a `dueDate` when the to-do says when it's needed; pass `today` as yyyy-mm-dd) or `summarize` (what the message a to-do was made from and its thread say; needs `todoId`). Send `title` and the current `notes`. Nothing is saved: put the answer in the to-do with PATCH. Request: ```json { "action": "plan", "title": "Ask Tom about a second forklift", "notes": "Before the budget meeting Thursday", "today": "2026-10-23" } ``` Answer: ```json { "notes": "1. Count the hours both shifts wait for the forklift\n2. …", "dueDate": "2026-10-29" } ``` ### PATCH /api/workspaces/{w}/todos/{id} Who: Its owner. App scope: `todos:write`. Change `text`, `notes` or `dueAt` (null clears either), or tick it off with `done: true` (`false` reopens it). Request: ```json { "done": true } ``` Answer: ```json { "todo": { "id": "01JBF3…", "doneAt": 1761242400000, … } } ``` ### DELETE /api/workspaces/{w}/todos/{id} Who: Its owner. App scope: `todos:write`. Delete one of your to-dos. Answers 204. ## Scheduled messages Messages you write now to go out later. Only their author can see or change them until they're sent. `sendAt` is milliseconds, at least a minute ahead and at most 120 days. At its time the message is posted as if you'd just sent it, with the usual notifications and outgoing webhooks. One that can't be sent (you left the channel, it was archived) stays listed with `status: "failed"` and the reason in `error`. ### GET /api/workspaces/{w}/scheduled-messages Who: Members. App scope: `chat:write`. Your scheduled messages, soonest first, including ones that failed. Answer: ```json { "scheduled": [ { "id": "01JBG1…", "channelId": "01JB8S…", "threadRootId": null, "body": "Weekly sync is at 10:00 today", "files": [], "sendAt": 1761440400000, "status": "scheduled", "error": null, "createdAt": 1761238800000 } ] } ``` ### POST /api/workspaces/{w}/scheduled-messages Who: Members. App scope: `chat:write`. Schedule a message in a channel, DM or thread (`threadRootId`) you can post in. `body` is written like a message's; `fileIds` are uploads that aren't attached yet. Up to 100 each. Request: ```json { "channelId": "01JB8S…", "body": "Weekly sync is at 10:00 today", "sendAt": 1761440400000 } ``` Answer: ```json { "scheduled": { "id": "01JBG1…", "status": "scheduled", … } } ``` ### PATCH /api/workspaces/{w}/scheduled-messages/{id} Who: Its owner. App scope: `chat:write`. Change the `body` or move `sendAt`. A failed one given a new `sendAt` is scheduled again. Request: ```json { "sendAt": 1761526800000 } ``` Answer: ```json { "scheduled": { "id": "01JBG1…", "sendAt": 1761526800000, … } } ``` ### POST /api/workspaces/{w}/scheduled-messages/{id}/send Who: Its owner. App scope: `chat:write`. Send it now instead. Answers 201 with the posted message. Answer: ```json { "message": { "id": "01JBG4…", "body": "Weekly sync is at 10:00 today", … } } ``` ### DELETE /api/workspaces/{w}/scheduled-messages/{id} Who: Its owner. App scope: `chat:write`. Delete it so it's never sent. Answers 204. ## Pins and bookmarks Everyone who can write in a channel or DM can pin its messages (up to 100) and keep links at its top as bookmarks (up to 50). Everyone who can read it sees them. Changes reach the conversation's members as `pins.changed` and `bookmarks.changed` events with its `channelId`. A pin goes when its message is deleted. ### GET /api/channels/{c}/pins Who: Channel members. App scope: `channels:history`. The conversation's pinned messages, newest pin first. Answer: ```json { "pins": [ { "message": { "id": "01JBA2…", "body": "Q4 launch plan for Northwind", … }, "pinnedBy": "01JB8Q…", "pinnedAt": 1761238800000 } ] } ``` ### PUT /api/messages/{m}/pin Who: Channel members. App scope: `pins:write`. Pin a message in its conversation. Pinning one that's pinned does nothing. Answers 204. ### DELETE /api/messages/{m}/pin Who: Channel members. App scope: `pins:write`. Unpin it. Answers 204. ### GET /api/channels/{c}/bookmarks Who: Channel members. App scope: `channels:history`. The links kept at the top of the conversation, left to right. Answer: ```json { "bookmarks": [ { "id": "01JBB4…", "channelId": "01JB8S…", "title": "Price list", "url": "https://northwind.example/prices", "createdBy": "01JB8Q…", "createdAt": 1761238800000 } ] } ``` ### POST /api/channels/{c}/bookmarks Who: Channel members. App scope: `bookmarks:write`. Add a link at the end. `url` must be a web address (`https://` is added when it's missing); without a `title` the link's address names it. Request: ```json { "title": "Price list", "url": "https://northwind.example/prices" } ``` Answer: ```json { "bookmark": { "id": "01JBB4…", … } } ``` ### PATCH /api/channels/{c}/bookmarks/{id} Who: Channel members. App scope: `bookmarks:write`. Rename it, change its link, or move it to `position` (0 is first). Request: ```json { "title": "Northwind price list", "position": 0 } ``` Answer: ```json { "bookmark": { "id": "01JBB4…", … } } ``` ### DELETE /api/channels/{c}/bookmarks/{id} Who: Channel members. App scope: `bookmarks:write`. Remove it. Answers 204. ## Save for later and reminders Your own Later list: messages you saved to come back to, each with an optional reminder (`remindAt`, in milliseconds). Only you see it. A reminder goes off once, as a `reminder.due` event to your apps and a push notification to your phone when you aren't using Freesbe; any change reaches your other devices as `saved.changed`. Up to 500 in progress per workspace. Apps can't use these. ### GET /api/workspaces/{w}/saved?status=open Who: Members. In progress (`open`): reminders that went off first, then upcoming ones soonest first, then the rest newest first. `done` lists the newest 100 completed. `message` is null for one that was deleted or is in a conversation you left. Answer: ```json { "items": [ { "messageId": "01JBA2…", "channelId": "01JB8S…", "message": { … }, "createdAt": 1761238800000, "remindAt": 1761296400000, "remindedAt": null, "doneAt": null } ], "channels": [ { "id": "01JB8S…", "name": "sales", … } ] } ``` ### PUT /api/messages/{m}/save Who: Channel members. Save a message you can read, or change its reminder. Saving a completed one brings it back. Request: ```json { "remindAt": 1761296400000 } ``` Answer: ```json { "item": { "messageId": "01JBA2…", "remindAt": 1761296400000, … } } ``` ### PATCH /api/messages/{m}/save Who: Its owner. Change the reminder (`remindAt`, null to take it off) or mark it `done`. A new time sets the reminder off again. Request: ```json { "done": true } ``` Answer: ```json { "item": { "messageId": "01JBA2…", "doneAt": 1761300000000, … } } ``` ### DELETE /api/messages/{m}/save Who: Its owner. Take it out of Later. Answers 204, also when it wasn't there. ## Drafts What you've typed in a conversation and not sent, kept for all your devices: one per channel, DM or thread (`threadRootId`). Only you can see your drafts. Each change reaches your other devices as a `draft.changed` event. A workspace keeps up to 200 per person; the oldest make room. Apps can't use these. ### GET /api/workspaces/{w}/drafts Who: Members. Your drafts, newest first. Answer: ```json { "drafts": [ { "channelId": "01JB8S…", "threadRootId": null, "body": "Can you check the Northwind order before", "files": [], "updatedAt": 1761238800000 } ] } ``` ### PUT /api/workspaces/{w}/drafts Who: Members. Save the draft for a conversation you can see, replacing the one there. `body` is written like a message's. `fileIds` are the uploads waiting to go with it (up to 10, from `POST /api/workspaces/{w}/files`, in order): the draft keeps them, and they aren't cleaned up while it does. Ones that aren't yours or were already sent are left out. An empty `body` with no files deletes it and answers `{ "draft": null }`. Request: ```json { "channelId": "01JB8S…", "threadRootId": null, "body": "Can you check the Northwind order before", "fileIds": ["01JB8T…"] } ``` Answer: ```json { "draft": { "channelId": "01JB8S…", "updatedAt": 1761238800000, … } } ``` ### DELETE /api/workspaces/{w}/drafts?channelId={c}&threadRootId={m} Who: Its owner. Forget a draft, for example once it's sent. Leave out `threadRootId` for the conversation's own. Answers 204, also when there was none. ## Canvases Each channel and DM can have one canvas, a shared document. Its content travels only over the canvas WebSocket as Yjs updates: binary frames whose first byte is 0 for a document update or 1 for a presence (awareness) update from y-protocols. On connect the server sends `{"t":"ready","canEdit":true}` and then the whole document as one update; send it what it's missing, then every local change. Members of the conversation can write; anyone who can read it can watch. ### GET /api/channels/{c}/canvas Who: Members. The conversation's canvas, or `null` before anyone has opened it to write, and whether you can edit it. Answer: ```json { "canvas": { "id": "01JBG1…", "channelId": "01JB8S…", "createdBy": "01JB8Q…", "createdAt": 1761238800000, "updatedAt": 1761242400000, "updatedBy": "01JB8R…" }, "canEdit": true } ``` ### GET /api/channels/{c}/canvas/ws Who: Members. Opens the canvas WebSocket (send `Upgrade: websocket`). A member who can post starts the canvas by connecting; others get 404 until it exists. Changes from people who can only read are refused with `{"t":"error","code":"read_only"}`. A canvas holds up to 4 MB, and one frame up to 512 KB. ## Huddles and recordings Huddles are live calls in a channel; in a DM they ring the other people. They need the realtime WebSocket (for the `connId`) and Cloudflare Realtime for the audio and video, so in practice the Freesbe apps run them. Scripts can follow them through the WebSocket events. Answers 503 when huddles aren't set up on the server. When the workspace has used its video allowance, `videoPaused` is set: cameras and screen sharing stop, and voice keeps working. ### POST /api/channels/{c}/huddle/join Who: Channel members. Join the channel's huddle, starting one if needed. `connId` is from the WebSocket's hello, so a dropped connection leaves the call. Up to 20 people. `merged: true` when moving into a merged call (see merge below): a new DM huddle then doesn't ring the others, who are on their way. Request: ```json { "connId": "01JBF0…" } ``` Answer: ```json { "huddle": { "id": "01JBF1…", "channelId": "01JB8S…", "startedBy": "01JB8Q…", "startedAt": 1761210000000, "participants": [ { "userId": "01JB8Q…", "joinedAt": 1761210000000, "muted": false, "track": { "sessionId": "…", "trackName": "…" }, "video": null, "screen": null } ], "recording": null, "videoPaused": null } } ``` ### POST /api/channels/{c}/huddle/leave Who: Members. Leave the huddle. Works even if you were just removed from the channel. Answers 204. ### POST /api/workspaces/{w}/call-quality Who: Members. How a huddle sounded for you, sent by the apps as you leave: the `huddleId` and a `report` measured from the connection (packet loss, made-up audio, jitter, round trip, an estimated MOS). Nothing about what was said. Answers 204. Request: ```json { "huddleId": "01JBE7…", "report": { "id": "5f0c…", "app": "web", "os": "mac", "seconds": 1260, "lossPct": 0.4, "concealedPct": 0.2, "rttMs": 48, "mos": 4.38, "grade": "good", … } } ``` ### POST /api/call-quality/{id}/rating Who: No token. Thumbs up or down after a huddle or meeting: `rating` 1 or -1 and, for -1, `issues` (`echo`, `noise`, `choppy`, `quiet`, `delay`). The report's random `id` is the permission, for an hour after it was sent. Answers 204. Request: ```json { "rating": -1, "issues": ["choppy"] } ``` ### POST /api/channels/{c}/huddle/decline Who: Channel members. Turn down a DM call that's ringing for you. Answers 204. ### POST /api/huddles/{id}/merge Who: Channel members. Merge the call ringing you (`callId`, its huddle id) into the huddle `{id}` you're in. From a DM or group DM, it opens the group DM of everyone in both calls; from a channel, the callers join its huddle (403 when they can't see the channel). Everyone who moves gets a `huddle.merged` event and joins `channelId` with `merged: true`, which doesn't ring anyone. The call isn't counted as missed. 404 when the call no longer rings you, 409 when you aren't in `{id}` or the merged call would have more than 20 people. Request: ```json { "callId": "01JBE9…" } ``` Answer: ```json { "channelId": "01JBEA…" } ``` ### PUT /api/huddles/{id}/state Who: Channel members. Your state in the call: `muted`, and the published `track` (microphone), `video` and `screen`, or null when they stop. After reconnecting, send the new `connId`. Request: ```json { "muted": true } ``` ### ANY /api/partytracks/{huddleId}/* Who: App only. The media proxy to Cloudflare Realtime. Only the huddle's participants get through. ### POST /api/huddles/{id}/recording Who: App only. Start recording the huddle, in `video/webm`, `audio/webm`, `video/mp4` or `audio/mp4`. Everyone in the call is told. 403 when an admin turned recordings off, or when the workspace's `recordingControl` is `hosts` and you aren't the huddle's host (whoever started it), a co-host or an admin. Guests can't. A `transcriptOnly` start in a workspace with `autoTranscribe` skips that rule. With an `audioMime`, `audio` in the answer says whether to also upload an audio-only track for the transcript. With `transcriptOnly`, nothing is recorded: `mime` is an audio type, the chunks are the transcript's audio, and only the transcript and notes are kept (the notes are posted in the channel). That needs transcripts on, not recordings; 403 when they're off or the plan's transcripts for the month are used, 503 when the server can't transcribe. Request: ```json { "mime": "video/webm", "audioMime": "audio/webm;codecs=opus" } ``` Answer: ```json { "recording": { "id": "01JBF4…", … }, "audio": true } ``` ### PUT /api/recordings/{id}/chunks/{seq} Who: App only. The recorder's app uploads chunk 0, 1, 2… as the body, up to 32 MB each. ### PUT /api/recordings/{id}/audio/{seq} Who: App only. The audio-only track for the transcript, chunk by chunk like the video, when the start answered `audio: true`. ### POST /api/recordings/{id}/stop Who: Channel members. Stops the recording; the recorder's app then finishes it. Who can depends on the workspace's `recordingControl`: `everyone` (anyone from the workspace in the call), `starter` (the default: the recorder, the call's hosts and co-hosts, and owners and admins) or `hosts` (the same, for starting too). A meeting's hosts are its organizer and invited members; a huddle's is whoever started it. The recorder can always stop their own. 403 otherwise. Answers 204. ### PUT /api/huddles/{id}/cohosts Who: Channel members. A host of the huddle (whoever started it, a co-host, or an owner or admin) makes someone in it a co-host, or stops them being one. Co-hosts start and stop recordings like a host, and name more co-hosts, until the huddle ends. Guests can't be co-hosts. The huddle's `cohosts` lists them. Request: ```json { "userId": "01JBF2…", "cohost": true } ``` Answer: ```json { "huddle": { "id": "01JBF4…", "cohosts": ["01JBF2…"], … } } ``` ### PUT /api/huddles/{id}/drawing Who: Channel members. A host of the huddle (whoever started it, a co-host, or an owner or admin) turns drawing on shared screens off for everyone, or back on. Drawing is on when a huddle starts. While it's off, the huddle's `drawingOffBy` is the host who turned it off and lines people draw aren't passed on. Each participant's `drawSlot` is their pen colour for the whole huddle. The lines themselves go over the realtime socket (`draw` frames out, `huddle.draw` events in) and are never stored. Request: ```json { "on": false } ``` Answer: ```json { "huddle": { "id": "01JBF4…", "drawingOffBy": "01JBF2…", … } } ``` ### POST /api/recordings/{id}/finish Who: App only. The recorder uploaded every chunk (`durationMs`). `speakers` is who spoke when, to name the voices in the transcript: `[{ id, name, member, spans: [[startMs, endMs], …] }]`. The recording is saved shortly after: a huddle's is posted in the channel as a file, a meeting's is kept with the meeting. Then the transcript and notes are written. ### GET /api/workspaces/{w}/recordings Who: Members. App scope: `recordings:read`. Recordings of huddles and meetings you can see, newest first, up to 100. A meeting's recording has the meeting's `attendees`. `state` is `saving` from when a recording stops until its file is ready, then `saved`, or `failed` when nothing usable arrived (`file` is null until it's saved). `transcript` is `queued`, `working`, `ready`, `failed`, `skipped` (with a `skipped` reason: `not_set_up`, `off`, `allowance`, `cap` or `empty`) or null for recordings made before transcripts. Answer: ```json { "recordings": [ { "id": "01JBF4…", "kind": "meeting", "title": "Weekly sync", "state": "saved", "startedAt": 1729303200000, "durationMs": 1601000, "file": { "url": "…" }, "transcript": "ready", "skipped": null, … } ] } ``` ### GET /api/recordings/{id} Who: Members. App scope: `recordings:read`. A recording with its `notes` (summary, decisions and action items with their owner) and its transcript as `segments`: who spoke, their user id when they're a member, start and end in ms, and the text. Answer: ```json { "recording": { "id": "01JBF4…", "transcript": "ready", "notes": { "summary": "…", "decisions": ["…"], "actionItems": [ { "text": "Send Acme the new price list by Friday", "owner": "James Okafor" } ] }, "segments": [ { "speaker": "Maya Chen", "userId": "01JBC1…", "start": 4000, "end": 12000, "text": "…" } ], "language": "en", … } } ``` ### POST /api/recordings/{id}/transcribe Who: Members. Try a transcript again after it failed, or after the workspace had used its plan's transcripts. Answers the recording. ### GET /api/recordings/{id}/guest-links Who: Members. A meeting recording's links to its notes and transcript for people outside the workspace: the ones its guests were emailed, and the ones made with Share (`createdBy`; `email` is null for a link for anyone who has it). Who, until when, when and how often it was opened (`opens`), whether it plays the recording, and whether the email went out. `canRevoke` is true for the meeting's organizer, whoever recorded it and admins, and only they get `url`, the link itself, for links made with Share. Answer: ```json { "links": [ { "id": "01JBH2…", "email": "gina.lopez@acme.example", "name": "Gina Lopez", "createdAt": 1729609800000, "expiresAt": 1730214600000, "openedAt": 1729611720000, "revokedAt": null, "video": true, "failed": false, "createdBy": null, "opens": 2, "lastOpenedAt": 1729672320000, "url": null } ], "canRevoke": true } ``` ### DELETE /api/recordings/{id}/guest-links/{link} Who: Admins. Revoke one outside link right away (the meeting's organizer and whoever recorded it can too). Without `/{link}`, revokes every outside link for the recording. Answers the list. ### GET /api/recordings/{id}/sharing Who: Members. Who the recording is shared with in the workspace (`shares`: a `userId` or a `channelId`, who shared it and when), its outside links when you can manage them, and what you can do: `canShare` (members, not guests), `outsideBlock` (null when you can make outside links, otherwise `off`, `not_allowed`, `no_transcript`, `huddle` or `guest`), `maxDays` and `videoAllowed`. Answer: ```json { "sharing": { "shares": [ { "id": "01JBH5…", "userId": "01JBC4…", "channelId": null, "sharedBy": "01JBC1…", "createdAt": 1729611000000 } ], "links": [ … ], "canShare": true, "outsideBlock": null, "canRevoke": true, "maxDays": 7, "videoAllowed": true } } ``` ### POST /api/recordings/{id}/shares Who: Members, not guests. Share a recording you can see with up to 20 people (`userIds`, posted in your DM with each) and channels or group DMs (`channelIds`, which you must be able to post in), with an optional `comment`. Each message carries an attachment with the recording's title, a link to its page, the start of its summary, and `recording`: its `id`, `kind`, `startedAt`, `durationMs` and whether it has a `video`. The people it's shared with, and everyone in a shared channel, can then see the recording. Answers the sharing, 201. Request: ```json { "userIds": ["01JBC4…"], "channelIds": ["01JBC9…"], "comment": "Notes from the Acme call" } ``` ### DELETE /api/recordings/{id}/shares/{share} Who: Members. Take a share back: whoever made it, the meeting's organizer, whoever recorded it and admins. The message stays. Answers the sharing. ### POST /api/recordings/{id}/links Who: Members. Make a link to a meeting's notes and transcript for people outside the workspace: the meeting's organizer, whoever recorded it and admins, when the workspace has outside sharing on (`guestTranscripts`) and the transcript is ready. `days` is 1, 3, 7, 14 or 30, up to the workspace's `guestTranscriptDays`; `video` plays the recording too, when `guestTranscriptVideo` allows it. With an `email` (not a member of the workspace), the link is emailed there. Answers the link, with its `url`, and the sharing, 201. Request: ```json { "days": 7, "video": false, "email": "pat@contoso.example" } ``` Answer: ```json { "link": { "id": "01JBH6…", "email": "pat@contoso.example", "url": "https://app.freesbe.net/notes/…", … }, "sharing": { … } } ``` ## AI summaries, translations and writing help Summaries and translations written by AI, for the person asking only: nothing is posted or kept. Each request counts toward the workspace's monthly AI allowance, which starts again on its billing day; past it, or when an admin turned AI off, they answer 403. Up to 20 a minute per person (429). ### GET /api/workspaces/{w}/ai Who: Members, not guests. Whether AI features are on for the workspace (`enabled`) and set up on the server (`available`), and this usage period's requests against the allowance (`limit`, null for no limit), which starts again on `resets` (the workspace's billing day, UTC). Answer: ```json { "enabled": true, "available": true, "used": 412, "limit": 800, "resets": "2026-12-14" } ``` ### POST /api/channels/{c}/summary Who: Channel members. Summarize a thread (`threadRootId`) or the channel over a `span`: `unread` (since you last read, the default), `day` or `week`, up to the newest 400 messages. `language` is an app language code; it defaults to yours. Each point links to its message (`messageId`, and `threadRootId` for a reply). Messages repeated by the same author (alerts, bots) are read once. Summaries are kept: asking again with nothing new answers at once and isn't counted as a request, and new messages are read into the kept summary. Send `Accept: text/event-stream` to get it as server-sent events while it's written: `{"type":"stage","stage":"reading","count":240,"folded":180}`, then `{"type":"text","text":"…"}` pieces, then `{"type":"done","summary":{…},"cached":false}` (or `{"type":"error","message":"…"}`). Guests get 403. Request: ```json { "span": "day", "language": "en" } ``` Answer: ```json { "summary": { "overview": "The launch is on track for April 14.", "points": [ { "text": "Priya confirmed the date.", "messageId": "01JBD2…", "threadRootId": null } ], "messageCount": 9, "from": 1729303200000, "to": 1729316400000 } } ``` ### POST /api/messages/{m}/translate Who: Channel members. Translate a message into `language` (an app language code; yours by default). `same` is true when it already is in that language. Guests get 403. Request: ```json { "language": "de" } ``` Answer: ```json { "text": "Neue Bestellung von Contoso …", "same": false } ``` ### POST /api/workspaces/{w}/ai/rewrite Who: Members, not guests. Writing help: a better version of a draft (`text`, up to 4,000 characters) in a `style`: `fix` (spelling and grammar), `clearer`, `shorter`, `friendlier` or `formal`. It only rewrites the draft, in the draft's language, keeping mentions, links and code. Nothing is sent. Send `Accept: text/event-stream` to get it as server-sent events while it's written: `{"type":"text","text":"…"}` pieces, then `{"type":"done","text":"…"}` (or `{"type":"error","message":"…"}`). Gives up after 30 seconds (502). Request: ```json { "text": "hey contoso, thx for the order!! we can deliver by nov 6", "style": "formal" } ``` Answer: ```json { "text": "Hi Contoso team, thank you for your order. We can deliver by November 6." } ``` ### GET /api/workspaces/{w}/ai/photo-touchup Who: Members, not guests. How many profile photo touch-ups you have left today (3 a day, across your workspaces). Answer: ```json { "left": 2, "limit": 3 } ``` ### POST /api/workspaces/{w}/ai/photo-touchup Who: Members, not guests. Improve with AI for a profile photo: send `image` as base64 JPEG (up to 1,024 px on its longest side and 1 MB). The model checks it's a photo of a person with their face showing and answers where the face is (fractions of the picture) and gentle light and colour fixes, which the app applies itself. Anything else answers `ok: false`. 3 a day per person and 20 a day per workspace on Free (200 on paid plans); past that, 429. Counts as an AI request. Request: ```json { "image": "/9j/4AAQSkZJRgABAQ…" } ``` Answer: ```json { "ok": true, "face": { "x": 0.41, "y": 0.11, "width": 0.21, "height": 0.5 }, "adjust": { "brightness": 1.1, "contrast": 1.08, "saturation": 1.04, "warmth": 0.06 }, "softenBackground": true, "left": 2 } ``` ## Meetings Scheduled calls with a link people outside the workspace can open in a browser. Invites go out by email with a calendar file. ### GET /api/workspaces/{w}/meetings Who: Members. App scope: `meetings:read`. Upcoming meetings, and the last week's, that you organize, are invited to or joined. `attendees` lists who joined the call, in the order they came in: `{ userId, name }`, with `userId` null for a guest. While a call can be going, `live` lists who's in it and who's waiting in the lobby. Once everyone has left a call that took place, `live.callEndedAt` says when, and `live.rejoinUntil` how long the link still lets them back in. `lastCallAt` is when its last call ended: the link works until 30 days after, and `calls` lists its calls that are over, oldest first (`startedAt`, `endedAt`, `attendees`). Last week's means scheduled or called in the last 7 days. `chatMessages` counts the messages in its chat. `email` says whether invites are emailed. Answer: ```json { "meetings": [ { "id": "01JBE7…", "title": "Quarterly review with Acme Corp", "live": { "inCall": [ { "id": "…", "name": "Dana Whitfield", "guest": false } ], "waiting": [ { "id": "01JBE9…", "name": "Jordan Lee", … } ] }, … } ], "email": true } ``` ### POST /api/workspaces/{w}/meetings Who: Members, not guests. App scope: `meetings:write`. Schedule one: `title`, `startsAt`, `durationMin` (15, 30, 45, 60, 90, 120, 180 or 240), and optionally `description`, `timeZone`, members by `userIds` (those also in `optionalUserIds` are invited as optional) and anyone else by `emails` (up to 50 people), and `waitingRoom` (default true). Invites are emailed unless `notify` is false; `unsent` lists addresses that couldn't be. Answers 201. Request: ```json { "title": "Quarterly review with Acme Corp", "startsAt": 1761310800000, "durationMin": 45, "timeZone": "Europe/London", "userIds": ["01JB8R…"], "emails": ["jordan@acme.example"], "waitingRoom": true } ``` Answer: ```json { "meeting": { "id": "01JBE7…", "workspaceId": "01JB8Q…", "code": "kdq-rmvt-zpa", "url": "https://meet.freesbe.net/kdq-rmvt-zpa", "title": "Quarterly review with Acme Corp", "description": "", "startsAt": 1761310800000, "durationMin": 45, "timeZone": "Europe/London", "organizerId": "01JB8Q…", "invitees": [ { "email": "priya@northwind.example", "name": "Priya Raman", "userId": "01JB8R…" }, { "email": "jordan@acme.example", "name": null, "userId": null, "response": "accepted" } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } ``` ### POST /api/workspaces/{w}/support-sessions Who: Members, not guests. Start a support session: a one-hour meeting starting now, with a waiting room, whose link and `code` you give a customer. Members in it can ask to control the screen the customer shares from the desktop app. Needs remote control turned on for the workspace, or answers 403. Answers 201 with the meeting, like scheduling one. Answer: ```json { "meeting": { "id": "01JBE7…", "workspaceId": "01JB8Q…", "code": "kdq-rmvt-zpa", "url": "https://meet.freesbe.net/kdq-rmvt-zpa", "title": "Quarterly review with Acme Corp", "description": "", "startsAt": 1761310800000, "durationMin": 45, "timeZone": "Europe/London", "organizerId": "01JB8Q…", "invitees": [ { "email": "priya@northwind.example", "name": "Priya Raman", "userId": "01JB8R…" }, { "email": "jordan@acme.example", "name": null, "userId": null, "response": "accepted" } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } ``` ### PATCH /api/meetings/{id} Who: Organizer or admins. App scope: `meetings:write`. Change a meeting. Send the whole meeting, as when scheduling it. Everyone still invited gets an updated invite, new people an invite, removed people a cancellation. ### DELETE /api/meetings/{id} Who: Organizer or admins. App scope: `meetings:write`. Cancel it and email the cancellation. Anyone in the call is disconnected. Answers 204. ### POST /api/meetings/{id}/end Who: Organizer or admins. App scope: `meetings:write`. End the call for everyone; the link stops working. Answers 204. ### POST /api/meetings/{id}/admit Who: Members, not guests. Let someone in from the lobby (`admit: true`) or turn them away. `peerId` is from `live.waiting`. The organizer, invited members and admins can. Answers 204. Request: ```json { "peerId": "01JBE9…", "admit": true } ``` ### POST /api/meetings/{id}/new-link Who: Organizer or admins. App scope: `meetings:write`. Replace the link; the old one stops working and invitees get the new one. Answer: ```json { "meeting": { "id": "01JBE7…", "workspaceId": "01JB8Q…", "code": "kdq-rmvt-zpa", "url": "https://meet.freesbe.net/kdq-rmvt-zpa", "title": "Quarterly review with Acme Corp", "description": "", "startsAt": 1761310800000, "durationMin": 45, "timeZone": "Europe/London", "organizerId": "01JB8Q…", "invitees": [ { "email": "priya@northwind.example", "name": "Priya Raman", "userId": "01JB8R…" }, { "email": "jordan@acme.example", "name": null, "userId": null, "response": "accepted" } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } ``` ### GET /api/meet/{code} Who: No token. What a meeting link shows before joining: title, time, organizer, workspace, whether it's `early`, `open`, `closed` or `canceled`, and whether guests wait in a lobby. ### POST /api/meet/{code}/join Who: App only. Knock or join, as a member or a guest with a `name` (or the personal `invite` token from their email). Answers a `token` for the call's socket and media, and whether you're `admitted` or waiting. ### WS /api/meet/{code}/ws?t= Who: App only. The meeting call's socket. ### ANY /api/meet/{code}/media/{token}/* Who: App only. The meeting's media proxy to Cloudflare Realtime. ### POST /api/meet/{code}/quality Who: App only. How the call sounded for you, sent as you leave, with the `token` from joining: the same `report` as a huddle's. Answers 204. ### GET /api/meet/{code}/chat/{token} Who: App only. The call's chat so far, oldest first, for anyone let into the call (members and guests, with the `token` from joining). New messages also arrive on the socket as `{ t: "chat", message }`. In the call, open a message's file at `/api/meet/{code}/chat/{token}/files/{id}`. Answer: ```json { "messages": [ { "id": "01JBG2…", "peerId": "01JBE9…", "userId": null, "name": "Jordan Lee", "guest": true, "body": "Here are our numbers", "file": { "id": "01JBG3…", "name": "forecast.xlsx", "mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "size": 48200, "url": "/api/files/01JBG3…" }, "createdAt": 1761310920000 } ] } ``` ### POST /api/meet/{code}/chat/{token} Who: App only. Send a message (`body`, up to 4,000 characters) to everyone in the call. Answers 201 with `message`. Request: ```json { "body": "The price list is at https://northwind.example/wholesale" } ``` ### POST /api/meet/{code}/chat/{token}/files?name= Who: App only. Send a file to the call's chat: the file is the body, with `Content-Type` and `Content-Length`, and `width`/`height` for pictures, `text` for a caption. Guests from outside follow the workspace's file rules for guests (403 `file_blocked`). Answers 201 with `message`. ### GET /api/meetings/{id}/chat Who: Members. The chat kept with a meeting, after the call: for its organizer, invited members, members who were in the call, people who can see its recording, and owners and admins. Files open at their `url`. 404 for anyone else. ### POST /api/meetings/code/{code}/recording Who: App only. Start recording the meeting you're in, by its code. Workspace members can; guests can't. Everyone in the call sees who is recording. Upload and finish it like a huddle recording; it's kept with the meeting and listed under `/api/workspaces/{w}/recordings`. 409 when it's already being recorded. Request: ```json { "mime": "video/webm", "audioMime": "audio/webm;codecs=opus" } ``` Answer: ```json { "recording": { "id": "01JBF4…", … }, "audio": true } ``` ## Calendar Each person can connect one Google or Microsoft calendar, with permission to see only when they're busy, never what the events are. Freesbe then shows busy times when scheduling and can set "In a meeting" as their status. With permission to read events too, the [Calendar app](/docs/calendar-app/) sends a daily agenda, reminders and invitations to answer. The [Outlook add-in](/docs/outlook/) schedules Freesbe meetings from Outlook events with its own token. ### GET /api/me/calendar Who: Signed in. Your connected calendar, if any, and the providers this server can connect to: `google`, `microsoft` and `zoho`. `needsReconnect` means the provider stopped accepting Freesbe's access. Answer: ```json { "connection": { "provider": "google", "email": "dana@northwind.example", "autoStatus": true, "connectedAt": 1761200000000, "needsReconnect": false, "events": true, "app": { "workspaceId": "01JB8Q…", "agenda": true, "agendaMinute": 480, "weekends": false, "reminderMinutes": 10, "invites": true } }, "providers": ["google", "microsoft", "zoho"] } ``` ### PATCH /api/me/calendar Who: Signed in. Turn the automatic "In a meeting" status on or off, and change the Calendar app: `app.workspaceId` turns it on (messages go to your DM with yourself there) or off with `null`; `agendaMinute` is minutes after midnight in your time zone; `reminderMinutes` is 1, 5, 10, 15, 30 or `null`. Setting your status by hand takes it back from the calendar. Request: ```json { "autoStatus": false, "app": { "agendaMinute": 450, "reminderMinutes": 5 } } ``` ### DELETE /api/me/calendar Who: Signed in. Disconnect the calendar and clear a status it set. Answers 204. ### GET /api/auth/oauth/{provider}/start?calendar=1 Who: App only. Open in the browser to connect a calendar. Comes back to `?next=` with `?calendar=connected` or `?calendar_error=`. Add `&events=1&workspace={w}` to also let the Calendar app read events and answer invitations, and turn it on in that workspace; without that permission it comes back with `?calendar_error=events`. ### GET /api/integrations/zoho/calendar/start Who: App only. Open in the browser to connect Zoho Calendar, in any Zoho data center. Takes the same `next`, `events=1` and `workspace={w}` as the Google and Microsoft one and comes back the same way, through Zoho's shared callback. Zoho Calendar events have `canAnswer: false`: Freesbe can't answer them, so `rsvp` answers 400. ### GET /api/me/calendar/agenda?day=YYYY-MM-DD Who: Signed in. Calendar app: your events on a day (today when left out) in your time zone, without declined or canceled ones. `response` is `organizer`, `pending`, `accepted`, `tentative` or `declined`; `joinUrl` is a video call link when the event has one. Answer: ```json { "day": "2026-10-23", "timezone": "Asia/Singapore", "events": [ { "id": "7k2…", "title": "Sales standup", "start": 1761183000000, "end": 1761183900000, "allDay": false, "location": null, "joinUrl": "https://meet.google.com/abc-defg-hij", "link": "https://calendar.google.com/…", "organizer": "Priya Raman", "response": "accepted", "cancelled": false } ] } ``` ### POST /api/me/calendar/agenda/send Who: Signed in. Calendar app: send today's agenda to your DM with yourself now. Answers 204. ### POST /api/me/calendar/events/{id}/rsvp Who: Signed in. Calendar app: answer an invitation in your calendar; the organizer hears about it. With `messageId`, the Calendar message it came from shows the answer. Request: ```json { "answer": "accepted", "messageId": "01JBF7…" } ``` Answer: ```json { "event": { "id": "7k2…", "response": "accepted", … } } ``` ### GET /api/workspaces/{w}/calendar-app Who: Admins. Whether people can use the Calendar app in the workspace, and how many get its messages there; whether they can schedule Freesbe meetings from Outlook and Google Calendar (`addins`), how many connected an add-in, the Outlook add-in's manifest address for the Microsoft 365 admin center, and the Google Calendar add-on's Apps Script files. Answer: ```json { "enabled": true, "people": 12, "addins": true, "addinPeople": 4, "outlookManifestUrl": "https://app.freesbe.net/addins/outlook/manifest.xml", "googleAddon": { "code": "https://app.freesbe.net/addins/google/Code.gs", "manifest": "https://app.freesbe.net/addins/google/appsscript.json" } } ``` ### PUT /api/workspaces/{w}/calendar-app Who: Admins. Turn the Calendar app (`enabled`) or the calendar add-ins for Outlook and Google Calendar (`addins`) on or off for the workspace; send either or both. Calendar app off stops everyone's Calendar messages there; add-ins off stop everyone's add-ins from scheduling meetings in it. Request: ```json { "enabled": false, "addins": true } ``` ### GET /api/me/calendar-addins Who: Signed in. The calendar add-ins (Outlook, Google Calendar) you connected, one per workspace and calendar, with how many meetings each scheduled, and the add-in's manifest address. Answer: ```json { "connections": [ { "id": "01JBP4…", "client": "outlook", "workspaceId": "01JB8Q…", "workspaceName": "Northwind Traders", "createdAt": 1761800000000, "lastUsedAt": 1761886400000, "meetings": 7 } ], "outlookManifestUrl": "https://app.freesbe.net/addins/outlook/manifest.xml" } ``` ### POST /api/me/calendar-addins Who: App only. What the add-in's sign-in window calls: connects the Outlook add-in (`client`: `outlook`) or the Google Calendar add-on (`google`) to a workspace. Outlook's gets its token (`fsb-cal-…`), which can only schedule your meetings there; Google's gets a one-time code, good for ten minutes, that the add-on trades for its token with `POST /addins/google/token` (body: `code`). Connecting again replaces the old token. Answers 403 when the workspace's admins turned the add-ins off, and to guests. Request: ```json { "client": "outlook", "workspaceId": "01JB8Q…" } ``` ### DELETE /api/me/calendar-addins/{id} Who: Signed in. Disconnect an add-in; its token stops working at once. Meetings it scheduled stay. Answers 204. ### POST /api/calendar-addins/meetings Who: App only. The add-in's own call, with its token as `Authorization: Bearer fsb-cal-…`: the Freesbe meeting for the Outlook or Google Calendar event it's in. The same event (by `eventId`, or the `code` of the link already in the invitation) keeps its meeting, brought up to date; otherwise a new one. Freesbe sends no emails for it, since the calendar sends the invitation. Returns the meeting, the link to put in the invitation, and the text for it in the calendar's `locale`. Request: ```json { "title": "Quarterly review", "startsAt": 1761912000000, "endsAt": 1761915600000, "timeZone": "America/New_York", "attendees": [ { "email": "jordan@acme.example", "name": "Jordan Lee" } ], "locale": "en-US" } ``` Answer: ```json { "meeting": { "id": "01JBP9…", "code": "abc-defg-hij", "source": "outlook", … }, "joinUrl": "https://meet.freesbe.net/abc-defg-hij", "workspaceName": "Northwind Traders", "reused": false, "inviteHtml": "
…
", "inviteText": "…" } ``` ### GET /api/calendar-addins/me Who: App only. With the add-in's token: who it belongs to and in which workspace. Answer: ```json { "name": "Maya Patel", "email": "maya@northwind.example", "workspaceName": "Northwind Traders" } ``` ### POST /api/workspaces/{w}/availability Who: Members, not guests. What it takes to find a time for up to 20 `userIds` between `from` and `to`, at most 8 days apart. `busy` comes from each person's connected calendar (`calendar` is `connected`, `none` or `error`); `meetings` are their Freesbe meetings in any workspace, leaving out `exceptMeetingId` when you're moving one. `workingHours` (Sunday first, minutes after midnight) are in their `timezone`, and `dndUntil` is when their paused notifications resume. Request: ```json { "userIds": ["01JB8R…", "01JB8T…"], "from": 1761292800000, "to": 1761379200000 } ``` Answer: ```json { "people": [ { "userId": "01JB8R…", "calendar": "connected", "busy": [ { "start": 1761300000000, "end": 1761303600000 } ], "meetings": [ { "start": 1761307200000, "end": 1761309000000 } ], "timezone": "Europe/London", "workingHours": [[], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 1020]], []], "dndUntil": null }, { "userId": "01JB8T…", "calendar": "none", "busy": [], "meetings": [], … } ] } ``` ## Bookings A public page where anyone can book a 15, 30 or 60 minute call with you, like Calendly. Each person makes their own, up to 10 per workspace. A booking becomes an ordinary meeting, with its email and calendar invite, and avoids busy times in your connected calendar. ### GET /api/workspaces/{w}/booking-links Who: Members. Your booking pages in the workspace, the `baseUrl` they start with (the workspace's `customDomain` when it has one), whether your calendar is checked, and whether bookings are emailed. ### POST /api/workspaces/{w}/booking-links Who: Members, not guests. Make a link: `slug` (3 to 40 lowercase letters, digits and dashes), `title`, `durations`, weekly `hours` (Sunday first, as minutes after midnight) and `timeZone`, and optionally `description`, `minNoticeMin`, `maxDaysAhead`, `bufferMin`, `showPhoto`, `verifyEmail` (bookers type a code we email them; on unless set), `onePerPerson` (one upcoming booking per address; on unless set), `enabled` and `questions`. `questions` (up to 6) are asked on the page after name and email: each has an `id` you pick (letters, digits, `-` and `_`), a `kind` (`company`, `team_size`, `phone`, or `text` with your own `label`) and `required`. Company, team size and phone can each be asked once. Answers 201, or 409 when the name is taken. Request: ```json { "slug": "dana-intro", "title": "Intro call with Dana", "durations": [15, 30], "hours": [[], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 720]], []], "timeZone": "Europe/London", "minNoticeMin": 240, "bufferMin": 10, "questions": [ { "id": "company", "kind": "company", "required": true }, { "id": "size", "kind": "team_size", "required": false }, { "id": "goal", "kind": "text", "label": "What would you like to see?", "required": false } ] } ``` Answer: ```json { "link": { "id": "01JBJ2…", "slug": "dana-intro", "url": "https://app.freesbe.net/b/dana-intro", "title": "Intro call with Dana", "durations": [15, 30], "maxDaysAhead": 60, "enabled": true, … } } ``` ### PATCH /api/booking-links/{id} Who: Its owner. Change one of your links. Send the whole link, as when making it; `enabled: false` pauses it. Leave out `questions` to keep the link's questions as they are. ### DELETE /api/booking-links/{id} Who: Its owner. Delete one of your links. Meetings already booked stay. Answers 204. ### GET /api/booking-slugs/{slug} Who: Signed in. Whether a link name is free. Answer: ```json { "slug": "dana-intro", "available": true } ``` ### GET /api/book/{slug} Who: No token. What the booking page shows: title, description, owner, workspace, the lengths offered, the owner's time zone and the link's `questions`. ### GET /api/book/{slug}/slots?duration=&from=&to= Who: No token. The free start times for one length, between `from` and `to` (at most 42 days apart). Answer: ```json { "durationMin": 30, "slots": [1761300000000, 1761301800000, 1761303600000] } ``` ### POST /api/book/{slug} Who: No token. Book a time: `durationMin`, `startsAt`, `name`, `email`, and optionally a `note`, the booker's `timeZone` and `answers` to the link's questions, by question id. Required questions need an answer; `team_size` is one of `1-10`, `11-50`, `51-200`, `201-1000` or `1000+`. The answers go into the meeting's description, the `booking.created` event and workflow variables. Answers 201 with `status: "booked"`, the booker's own meeting link, whether an invite was emailed, and `manageUrl`, the booker's page to move or cancel it. When the page verifies bookers' email, it answers 202 with `status: "verify"` and a `holdId` instead: a 6-digit code went to the address and the time is held for 10 minutes. When the address already has an upcoming booking on a page that takes one per person, it answers `status: "already_booked"` and emails the address its booking's link. Request: ```json { "durationMin": 30, "startsAt": 1761301800000, "name": "Jordan Lee", "email": "jordan@acme.example", "note": "Pricing for 40 seats", "timeZone": "America/Chicago", "answers": { "company": "Acme Corp", "size": "11-50" } } ``` Answer: ```json { "title": "Intro call with Dana", "startsAt": 1761301800000, "durationMin": 30, "ownerName": "Dana Whitfield", "meetingUrl": "https://meet.freesbe.net/…", "emailed": true, "manageUrl": "https://app.freesbe.net/b/dana?manage=…" } ``` ### POST /api/book/{slug}/verify Who: No token. Confirm a held time with the code from the email: `holdId` and `code`. Answers like booking does, or `status: "already_booked"` with the existing booking's `manageUrl`. A wrong code answers 400 (five tries); a code that ran out answers 409. Request: ```json { "holdId": "01JBK7…", "code": "482913" } ``` ### POST /api/book/{slug}/resend Who: No token. Email a new code for a held time: `holdId`. Up to two more codes per hold; the hold runs 10 minutes from the new code. Request: ```json { "holdId": "01JBK7…" } ``` ### GET /api/book/manage/{meetingId}?t= Who: No token. The booker's own booking, by the secret `t` from their `manageUrl` (the part after the dot in `manage=.`): its time, who it's with, their meeting link, whether it's canceled, the booking page it came from, and whether it can still be moved (`canReschedule`) or canceled (`canCancel`), and `changesLeft`, how many more times the booker can move it. ### GET /api/book/manage/{meetingId}/slots?t=&from=&to= Who: No token. The free times the booking can move to, at its own length. Its current time counts as free. ### POST /api/book/manage/{meetingId}/reschedule Who: No token. Move the booking to a free time: `token` (the secret) and `startsAt`, and optionally the booker's `timeZone`. Both sides get an updated invite. Answers like booking does. Request: ```json { "token": "…", "startsAt": 1761309000000, "timeZone": "America/Chicago" } ``` ### POST /api/book/manage/{meetingId}/cancel Who: No token. Cancel the booking: `token`, and optionally a `reason` for the host. Both sides get a cancellation. Answers 204. Request: ```json { "token": "…", "reason": "Our budget review moved to November" } ``` ### GET /api/book/{slug}/photo Who: No token. The owner's photo, when the link shows it. On a team page, `?h=` is the person's place in the page's `team.hosts`. ## Team booking pages On the Corporate and Enterprise plans, admins make booking pages for several people. In `round_robin` mode a time is offered when anyone on the team is free, and the booking goes to one of the people free then: whoever had the fewest bookings in the last 30 days (`order: fewest`) or the first one in the list (`order: priority`). In `together` mode a time is offered only when everyone is free, and they all join the meeting. Each person can pause themselves. The public side is the same as any booking page (Bookings), and `GET /api/book/{slug}` adds `team` with the mode and the people on it. ### GET /api/workspaces/{w}/team-booking-pages Who: Admins. The workspace's team booking pages, with their people in order (and who paused themselves), the `baseUrl` links start with, and whether the plan `included` them. ### POST /api/workspaces/{w}/team-booking-pages Who: Admins. Make a team page: everything a booking page takes (see Bookings), plus `mode` (`round_robin` or `together`), `order` (`fewest` or `priority`, for round robin) and `hostIds`, up to 20 members in order. Guests can't take bookings. Answers 201, 403 when the plan doesn't include team pages, or 409 when the name is taken. Request: ```json { "slug": "northwind-demo", "title": "Product demo", "durations": [30], "hours": [[], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 1020]], [[540, 1020]], []], "timeZone": "America/New_York", "mode": "round_robin", "order": "fewest", "hostIds": ["01JBJ2…", "01JBJ3…", "01JBJ4…"] } ``` Answer: ```json { "id": "01JBK1…", "slug": "northwind-demo", "url": "https://app.freesbe.net/b/northwind-demo", "mode": "round_robin", "order": "fewest", "hosts": [{ "userId": "01JBJ2…", "name": "Maya Chen", "paused": false }, …], … } ``` ### PATCH /api/team-booking-pages/{id} Who: Admins. Change a team page. Send the whole page, as when making it; `enabled: false` pauses it for everyone. People who paused themselves stay paused. ### DELETE /api/team-booking-pages/{id} Who: Admins. Delete a team page. Meetings already booked stay. Answers 204. ### GET /api/workspaces/{w}/team-booking-pages/mine Who: Members. The team pages you take bookings on, and whether you paused yourself on each. Answer: ```json { "pages": [{ "id": "01JBK1…", "title": "Product demo", "url": "https://app.freesbe.net/b/northwind-demo", "mode": "round_robin", "enabled": true, "paused": false, "hostNames": ["Maya Chen", "Sam Ortiz"] }] } ``` ### PUT /api/team-booking-pages/{id}/me Who: Members. Pause or resume yourself on a team page: `paused`. While paused, your times aren't offered and no bookings go to you. Request: ```json { "paused": true } ``` ## Custom domain On the Corporate and Enterprise plans, a workspace can serve its booking pages, meeting links and invite links from its own subdomain, such as `meetme.northwind.example`. That address answers only the public booking and meeting endpoints for its own workspace, never the rest of the API. ### GET /api/workspaces/{w}/custom-domain Who: Admins. The workspace's domain, if any, with its `status` (`pending`, `active` or `failed`), what it's still waiting for, and the DNS records to add. Also the workspace's `plan` and whether it `included` a custom domain. Answer: ```json { "domain": { "hostname": "meetme.northwind.example", "status": "pending", "problem": "Waiting for the CNAME record", "cname": { "name": "meetme.northwind.example", "target": "…" }, "txt": null, "createdAt": 1761200000000, … }, "plan": "corporate", "included": true, "configured": true } ``` ### PUT /api/workspaces/{w}/custom-domain Who: Admins. Connect a subdomain you own (`hostname`; a bare domain like `northwind.example` can't be used). 403 when the plan doesn't include it, 409 when the workspace already has one or another workspace uses it. Request: ```json { "hostname": "meetme.northwind.example" } ``` ### POST /api/workspaces/{w}/custom-domain/check Who: Admins. Check again whether the DNS record and certificate are ready. ### DELETE /api/workspaces/{w}/custom-domain Who: Admins. Disconnect the domain. Links go back to the Freesbe address. Answers 204. ## Punch clock When an admin turns the punch clock on, the people it covers clock in and out. Times are milliseconds; every change after the fact needs a `reason` and is kept in the entry's history. ### GET /api/workspaces/{w}/time/clock Who: Members. Your clock: the settings, whether the clock covers you, and whether you're `working`, on a `break` or `off`. Answer: ```json { "settings": { "enabled": true, "trackEveryone": false, "weekStart": 1, "timezone": null, "dailyOvertimeMinutes": 480, "weeklyOvertimeMinutes": 2400, "maxShiftMinutes": 720, "paidBreaks": false }, "tracked": true, "clock": { "userId": "01JB8R…", "state": "working", "since": 1761202800000, "shiftStartedAt": 1761202800000 } } ``` ### POST /api/workspaces/{w}/time/clock Who: Members. `action`: `in`, `break`, `resume` or `out`, with an optional `note` and your current `timezone`. Request: ```json { "action": "in", "note": "Opening the Acme Corp warehouse" } ``` Answer: ```json { "settings": { "enabled": true, "trackEveryone": false, "weekStart": 1, "timezone": null, "dailyOvertimeMinutes": 480, "weeklyOvertimeMinutes": 2400, "maxShiftMinutes": 720, "paidBreaks": false }, "tracked": true, "clock": { "userId": "01JB8R…", "state": "working", "since": 1761202800000, "shiftStartedAt": 1761202800000 } } ``` ### GET /api/workspaces/{w}/time/entries Who: Members. Entries between `from` and `to` (milliseconds, at most 93 days): your own, or for admins everyone's, or one person's with `userId`. ### POST /api/workspaces/{w}/time/entries Who: Members. Add time after the fact: `kind` (`work` or `break`), `startedAt`, `endedAt`, `note` and a `reason`. Admins can add it for someone with `userId`. Request: ```json { "kind": "work", "startedAt": 1761116400000, "endedAt": 1761145200000, "note": "Stocktake at the Acme Corp warehouse", "reason": "Forgot to clock in" } ``` Answer: ```json { "entry": { "id": "01JBH5…", "userId": "01JB8R…", "kind": "work", "startedAt": 1761116400000, "endedAt": 1761145200000, "timezone": "Asia/Singapore", "source": "manual", "note": "Stocktake at the Acme Corp warehouse", "edited": false } } ``` ### PATCH /api/workspaces/{w}/time/entries/{id} Who: Members. Change an entry: your own, or anyone's for admins. Needs a `reason`. Request: ```json { "endedAt": 1761148800000, "reason": "Stayed for the delivery" } ``` ### DELETE /api/workspaces/{w}/time/entries/{id} Who: Members. Delete an entry, with a `reason` in the body. Answers 204. ### GET /api/workspaces/{w}/time/entries/{id}/history Who: Members. Every change to an entry, oldest first: who, when, before, after and why. ### GET /api/workspaces/{w}/time/board Who: Admins. Who is on the clock right now. ### GET /api/workspaces/{w}/time/export Who: Admins. Timesheets as CSV: `from` and `to` dates (`YYYY-MM-DD`), `format` `shifts` or `daily`, optionally one `userId`. Days are cut in each person's time zone. ### GET /api/workspaces/{w}/time/settings Who: Admins. The punch clock settings and who it covers (`trackedUserIds`). ### PATCH /api/workspaces/{w}/time/settings Who: Admins. Turn it on (`enabled`), cover everyone (`trackEveryone`) or some people (`trackedUserIds`), and set `weekStart`, `timezone`, daily and weekly overtime, the longest shift and `paidBreaks`. Request: ```json { "enabled": true, "trackEveryone": false, "trackedUserIds": ["01JB8R…"], "weeklyOvertimeMinutes": 2400 } ``` ## Apps Admins make apps from a Slack-shaped manifest. Each app has a bot user and a bot token (`fsb-bot-…`) that only reaches the endpoints its scopes allow; each endpoint below lists the scope it needs. See [apps and bot tokens](#apps) above. ### GET /api/workspaces/{w}/apps Who: Admins. The workspace's apps, by name. Tokens aren't included, only their last four characters (`tokenHint`). ### POST /api/workspaces/{w}/apps Who: Admins. Make an app from `manifest` (parse YAML to JSON first). It gets a bot user, in no channels yet, a bot token and a `signingSecret`; the token is only in this answer. Slack scope and event names are mapped; ones Freesbe doesn't have are listed in `unsupportedScopes` and `unsupportedEvents`, and events whose scope is missing in `eventsMissingScope`. With `settings.event_subscriptions.request_url`, the URL must answer [the challenge](#app-events) first, or this answers 400. Answers 201. Request: ```json { "manifest": { "display_information": { "name": "Order desk", "description": "Posts new wholesale orders to #orders" }, "features": { "bot_user": { "display_name": "Order desk" } }, "oauth_config": { "scopes": { "bot": ["channels:read", "channels:join", "chat:write"] } } } } ``` Answer: ```json { "app": { "id": "01JC2A…", "name": "Order desk", "description": "Posts new wholesale orders to #orders", "color": "#2D6A4F", "botUserId": "01JC2B…", "botName": "Order desk", "iconUrl": "/api/avatars/01JC2B…?v=01JF…", "scopes": ["channels:read", "channels:join", "chat:write"], "manifest": { "display_information": { "name": "Order desk", … }, … }, "eventsUrl": null, "events": [], "tokenHint": "x9Qa", "tokenCreatedAt": 1761200000000, "lastUsedAt": null, "createdBy": "01JB8Q…", "createdAt": 1761200000000, "disabledAt": null }, "token": "fsb-bot-…", "signingSecret": "8f14e45fceea167a5a36dedd4bea2543" } ``` ### GET /api/workspaces/{w}/apps/{id} Who: Admins. One app. ### PATCH /api/workspaces/{w}/apps/{id} Who: Admins. Replace the `manifest` (the name, colour, scopes, events and bot name follow it; a new request URL must answer the challenge), or turn the app off and on with `disabled`. A turned-off app's token answers 401 and it gets no events. Request: ```json { "disabled": true } ``` ### PUT /api/workspaces/{w}/apps/{id}/icon Who: Admins. Give the app an icon, which is also its bot's picture in messages and member lists: the raw PNG, JPEG, GIF or WebP bytes with their `Content-Type`, up to 2 MB. A square picture works best. Answers with the app, its `iconUrl` set. Request: ```json curl -X PUT $API/api/workspaces/$W/apps/$APP/icon \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: image/png' --data-binary @order-desk.png ``` ### DELETE /api/workspaces/{w}/apps/{id}/icon Who: Admins. Remove the app's icon, so its first letter shows again (or, for a Zapier, Make, n8n or Zoho Flow app, that tool's logo). Answers with the app. ### POST /api/workspaces/{w}/apps/{id}/token Who: Admins. A new bot token, only in this answer. The old one stops working at once. ### DELETE /api/workspaces/{w}/apps/{id} Who: Admins. Delete the app and its token. Its bot stays as a deactivated member, so its messages keep its name. Answers 204. ### GET /api/workspaces/{w}/apps/{id}/secret Who: Admins. The signing secret the app's event deliveries are signed with. Answer: ```json { "signingSecret": "8f14e45fceea167a5a36dedd4bea2543" } ``` ### GET /api/workspaces/{w}/apps/{id}/deliveries Who: Admins. The last 50 event deliveries to the app's request URL, newest first, kept for 14 days. Same shape as a webhook's deliveries. ### GET /api/apps/me Who: App token. Which app, bot user and workspace the token belongs to, and its scopes, like Slack's `auth.test`. Needs no scope. Answer: ```json { "appId": "01JC2A…", "appName": "Order desk", "botUserId": "01JC2B…", "workspaceId": "01JB8Q…", "workspaceName": "Northwind Traders", "scopes": ["channels:read", "channels:join", "chat:write"] } ``` ### PUT /api/apps/me/icon Who: App token. Set the app's own icon, its bot's picture, like Slack's `users.setPhoto`: the raw PNG, JPEG, GIF or WebP bytes with their `Content-Type`, up to 2 MB. Needs no scope. Answers with the app. Request: ```json curl -X PUT $API/api/apps/me/icon \ -H "Authorization: Bearer $BOT_TOKEN" \ -H 'Content-Type: image/png' --data-binary @icon.png ``` ### DELETE /api/apps/me/icon Who: App token. Remove the app's icon, so its first letter shows again. Needs no scope. Answers with the app. ### GET /api/workspaces/{w}/app-commands Who: Members. The slash commands and message actions the workspace's apps add, for the message box and message menus. Turned-off apps are left out. Answer: ```json { "commands": [ { "appId": "01JC2A…", "appName": "Order desk", "color": "#2D6A4F", "command": "/order", "description": "Look up an order", "usageHint": "[order number]" } ], "messageActions": [ { "appId": "01JC2A…", "appName": "Order desk", "color": "#2D6A4F", "callbackId": "create_order", "name": "Create order", "description": "" } ] } ``` ### POST /api/channels/{c}/commands Who: Channel members. Run a slash command in the channel (or the thread in `threadRootId`): Freesbe sends it to the app that has it. `reply` is the app's answer when only you see it, or null. 404 when no app has the command; 502 when the app can't be reached, answers with an error or takes longer than 3 seconds. Request: ```json { "command": "/order", "text": "4512" } ``` Answer: ```json { "reply": { "id": "01JD…", "appId": "01JC2A…", "appName": "Order desk", "color": "#2D6A4F", "channelId": "01JB9…", "threadRootId": null, "body": "Order 4512 is packed and ships today.", "blocks": [], "createdAt": 1761200000000 } } ``` ### POST /api/messages/{m}/app-actions Who: Channel members. Send the message to one of an app's message actions, as Slack's `message_action`. Answers 204; the app answers later through its `response_url`, if at all. Request: ```json { "appId": "01JC2A…", "callbackId": "create_order" } ``` ### POST /api/messages/{m}/app-buttons Who: Channel members. Press a button on an app's message: Freesbe sends the app Slack's `block_actions`. `clientTrigger` is any random string; a dialog the app opens in answer comes over the socket with it. Answers 204; 404 when the button or app is gone. Request: ```json { "actionId": "pack_order", "blockId": "order_4471", "clientTrigger": "k3v9q2" } ``` ### POST /api/channels/{c}/app-buttons Who: Channel members. Press a button on an app's answer only you see. Those aren't kept, so send the button's `text` and `value` with it. Answers 204. Request: ```json { "appId": "01JC2A…", "ephemeralId": "01JD…", "actionId": "restock", "text": "Restock", "value": "assam" } ``` ### POST /api/app-views/{id}/submit Who: Members. Send an app's dialog. `values` holds each input's answer by its block id: a string, or a list for checkboxes. The app hears it as `view_submission`; `action` is what it answered: `close`, `clear` (close every dialog), `errors` (by block id), or `update` and `push` with the next `view`. Request: ```json { "values": { "qty": "24", "warehouse": "singapore" } } ``` Answer: ```json { "action": "errors", "errors": { "qty": "Only 12 cases left" } } ``` ### POST /api/app-views/{id}/buttons Who: Members. Press a button in an app's dialog, with what's filled in so far. Answers 204. Request: ```json { "actionId": "add_item", "values": { "qty": "24" } } ``` ### POST /api/app-views/{id}/close Who: Members. Close an app's dialog, or every dialog of its stack with `cleared`. An app that set `notify_on_close` hears `view_closed`. Answers 204. Request: ```json { "cleared": true } ``` ### POST /api/apps/views Who: App token. Open a dialog, like Slack's `views.open`: `triggerId` is the `trigger_id` of a command, action or button press (it works for 30 seconds), and `view` is a Block Kit modal. Needs no scope. Request: ```json { "triggerId": "eyJh…", "view": { "type": "modal", "callback_id": "new_order", "title": { "type": "plain_text", "text": "New order" }, "submit": { "type": "plain_text", "text": "Create" }, "blocks": [ { "type": "input", "block_id": "qty", "label": { "type": "plain_text", "text": "Cases" }, "element": { "type": "number_input", "action_id": "qty", "is_decimal_allowed": false } } ] } } ``` Answer: ```json { "view": { "id": "01JD…", "rootId": "01JD…", "parentId": null, "hash": "a1b2c3d4e5f6" } } ``` ### POST /api/apps/views/push Who: App token. Add a dialog on top of an open one (`views.push`), with a `trigger_id` from inside it. Dialogs stack three deep. Request: ```json { "triggerId": "eyJh…", "view": { "type": "modal", "title": { "type": "plain_text", "text": "Pick a tea" }, "blocks": [] } } ``` ### PUT /api/apps/views/{id} Who: App token. Change an open dialog (`views.update`). With `hash`, 409 when it changed since. Request: ```json { "view": { "type": "modal", "title": { "type": "plain_text", "text": "Order created" }, "blocks": [] }, "hash": "a1b2c3d4e5f6" } ``` ### GET /api/workspaces/{w}/app-hooks Who: Admins. Every app's REST hooks, newest first, with the app's name and the channel's: what **Workspace settings → Integrations & apps → Integrations → Automation** lists. ### DELETE /api/workspaces/{w}/app-hooks/{id} Who: Admins. Stop an app's REST hook. The flow it belongs to stops hearing about the event. ### GET /api/apps/hooks Who: App token. The app's REST hooks: the events it asked to be sent to a URL, as Zapier, Make, n8n and Zoho Flow do. Needs no scope. Answer: ```json { "hooks": [ { "id": "01JE…", "event": "message.created", "url": "https://hooks.zapier.com/hooks/standard/…", "channelId": "01JB8R…", "source": "zapier", "createdAt": 1767000000000 } ] } ``` ### POST /api/apps/hooks Who: App token. Send one [event](#app-events) to a URL from now on (a REST hook). The app needs the event's scope. With `channelId`, only that channel's events; for messages, reactions and files the bot has to be in the channel, and it joins a public one by itself when the app has `channels:join`. Each delivery has the event's `type` and `data` as an outgoing webhook's do, plus `user`: who did it (`id`, `name`, `isBot`, and `email` when the app has `users:read`). It's signed with the app's signing secret. Answer `410` and the hook is deleted. 100 hooks per app. Request: ```json { "event": "message.created", "url": "https://hooks.zapier.com/hooks/standard/…", "channelId": "01JB8R…", "source": "zapier" } ``` Answer: ```json { "hook": { "id": "01JE…", "event": "message.created", "channelId": "01JB8R…", … } } ``` ### DELETE /api/apps/hooks/{id} Who: App token. Stop a REST hook. Answers 204. ### POST /hooks/commands/{token} Who: No token. An app's `response_url`: answer a slash command, message action or button press later, up to 30 minutes after. The body is a message like an incoming webhook's (`text`, `blocks`, `attachments`), with `"response_type": "in_channel"` to post it in the channel; otherwise only the person sees it. After a button press, `"replace_original": true` changes the message or answer the button was on, and `"delete_original": true` removes it. Answers `{ "ok": true }`. Request: ```json { "replace_original": true, "text": "Order 4512 packed by Marcus" } ``` ## Sign in with Freesbe Settings for letting people sign in to an app with their Freesbe account. The OpenID Connect endpoints themselves are at `https://id.freesbe.net`; see [Sign in with Freesbe](/docs/sign-in-with-freesbe/#for-developers). ### GET /api/workspaces/{w}/sign-in-apps Who: Admins. Every app people can sign in to with Freesbe, by name: the app, its Sign in with Freesbe settings, and when someone last signed in to it (`lastUsedAt`, null if nobody has). Answer: ```json { "issuer": "https://id.freesbe.net", "apps": [{ "app": { "id": "01JC2A…", "name": "Northwind Dashboard", "color": "#1D4ED8", … }, "client": { "clientId": "01JC2A…", "redirectUris": ["https://dashboard.northwind.example/auth/callback"], "who": "members", "requireMfa": true, … }, "lastUsedAt": 1792760520000 }] } ``` ### GET /api/workspaces/{w}/apps/{id}/sign-in Who: Admins. The app's Sign in with Freesbe settings, or `client: null` while it's off. The client id is the app's id; the secret shows only its last four characters. Answer: ```json { "issuer": "https://id.freesbe.net", "client": { "clientId": "01JC2A…", "redirectUris": ["https://dashboard.northwind.example/auth/callback"], "public": false, "who": "members", "requireMfa": false, "preapproved": false, "secretHint": "x9Qa", "people": 12, "notifyChannelId": null } } ``` ### PUT /api/workspaces/{w}/apps/{id}/sign-in Who: Admins. Turns it on or changes it. Return addresses must use https (http only for localhost); `public` apps may also use their own reverse-domain scheme and must use PKCE. `notifyChannelId` (a public or private channel, or null) is where the app's bot posts each sign-in. `requireMfa` (on unless you send false) makes people confirm it's them at every sign-in: a passkey, a phone approval or an authenticator app code from the last 5 minutes, or an emailed code for people with none of those; until they do, the sign-in page's request answers 403 `strong_mfa_required` (or `mfa_required`). The first time, the answer includes the client `secret`, shown once. `DELETE` turns it off. Request: ```json { "redirectUris": ["https://dashboard.northwind.example/auth/callback"], "who": "members", "requireMfa": true, "preapproved": false, "public": false, "notifyChannelId": "01JC2C…" } ``` ### POST /api/workspaces/{w}/apps/{id}/sign-in/secret Who: Admins. A new client secret, in `secret`; the old one stops working at once. ### GET /api/me/connected-apps Who: Signed in. Apps you've allowed to sign you in, most recently used first. `DELETE /api/me/connected-apps/{appId}` takes it back; the app asks again next time. Answer: ```json { "apps": [{ "appId": "01JC2A…", "name": "Northwind Dashboard", "color": "#1D4ED8", "workspaceName": "Northwind Traders", "scopes": ["openid", "profile", "email"], "createdAt": 1792540320000, "lastUsedAt": 1792760520000 }] } ``` ## AI agents The pages behind connecting AI tools (Claude, ChatGPT, code editors) to a workspace. The MCP server itself is at `https://api.freesbe.net/mcp` and its OAuth endpoints at `https://id.freesbe.net`; see [AI agents](/docs/ai-agents/#for-developers). Permissions are the scopes listed there, and the [tools](/docs/ai-agents/#tools) an agent gets depend on them: `search`, `list_channels`, `read_channel`, `read_thread`, `send_message`, `send_dm`, `add_reaction`, `find_people`, `check_presence`, `find_free_time`, `ask`, `get_answer`, `list_questions`, `cancel_question` and `get_connection`. Messages come back as plain text with their author, channel and a link; what an agent posts carries a context block naming the agent and the person it acts for. Where the workspace lets agents post as the person (`postAsPerson`), the message is the person's, its context block has `viaAgent` set to the agent's name, and messages agents read back carry `via_ai_agent`. ### GET /api/agent-connect/{id} Who: Signed in. The connect page: the agent asking, the permissions it wants, and the person's workspaces with whether each one lets it in (`allowed`, `approval` when an admin has to approve it first, `requested` while they do, `off`, or `blocked`) and which permissions it wouldn't get there (`withheld`). The id comes from the browser's redirect to `/connect/{id}`. Answer: ```json { "id": "01JC7A…", "agent": { "name": "Claude", "host": "claude.ai", "logoUrl": "https://claude.ai/logo.png" }, "redirectHost": "claude.ai", "scopes": ["channels:read", "channels:history", "chat:write"], "workspaces": [{ "id": "01JC2W…", "name": "Northwind Traders", "access": "allowed", "withheld": [] }] } ``` ### POST /api/agent-connect/{id} Who: Signed in. Allows the agent in a workspace, or not. The answer's `redirect` sends the browser back to the agent with a one-time code (`outcome: "connected"`), or with an error once admins have been asked to approve it (`"requested"`) or when the person said no (`"denied"`). Guests can't connect agents. Request: ```json { "workspaceId": "01JC2W…", "allow": true, "note": "For drafting release notes" } ``` Answer: ```json { "outcome": "connected", "redirect": "https://claude.ai/api/mcp/auth_callback?code=…&state=…" } ``` ### GET /api/me/agents Who: Signed in. The agents you connected, most recently used first. `status` is `active`, `waiting` for an admin's approval, or `approved` once they gave it (connect again from the agent). `DELETE /api/me/agents/{grantId}` disconnects one at once. Answer: ```json { "agents": [{ "grantId": "01JC7G…", "agent": { "name": "Claude", "host": "claude.ai", "logoUrl": null }, "workspaceId": "01JC2W…", "workspaceName": "Northwind Traders", "scopes": ["channels:read", "chat:write"], "status": "active", "createdAt": 1792540320000, "lastUsedAt": 1792760520000 }] } ``` ### GET /api/workspaces/{w}/agents Who: Admins. The workspace's agent settings, every agent anyone connected (with how many people, who is waiting for approval and their notes, and the permission ceiling), and the MCP server address to give people. Answer: ```json { "settings": { "policy": "open", "allowPrivate": false, "postAsPerson": false }, "mcpUrl": "https://api.freesbe.net/mcp", "agents": [{ "id": "01JC7B…", "agent": { "name": "Claude", "host": "claude.ai", "logoUrl": null }, "botUserId": "01JC7C…", "status": "allowed", "scopeCeiling": null, "people": 4, "waiting": [], "createdAt": 1792540320000, "lastUsedAt": 1792760520000 }] } ``` ### PUT /api/workspaces/{w}/agents Who: Admins. Changes who can connect agents (`off`, `approved` or `open`), whether they may read private channels and direct messages, and whether they post as the person who connected them, with a "via" line (`postAsPerson`, kept as it is when left out). Answers like `GET`. Request: ```json { "policy": "approved", "allowPrivate": false, "postAsPerson": true } ``` ### PATCH /api/workspaces/{w}/agents/{id} Who: Admins. Approves, allows or blocks an agent (`status`), or sets the most it may do here (`scopeCeiling`, a list of permissions, or `null` for any). Both apply to existing connections at once. `DELETE` removes the agent and all its connections from the workspace. Request: ```json { "status": "allowed", "scopeCeiling": ["channels:read", "channels:history", "chat:write"] } ``` ### GET /api/workspaces/{w}/agents/activity Who: Admins. What agents did here, newest first, 50 at a time: connections, approvals, blocks, removals and every tool call. Pass `next` back as `?before=` for older entries. Answer: ```json { "activity": [{ "id": "01JC7D…", "agentName": "Claude", "userId": "01JC2U…", "userName": "Maya Chen", "action": "tool", "tool": "get_connection", "ok": true, "detail": null, "at": 1792760520000 }], "next": null } ``` ### POST /api/agent-questions/{id}/answer Who: Members. Answers a question an agent asked you (the `agent_question` block on its message). `value` fits the question's `answer.type`: text, a day (`"2026-11-06"`), an ETA (`{ "date", "time" }`, time optional), the choice or list of choices, `true` or `false`, or a number. Only the person it's with can answer, once; `409` when it's no longer waiting. Request: ```json { "value": { "date": "2026-11-06", "time": "15:00" }, "note": "Waiting on the carrier pickup" } ``` ### POST /api/agent-questions/{id}/pass Who: Members. Says the question isn't yours to answer. It goes to `suggestUserId` if given, or the next person the agent named, or back to the agent as declined. Request: ```json { "suggestUserId": "01JC2U…" } ``` ### POST /api/agent-questions/{id}/later Who: Members. Can't say yet: you're asked again at 9:00 on your next working day. ## Webhooks Admins see the webhooks for public channels and the private channels they're in. See [incoming](#incoming) and [outgoing](#outgoing) webhooks above for the formats. ### GET /api/workspaces/{w}/webhooks Who: Admins. Every webhook you can see, newest first. ### POST /api/workspaces/{w}/webhooks Who: Admins. Create an incoming webhook (`kind`, `name`, `channelId`) or an outgoing one (`kind`, `name`, `url`, `events`, and `channelId` for one channel). The incoming `hookUrl` or outgoing `secret` is only in this answer. Answers 201. Request: ```json { "kind": "outgoing", "name": "Support bot", "url": "https://bot.northwind.example/freesbe", "events": ["message.created", "reaction.added"] } ``` Answer: ```json { "webhook": { "id": "01JBG3…", "kind": "outgoing", "name": "Support bot", "channelId": null, "url": "https://bot.northwind.example/freesbe", "events": ["message.created", "reaction.added"], "createdBy": "01JB8Q…", "createdAt": 1761200000000, "disabledAt": null }, "secret": "…" } ``` ### PATCH /api/workspaces/{w}/webhooks/{id} Who: Admins. Change `name`, `url`, `events`, `channelId`, or turn it off and on with `disabled`. Request: ```json { "disabled": false } ``` ### DELETE /api/workspaces/{w}/webhooks/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/webhooks/{id}/rotate Who: Admins. A new URL (incoming) or signing secret (outgoing). The old one stops working at once. ### POST /api/workspaces/{w}/webhooks/{id}/test Who: Admins. Send a `webhook.test` event (outgoing). Answers 204. ### GET /api/workspaces/{w}/webhooks/{id}/deliveries Who: Admins. The last 14 days of deliveries, newest first (outgoing): event, status, attempts, last HTTP code and error. ### POST /hooks/custom/{id}/{token} Who: No token. Post to an incoming webhook. `{id}` is the webhook's ID; the token is the secret. Older `/hooks/{token}` URLs keep working. Answers `ok` as plain text. ## Payment alerts Mercury, Stripe and Airwallex activity posted into a channel. See the [payment alerts guide](/docs/payment-alerts/). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/payment-alerts Who: Admins. Every payment alert you can see, newest first: `provider`, `channelId`, `kinds`, `minAmount`, whether it's `ready` (its signing secret is saved) and `lastEventAt`. ### POST /api/workspaces/{w}/payment-alerts Who: Admins. Connect an account: `provider` (`mercury`, `stripe` or `airwallex`), `channelId`, `kinds` (`money_in`, `money_out`, `balance`, `other`) and an optional `minAmount`. Mercury also takes `apiToken`, used once to register the webhook and not kept. Stripe and Airwallex answer with the `hookUrl` to paste into their dashboard, only in this answer. Answers 201. Request: ```json { "provider": "stripe", "channelId": "01J…", "kinds": ["money_in", "other"], "minAmount": 100 } ``` ### PATCH /api/workspaces/{w}/payment-alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, save the provider's signing `secret` (Stripe and Airwallex), or turn it off and on with `disabled`. Request: ```json { "secret": "whsec_…" } ``` ### DELETE /api/workspaces/{w}/payment-alerts/{id} Who: Admins. Disconnect it. Answers 204. ### POST /api/workspaces/{w}/payment-alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/finance/{provider}/{token} Who: No token. Where the provider (`mercury`, `stripe` or `airwallex`) delivers events, signed with `Mercury-Signature`, `Stripe-Signature`, or Airwallex's `x-timestamp` and `x-signature`. Answers 200 to signed events, posted or filtered out, 401 to a bad or old signature and 404 when the alert is off or gone. ## Commerce: Shopify and Amazon A store's orders posted into a channel. See the [Shopify and Amazon guide](/docs/commerce/). Alerts carry the order number, what was bought and the amounts, never the buyer's name, email or address. Admins see the stores for public channels and the private channels they're in. ### GET /api/workspaces/{w}/commerce Who: Admins. Every store you can see, newest first: `provider`, `channelId`, `kinds`, `minAmount`, whether it's `ready`, the `store` (the myshopify.com domain or Amazon's seller id), Amazon's `region`, `brokenAt` and `lastEventAt`, plus `amazonAvailable`. ### POST /api/workspaces/{w}/commerce Who: Admins. Connect a store: `provider` (`shopify` or `amazon`), `channelId`, `kinds` (`order`, `fulfilled`, `cancelled`, `refund`, `payout`; Shopify leaves out `payout`, Amazon `refund`), an optional `minAmount`, and for Amazon a `region` (`na`, `eu` or `fe`). Shopify answers with the `hookUrl` to paste into the store's webhook settings, only in this answer; Amazon with the `authorizeUrl` where the admin approves Freesbe in Seller Central. Answers 201. Request: ```json { "provider": "shopify", "channelId": "01J…", "kinds": ["order", "refund"], "minAmount": 50 } ``` ### PATCH /api/workspaces/{w}/commerce/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, save the key Shopify signs the store's webhooks with (`secret`), or turn it off and on with `disabled`. Request: ```json { "secret": "…" } ``` ### DELETE /api/workspaces/{w}/commerce/{id} Who: Admins. Disconnect it: Freesbe forgets the signing key or Amazon's token. Answers 204. ### POST /api/workspaces/{w}/commerce/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /api/workspaces/{w}/commerce/{id}/connect Who: Admins. Amazon only: a fresh `url` to approve Freesbe in Seller Central again, after Amazon stopped accepting it. ### POST /hooks/commerce/shopify/{token} Who: No token. Where a Shopify store delivers its order and refund webhooks, signed with `X-Shopify-Hmac-Sha256`. Answers 200 to signed deliveries, posted or filtered out, 401 to a bad signature and 404 when the store is off or gone. ## Kraken alerts A Kraken account's deposits, withdrawals, trades and balances posted into a channel. Freesbe reads the account with a read-only API key every minute, and every five minutes once it has been quiet for an hour. See the [Kraken guide](/docs/kraken/). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/kraken Who: Admins. Every Kraken account you can see, newest first: `channelId`, `kinds`, `minUsd`, `keyHint` (the key's last four characters), `balanceRules`, `summaryHour` and `summaryTimeZone`, `checkedAt`, `lastEventAt`, and `error` when Kraken refused the key. ### POST /api/workspaces/{w}/kraken Who: Admins. Connect an account: `apiKey` and `privateKey` (a key with Query Funds and Query Ledger Entries), `channelId`, `kinds` (`deposits`, `withdrawals`, `trades`, `other`), and optionally `minUsd`, up to 10 `balanceRules` (`asset`, `op` `below` or `above`, `amount`), `summaryHour` (0 to 23) and `summaryTimeZone`. Freesbe checks the key reads the account first and answers 400 when Kraken refuses it, or when the key is already connected. The private key is never sent back. Answers 201. Request: ```json { "apiKey": "…", "privateKey": "…", "channelId": "01J…", "kinds": ["deposits", "trades"], "minUsd": 100, "balanceRules": [{ "asset": "BTC", "op": "below", "amount": 0.5 }] } ``` ### PATCH /api/workspaces/{w}/kraken/{id} Who: Admins. Change any of the above, replace the key (`apiKey` and `privateKey` together), or turn it off and on with `disabled`. Turned back on, it carries on from then. Request: ```json { "summaryHour": 9, "summaryTimeZone": "America/New_York" } ``` ### DELETE /api/workspaces/{w}/kraken/{id} Who: Admins. Disconnect it and forget the key. Answers 204. ### POST /api/workspaces/{w}/kraken/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ## Crypto wallets Money in and out of public addresses on Bitcoin, Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Solana and Tron, posted into a channel. See the [Crypto wallets guide](/docs/crypto-wallets/). Admins see the wallets that post to public channels and the private channels they're in. ### GET /api/workspaces/{w}/crypto-wallets Who: Admins. Every wallet you can see, newest first: `network`, `address`, `name`, `channelId`, `directions`, `minUsd`, `balanceRules`, `checkedAt`, `lastEventAt`, and `error` when the network's data couldn't be read. Also `networks`, the ones this server can watch, and the workspace's daily `summary` (`channelId`, `hour`, `timeZone`) or null. ### POST /api/workspaces/{w}/crypto-wallets Who: Admins. Watch a wallet: `network` (`bitcoin`, `ethereum`, `base`, `arbitrum`, `optimism`, `polygon`, `bnb`, `avalanche`, `solana`, `tron`), its public `address`, a `name`, `channelId`, and optionally `directions` (`in`, `out`), `minUsd` and up to 5 `balanceRules` (`asset`: the network's coin, `USDC` or `USDT`; `op` `below` or `above`; `amount`). Answers 400 when the address isn't one for the network or is already watched. Answers 201. Request: ```json { "network": "tron", "address": "T…", "name": "Customer payments", "channelId": "01J…", "minUsd": 50, "balanceRules": [{ "asset": "USDT", "op": "below", "amount": 10000 }] } ``` ### PATCH /api/workspaces/{w}/crypto-wallets/{id} Who: Admins. Change the `name`, `channelId`, `directions`, `minUsd` or `balanceRules`, or turn it off and on with `disabled`. Turned back on, it carries on from then. The network and address stay as they are. Request: ```json { "directions": ["in"], "minUsd": 100 } ``` ### DELETE /api/workspaces/{w}/crypto-wallets/{id} Who: Admins. Stop watching the wallet. Answers 204. ### POST /api/workspaces/{w}/crypto-wallets/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### PUT /api/workspaces/{w}/crypto-wallets/summary Who: Admins. Set the daily card with every wallet's balances: `summary` with `channelId`, `hour` (0 to 23) and `timeZone`, or null to turn it off. Answers 204. Request: ```json { "summary": { "channelId": "01J…", "hour": 9, "timeZone": "America/New_York" } } ``` ## HubSpot The HubSpot app: a connected HubSpot account, its deal and contact alerts and link previews. See the [HubSpot guide](/docs/hubspot/). `/hubspot` and **Log to HubSpot** run like any app's [slash command and message action](#ref-apps). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/hubspot Who: Admins. Whether the server can connect HubSpot (`available`), the `connection` (`portalId`, `hubDomain`, `botUserId`, `unfurls`, `brokenAt` when HubSpot stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/hubspot/connect Who: Admins. The `url` of HubSpot's page that asks to allow Freesbe. Send the browser there; HubSpot sends it back to Freesbe, which finishes connecting and opens the app with `?hubspot=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/hubspot Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/hubspot Who: Admins. Disconnect: Freesbe forgets HubSpot's access and the app's command and action go away. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/hubspot/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`deal_created`, `deal_stage`, `deal_won`, `deal_lost`, `contact_created`) and an optional `minAmount` for deals. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 5000 } ``` ### PATCH /api/workspaces/{w}/hubspot/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/hubspot/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/hubspot/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/crm/hubspot Who: No token. Where HubSpot delivers webhook events for every connected account, signed with `X-HubSpot-Signature-v3` and `X-HubSpot-Request-Timestamp`. Answers 200 to signed events and posts them afterwards, and 401 to a bad or old signature. ## GitHub The GitHub app: Freesbe's GitHub App installed on an organization, the repositories channels subscribe to, and link previews. See the [GitHub guide](/docs/github/). `/github` and **Create GitHub issue** run like any app's [slash command and message action](#ref-apps). Admins see the subscriptions for public channels and the private channels they're in. ### GET /api/workspaces/{w}/github Who: Admins. Whether the server can install GitHub (`available`), the `connection` (`installationId`, `account`, `accountType`, `repositorySelection`, `botUserId`, `unfurls`, `brokenAt` when the app was removed or suspended in GitHub) or null, and the `subscriptions`. ### POST /api/workspaces/{w}/github/connect Who: Admins. The `url` of GitHub's page that installs the app. Send the browser there; GitHub sends it back to Freesbe, which checks the installation is one this person can reach and opens the app with `?github=connected`. Only the admin who started it can finish it, within 30 minutes. ### PATCH /api/workspaces/{w}/github Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/github Who: Admins. Disconnect: Freesbe forgets the installation and its subscriptions, and the app's command and action go away. The app stays installed in GitHub until someone removes it there. Answers 204. ### GET /api/workspaces/{w}/github/repos Who: Admins. The `repos` the installation can see (`fullName`, `private`), up to 300. ### POST /api/workspaces/{w}/github/subscriptions Who: Admins. Subscribe a channel to a repository: `channelId`, `repo` (`owner/repo`), and optional `features` (`pulls`, `reviews`, `issues`, `comments`, `commits`, `releases`, `deployments`, `workflows`; pull requests, issues, releases and deployments when left out), `labels` and `branch`. A channel already subscribed to the repository gets the features added. Answers 201. Request: ```json { "channelId": "01J…", "repo": "northwind-traders/storefront", "features": ["pulls", "releases"] } ``` ### PATCH /api/workspaces/{w}/github/subscriptions/{id} Who: Admins. Change `channelId`, `features`, `labels` or `branch`, or turn it off and on with `disabled`. Request: ```json { "labels": ["bug"] } ``` ### DELETE /api/workspaces/{w}/github/subscriptions/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/github/subscriptions/{id}/test Who: Admins. Post a sample pull request card into its channel. Answers 204. ### POST /hooks/dev-tools/github Who: No token. Where GitHub delivers webhook events for every installation, signed with `X-Hub-Signature-256`. Answers 200 to signed deliveries and posts them afterwards (each `X-GitHub-Delivery` once), and 401 to a bad signature. ## Jira The Jira app: a connected Jira Cloud site, the projects channels subscribe to, and link previews. See the [Jira guide](/docs/jira/). `/jira` and **Create Jira issue** run like any app's [slash command and message action](#ref-apps). Admins see the subscriptions for public channels and the private channels they're in. ### GET /api/workspaces/{w}/jira Who: Admins. Whether the server can connect Jira (`available`), the `connection` (`siteUrl`, `siteName`, `jiraUser` who Freesbe acts as, `botUserId`, `unfurls`, `brokenAt` when Atlassian stopped accepting its access) or null, and the `subscriptions`. ### POST /api/workspaces/{w}/jira/connect Who: Admins. The `url` of Atlassian's page that asks to allow Freesbe. Send the browser there; Atlassian sends it back to Freesbe, which finishes connecting and opens the app with `?jira=connected`. Only the admin who started it can finish it, within 30 minutes. ### PATCH /api/workspaces/{w}/jira Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/jira Who: Admins. Disconnect: Freesbe removes its webhook from the site and forgets the connection and its subscriptions, and the app's command and action go away. Answers 204. ### GET /api/workspaces/{w}/jira/projects Who: Admins. The `projects` the connected account can browse (`key`, `name`), up to 300. ### POST /api/workspaces/{w}/jira/subscriptions Who: Admins. Subscribe a channel to a project: `channelId`, `project` (its key), and optional `features` (`created`, `status`, `assigned`, `comments`; all but comments when left out) and `labels`. A channel already subscribed to the project gets the features added. Answers 201. Request: ```json { "channelId": "01J…", "project": "OPS", "features": ["created", "comments"] } ``` ### PATCH /api/workspaces/{w}/jira/subscriptions/{id} Who: Admins. Change `channelId`, `features` or `labels`, or turn it off and on with `disabled`. Request: ```json { "labels": ["customer"] } ``` ### DELETE /api/workspaces/{w}/jira/subscriptions/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/jira/subscriptions/{id}/test Who: Admins. Post a sample issue card into its channel. Answers 204. ### POST /hooks/dev-tools/jira/{token} Who: No token. Where the site's webhook delivers issue and comment events, to the connection's own secret address, which Freesbe registers with Jira itself. Answers 200 and posts afterwards (each `X-Atlassian-Webhook-Identifier` once), 404 to an unknown address, and 401 to a bad signature. ## GitLab The GitLab app: a connected GitLab.com account or self-managed server, the projects channels subscribe to, and link previews. See the [GitLab guide](/docs/gitlab/). `/gitlab` and **Create GitLab issue** run like any app's [slash command and message action](#ref-apps). Admins see the subscriptions for public channels and the private channels they're in. ### GET /api/workspaces/{w}/gitlab Who: Admins. Whether the server can connect GitLab.com (`available`), the `connection` (`instanceUrl`, `selfManaged`, `gitlabUser` who Freesbe acts as, `botUserId`, `unfurls`, `brokenAt` when GitLab stopped accepting its access) or null, the `subscriptions`, the `callbackUrl` a self-managed server's application needs as its redirect URI, and for a `private` server the `webhook` (`url`, `secret`) its admin adds to each project. ### POST /api/workspaces/{w}/gitlab/connect Who: Admins. The `url` of GitLab's page that asks to allow Freesbe. Leave the body empty for GitLab.com; for a self-managed server send its https `instanceUrl` and the `clientId` and `clientSecret` of the application made on it. For a private server Freesbe can't reach, send `instanceUrl` and `private: true`: it connects straight away and `url` is null. Otherwise send the browser there; GitLab sends it back to Freesbe, which finishes connecting and opens the app with `?gitlab=connected`. Only the admin who started it can finish it, within 30 minutes. Request: ```json { "instanceUrl": "https://gitlab.northwind.example", "clientId": "…", "clientSecret": "…" } ``` ### PATCH /api/workspaces/{w}/gitlab Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/gitlab Who: Admins. Disconnect: Freesbe takes its webhooks off the projects and forgets the connection and its subscriptions, and the app's command and action go away. Answers 204. ### GET /api/workspaces/{w}/gitlab/projects Who: Admins. The `projects` the connected account is a member of (`path`, `name`, `private`), most active first, up to 300. ### POST /api/workspaces/{w}/gitlab/subscriptions Who: Admins. Subscribe a channel to a project: `channelId`, `project` (its path), and optional `features` (`merge_requests`, `issues`, `comments`, `pushes`, `tags`, `releases`, `deployments`, `pipelines`; merge requests, issues, releases and deployments when left out), `labels` and `branch`. Freesbe adds its webhook to the project; `hookError` says why when GitLab refused (usually a missing Maintainer role). A channel already subscribed to the project gets the features added. Answers 201. Request: ```json { "channelId": "01J…", "project": "northwind/storefront", "features": ["merge_requests", "pipelines"] } ``` ### PATCH /api/workspaces/{w}/gitlab/subscriptions/{id} Who: Admins. Change `channelId`, `features`, `labels` or `branch`, or turn it off and on with `disabled`. Turning it on tries the project's webhook again. Request: ```json { "labels": ["bug"] } ``` ### DELETE /api/workspaces/{w}/gitlab/subscriptions/{id} Who: Admins. Delete it. The project's webhook goes when no channel follows the project any more. Answers 204. ### POST /api/workspaces/{w}/gitlab/subscriptions/{id}/test Who: Admins. Post a sample merge request card into its channel. Answers 204. ### POST /hooks/dev-tools/gitlab/{token} Who: No token. Where the projects' webhooks deliver, to the connection's own secret address, with its secret in `X-Gitlab-Token`. Answers 200 and posts afterwards (each `X-Gitlab-Event-UUID` once), 404 to an unknown address, and 401 to a wrong token. ## monday.com The monday.com app: a connected monday.com account, the boards channels subscribe to, and link previews. See the [monday.com guide](/docs/monday/). `/monday` and **Create monday.com item** run like any app's [slash command and message action](#ref-apps). Admins see the subscriptions for public channels and the private channels they're in. ### GET /api/workspaces/{w}/monday Who: Admins. Whether the server can connect monday.com (`available`), the `connection` (`accountUrl`, `accountName`, `mondayUser` who Freesbe acts as, `botUserId`, `unfurls`, `brokenAt` when monday.com stopped accepting its access) or null, and the `subscriptions`. ### POST /api/workspaces/{w}/monday/connect Who: Admins. The `url` of monday.com's page that asks to allow Freesbe. Send the browser there; monday.com sends it back to Freesbe, which finishes connecting and opens the app with `?monday=connected`. Only the admin who started it can finish it, within 30 minutes. ### PATCH /api/workspaces/{w}/monday Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/monday Who: Admins. Disconnect: Freesbe takes its webhooks off the boards and forgets the connection and its subscriptions, and the app's command and action go away. Answers 204. ### GET /api/workspaces/{w}/monday/boards Who: Admins. The `boards` the connected account can open (`id`, `name`), by name, up to 500. Subitem boards and docs are left out. ### POST /api/workspaces/{w}/monday/subscriptions Who: Admins. Subscribe a channel to a board: `channelId`, `boardId`, and optional `features` (`created`, `status`, `assigned`, `updates`; the first three when left out) and `statuses` (only status changes to these post). Freesbe adds its webhooks to the board. A channel already subscribed to the board gets the features added. Answers 201. Request: ```json { "channelId": "01J…", "boardId": "1001", "features": ["created", "status"], "statuses": ["Stuck", "Done"] } ``` ### PATCH /api/workspaces/{w}/monday/subscriptions/{id} Who: Admins. Change `channelId`, `features` or `statuses`, or turn it off and on with `disabled`. Request: ```json { "statuses": [] } ``` ### DELETE /api/workspaces/{w}/monday/subscriptions/{id} Who: Admins. Delete it. The board's webhooks go when no channel follows the board any more. Answers 204. ### POST /api/workspaces/{w}/monday/subscriptions/{id}/test Who: Admins. Post a sample item card into its channel. Answers 204. ### POST /hooks/projects/monday/{token} Who: No token. Where the boards' webhooks deliver new items, column changes and updates, to the connection's own secret address, which Freesbe gives monday.com itself. Answers monday.com's `challenge` when a webhook is made, otherwise 200 and posts afterwards (each trigger once), 404 to an unknown address, and 401 to a bad signature. ## Salesforce The Salesforce app: a connected Salesforce org, its opportunity, lead and case alerts and link previews, on the plans that include it. See the [Salesforce guide](/docs/salesforce/). `/salesforce` and **Log to Salesforce** run like any app's [slash command and message action](#ref-apps). Alerts come from Freesbe checking the org every five minutes, so there's no webhook. Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/salesforce Who: Admins. Whether the server can connect Salesforce (`available`), whether the workspace's `plan` includes it (`included`), the `connection` (`orgId`, `orgName`, `instanceUrl`, `sandbox`, `botUserId`, `unfurls`, `brokenAt` when Salesforce stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/salesforce/connect Who: Admins. The `url` of Salesforce's page that asks to allow Freesbe, at test.salesforce.com with `sandbox: true`. Send the browser there; Salesforce sends it back to Freesbe, which finishes connecting and opens the app with `?salesforce=connected`. Only the admin who started it can finish it, within 15 minutes. 403 when the plan doesn't include Salesforce. Request: ```json { "sandbox": false } ``` ### PATCH /api/workspaces/{w}/salesforce Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/salesforce Who: Admins. Disconnect: Freesbe forgets Salesforce's access and the app's command and action go away. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/salesforce/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`opportunity_created`, `opportunity_stage`, `opportunity_won`, `opportunity_lost`, `lead_created`, `case_created`) and an optional `minAmount` for opportunities. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["opportunity_created", "opportunity_won"], "minAmount": 10000 } ``` ### PATCH /api/workspaces/{w}/salesforce/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/salesforce/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/salesforce/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ## Customer orders A customer's orders from the workspace's WooCommerce store, next to their customer inbox conversation and on their phone calls. See the [customer orders guide](/docs/customer-orders/). Freesbe asks the store each time and keeps no orders. Lookups answer `store: null` until an admin sets the store up, and `customer: null` when the store doesn't know them. Apps can't call these. ### GET /api/workspaces/{w}/order-lookup Who: Admins. The store customers are looked up in, or `lookup: null`: `store`, `storeUrl` and the store's `lastError`, if it refused the last lookup. The API key never comes back. ### PUT /api/workspaces/{w}/order-lookup Who: Admins. Set up the store, or replace its key: `store` (`woocommerce`), `storeUrl` (https), and a REST API key with Read permission as `consumerKey` and `consumerSecret`. Freesbe checks the key with the store first and answers 400 with the store's reason when it doesn't work. Request: ```json { "store": "woocommerce", "storeUrl": "https://shop.example", "consumerKey": "ck_…", "consumerSecret": "cs_…" } ``` ### DELETE /api/workspaces/{w}/order-lookup Who: Admins. Stop looking customers up and forget the key. Answers 204. ### POST /api/workspaces/{w}/order-lookup/test Who: Admins. Look someone up by `email` or `phone`, to see what the team would see. Request: ```json { "email": "customer@example.com" } ``` ### GET /api/inbox/conversations/{id}/orders Who: Members. The customer's orders, found by the email or phone number of the conversation's customer: `store`, `customer` (`orderCount`, `totalSpent`, `currency`, `since`) and up to five `orders`, newest first, each with `name`, `status`, `total`, `items`, `tracking` and `url`. For people who can see the conversation's channel. Answers 502 when the store doesn't answer. ### GET /api/workspaces/{w}/phone/caller-orders?number={number} Who: Members, not guests. The same, for a caller, found by their phone number. ## Customer inbox Customers write in from a web chat on the workspace's site, and each conversation is a thread in a channel: the inbox bot's card starts it, the customer's messages are replies under their name (`fromCustomer`), and the team's replies in that thread go back to the customer. Send a reply with `note: true` ([post a message](#ref-messages)) to keep it inside the team. People see the conversations of the channels they can see. See the [customer inbox guide](/docs/customer-inbox/). Apps can't use these endpoints. ### GET /api/workspaces/{w}/inbox/sources Who: Admins. Where customers write in from: each source's `id`, `kind` (`webchat`, `email`, `sms`, `whatsapp` or `telegram`), `name`, `channelId`, `enabled`, `testMode`, for a web chat its `webchat` settings with the public `key`, the `snippet` to paste on the site and a `pageUrl` with the chat on its own, for an email inbox its `email` settings with the `address` to forward to, for SMS its `sms.number`, and for WhatsApp its `whatsapp` settings: the `number`, whether it's `connected` to a WhatsApp Business account, and that account's `verifiedName`, and for Telegram its `telegram` settings: the `name` and `username` customers see, the `link` (https://t.me/…) to message it, and whether customer `chats` come in. `available` lists the kinds this server can add now, and `live` the kinds that can leave test mode (`sms` once texting customers is on, `whatsapp` once WhatsApp is set up on the server). `whatsappIncluded` and `telegramIncluded` say whether the workspace's plan includes WhatsApp and Telegram (Business and up), and `chatBrandingIncluded` whether a web chat can hide “Chat by Freesbe” (Corporate and up). ### POST /api/workspaces/{w}/inbox/sources Who: Admins. Add a web chat, an email inbox, an SMS inbox or a WhatsApp inbox: a `kind`, a `name`, the `channelId` new conversations go to (not a DM), and optional settings. A web chat takes `webchat`: `title`, `greeting`, `color` (#rrggbb), `askEmail`, `questions` (up to 6, asked before the chat: each `{ id, kind, label, required, choices }`, `kind` one of `text`, `long_text`, `phone` or `choice`, and a `choice` needs at least two `choices`), `logo` (`workspace`, `custom` or `none`), `hours` (`{ mode, own }`: `mode` is `always`, `workspace` for the phone system's business hours, or `own` for the chat's `own` week `{ timezone, days: [{ day, open, close }] }`, `day` 0 for Sunday and times `HH:MM`), `showAvailable` (show who's online, and be away when nobody is), `busyReply` and `noAgentReply` (automatic replies `{ enabled, afterMin, text }`, the second after the first, 1 to 120 minutes after the customer's first unanswered message), `awayMessage`, `hideBranding` (leave out “Chat by Freesbe”, on plans that include it: 403 `forbidden` otherwise), `popup` (open by itself once a visit: `{ enabled, afterPages, afterSeconds }`, 1 to 20 pages and 0 to 300 seconds), `chime` (`none`, `pop`, `ding`, `bell` or `bubble`, played when it opens by itself and for replies while it's closed), `rememberDays` (how long a returning visitor sees their conversation, counted from their last message: `0` for this visit only, `1`, `7`, `30` (the default) or `90`; after that their token answers 401), `animation` (`none`, `bounce`, `wiggle` or `pulse`: the button moves twice a few seconds in), `teaser` (`{ enabled, text, afterSeconds }`, a line beside the button once a visit, up to 120 characters and 0 to 300 seconds; left out while `popup` is on), `launcher` (`{ side, bottom, style, label, hideOnPhones, hideOn }`: `side` `left` or `right`, `bottom` 0 to 200 pixels, `style` `icon` or `label` with `label` as its words, and `hideOn` up to 20 pieces of addresses where the chat stays off), `language` (`auto` for the visitor's browser, or one of the app's languages, like `he`: the chat's own words), `callLinkId` (one of the workspace's Call us links, opened by a phone in the chat's header while it takes calls, or null; 400 `invalid` for another workspace's link), `collectDetails` (visitor details for the team, on by default; off keeps only the visitor's country), and `sites`, the websites that may show it (any when empty). An email inbox takes `email`: `replyTo`, the team's own address replies ask customers to answer, and `fromName`, the name replies come from. An SMS inbox takes `sms`: the `number` customers text, with its country code; it starts in test mode. A WhatsApp inbox takes `whatsapp`: the WhatsApp Business `number`, with its country code; it starts in test mode too, and needs a plan that includes WhatsApp (403 `forbidden` otherwise). Telegram takes `telegram.token`, the key Telegram gave the business's bot: Freesbe checks it with Telegram and starts taking the bot's messages, or answers 400 and keeps nothing. One Telegram per workspace, on a plan that includes it. Answers 201. Request: ```json { "kind": "webchat", "name": "Main website", "channelId": "01J…", "webchat": { "color": "#e8590c", "sites": ["www.northwind.example"] } } ``` ### PATCH /api/workspaces/{w}/inbox/sources/{id} Who: Admins. Change its `name`, `channelId`, `webchat`, `email`, `sms` or `whatsapp` settings, or turn it off and on with `enabled`. A web chat that's off disappears from the site; an email inbox that's off bounces mail. For SMS, `testMode: false` takes it live, texting customers for real (only when `live` lists `sms`, and only on one of the workspace's own phone numbers); `testMode: true` takes it back. For WhatsApp, `testMode: false` needs the inbox connected first ([connect WhatsApp](#ref-inbox)), and a connected inbox keeps its number. For Telegram, `telegram.chats: false` stops customers' messages coming in (shared channels keep posting). `telegram.callLinkId` picks the [Call us link](/docs/phone/#call-us-links) customers get when they send /call (null for none). Deleting a Telegram inbox disconnects the bot. Messenger and Instagram inboxes aren't added here: [connect them with Facebook](#ref-inbox); deleting one disconnects it, and Freesbe stops listening to the Page once neither is left. Request: ```json { "enabled": false } ``` ### POST /api/workspaces/{w}/inbox/sources/{id}/test Who: Admins. Only for an inbox in test mode: a made-up customer's `text`, from the number in `from` (a made-up one when left out), with up to 10 of your uploaded `fileIds` as photos or files they sent. It lands like a real one, and replies to it are kept with `test: true` instead of being sent. Answers the conversation and its deliveries. Request: ```json { "text": "Are you open on Saturday?" } ``` ### GET /api/workspaces/{w}/inbox/sources/{id}/telegram/shares Who: Admins. A Telegram inbox's shared channels and where they can go: `shares`, each with its `id`, the Freesbe `channelId`, the Telegram `chat` it posts to, `twoWay` (a group's messages come back into the channel) and `enabled`; and `chats`, the Telegram channels and groups the bot was added to, each with its `id`, `title`, `kind` (`channel` or `group`), `members` and `canPost` (an admin who can post, for a channel). Answer: ```json { "shares": [ { "id": "01J…", "channelId": "01J…", "chat": { "id": "-1001…", "title": "Northwind Traders News", "kind": "channel", "members": 4210, "canPost": true }, "twoWay": false, "enabled": true, … } ], "chats": [ … ] } ``` ### POST /api/workspaces/{w}/inbox/sources/{id}/telegram/shares Who: Admins. Shares a channel on Telegram: the `channelId` and the `chatId` of one of `chats` the bot can post in. Posts by people in the channel then go out from the bot, and edits and deletes follow. `twoWay` (groups only) brings what people write in the group into the channel, and thread replies go both ways. Up to 50 per workspace. Answers 201 with the share. Request: ```json { "channelId": "01J…", "chatId": "-1001…", "twoWay": true } ``` ### PATCH /api/workspaces/{w}/inbox/sources/{id}/telegram/shares/{share} Who: Admins. Pauses or resumes a share with `enabled`, or changes `twoWay`. Request: ```json { "enabled": false } ``` ### DELETE /api/workspaces/{w}/inbox/sources/{id}/telegram/shares/{share} Who: Admins. Stops sharing the channel. What's already on Telegram stays. Answers 204. ### POST /api/workspaces/{w}/inbox/sources/{id}/whatsapp/connect Who: Admins. Starts connecting a WhatsApp inbox to the business's WhatsApp Business account: answers the `url` of Meta's sign-up to send the admin to. Meta sends them back to Freesbe, which finds the inbox's number in the account they picked and takes the inbox live. Only when `live` lists `whatsapp`; the link works for 15 minutes, for the admin who started it. ### POST /api/workspaces/{w}/inbox/meta/connect Who: Admins. Starts connecting the business's Facebook Page for Messenger, Instagram or both: the `channelId` new conversations go to (not a DM) and `kinds`, `messenger`, `instagram` or both. Answers the `url` of Facebook's window to send the admin to. Facebook sends them back to Freesbe, which makes a Messenger inbox for the Page and an Instagram inbox for the Instagram professional account linked to it (or connects the workspace's existing ones again), and they take messages straight away. When the admin manages more than one Page, they come back with `meta=pick&meta-pick=` in the address: [pick the Page](#ref-inbox) with it. Only when `available` lists `messenger`, on a plan that includes Messenger and Instagram (403 `forbidden` otherwise). The link works for an hour, for the admin who started it. One Page feeds one workspace. Request: ```json { "channelId": "01J…", "kinds": ["messenger", "instagram"] } ``` Answer: ```json { "url": "https://www.facebook.com/v21.0/dialog/oauth?…" } ``` ### GET /api/workspaces/{w}/inbox/meta/picks/{id} Who: Admins. The Facebook Pages the admin can connect after Facebook's window, for an hour and only for that admin: each Page's `id`, `name` and linked `instagram` account (its `username`, or null), and the `kinds` they asked for. Answer: ```json { "pages": [ { "id": "1029…", "name": "Northwind Traders", "instagram": { "username": "northwindtraders" } } ], "kinds": ["messenger", "instagram"] } ``` ### POST /api/workspaces/{w}/inbox/meta/picks/{id} Who: Admins. Connects the Page with `pageId`, one of the pick's `pages`. 400 when it has no Instagram account and Instagram was asked for, or when it's already connected to another workspace. Answers 204. Request: ```json { "pageId": "1029…" } ``` ### GET /api/workspaces/{w}/inbox/sources/{id}/whatsapp/templates Who: Members, not guests. A WhatsApp inbox's message templates, from its WhatsApp Business account, approved ones first: each one's `id`, `name`, `language`, `category` (`UTILITY`, `MARKETING` or `AUTHENTICATION`), `status` (`approved`, `pending`, `rejected`, `paused` or `disabled`), `header`, `body` with blanks `{{1}}`, `{{2}}`…, `footer`, how many `params` it takes, whether Freesbe can fill it (`supported`: not with a picture header, header blanks or a link button that changes) and Meta's `rejectedReason`. Until the inbox is connected, `sample` is true and three made-up templates stand in. `manageUrl` opens the account's templates in WhatsApp Manager. Answer: ```json { "templates": [ { "id": "1234…", "name": "order_update", "language": "en_US", "category": "UTILITY", "status": "approved", "body": "Hi {{1}}, your order {{2}} is on its way.", "params": 2, "supported": true, … } ], "sample": false, "manageUrl": "https://business.facebook.com/wa/manage/message-templates/?waba_id=…" } ``` ### POST /api/workspaces/{w}/inbox/sources/{id}/whatsapp/templates Who: Admins. Sends a new template to Meta for review: a lowercase `name` with underscores, a `language` code like `en_US`, a `category` (`UTILITY` or `MARKETING`), the `body` with blanks `{{1}}`, `{{2}}`… numbered in order, an example for each blank in `examples`, and an optional `footer`. The inbox has to be connected. Answers 201 with the template, `pending` until Meta approves it. Request: ```json { "name": "restock_alert", "language": "en_US", "category": "UTILITY", "body": "Hi {{1}}, {{2}} is back in stock. Reply to order.", "examples": ["Dana", "Darjeeling First Flush"] } ``` ### POST /api/workspaces/{w}/inbox/sources/{id}/whatsapp/start Who: Members, not guests. Write to a customer first, with an approved `template` (`name`, `language` and the `params` that fill its blanks), to their WhatsApp number `to` with its country code, and an optional `name`. It goes into their open conversation from this inbox, or starts a thread in the inbox's channel, and shows there as your message. You have to be able to post in that channel. Answers 201 with the conversation and its deliveries; in test mode it's kept with `test: true`. Request: ```json { "to": "+44 7700 900123", "name": "Oliver Grant", "template": { "name": "order_update", "language": "en_US", "params": ["Oliver", "NW-48240"] } } ``` ### DELETE /api/workspaces/{w}/inbox/sources/{id} Who: Admins. Delete it, with its conversations. Their threads stay in the channel. Answers 204. ### PUT /api/workspaces/{w}/inbox/sources/{id}/logo Who: Admins. A web chat's own logo: the picture is the body (PNG, JPEG, GIF or WebP, with its `Content-Type`). Sets `webchat.logo` to `custom` and returns the `source`, with the picture at `webchat.customLogoUrl`. ### DELETE /api/workspaces/{w}/inbox/sources/{id}/logo Who: Admins. Remove a web chat's own logo. A chat that showed it goes back to the workspace logo. Returns the `source`. ### GET /api/workspaces/{w}/inbox Who: Members, not guests. The `sources` you can see, a page of `conversations` (newest change first) and the `counts` of open ones: all, yours and nobody's. Pick a list with `?view=open|mine|unassigned|closed`, one source with `?source=`, one person's with `?assignee=`, and page with `?before=`. `?sort=newest|oldest|waiting` orders by when conversations were opened, or puts the customers who have waited longest for a reply first; those pages with `?offset=` instead of `before`. When the person on an open conversation leaves the workspace or its channel, it goes back to nobody and `previousAssigneeId` says who had it, until someone takes it. Answer: ```json { "sources": [ … ], "conversations": [ { "id": "01J…", "status": "open", "assigneeId": null, "contact": { "name": "Jordan Fields", "email": "jordan@contoso.example" }, "rootMessageId": "01J…", … } ], "hasMore": false, "counts": { "open": 3, "mine": 1, "unassigned": 2 } } ``` ### GET /api/inbox/conversations/{id} Who: Members, not guests. One conversation, and the `deliveries` of the replies that went to the customer (`messageId`, `status`, `error`). Thread replies without one were notes. For a web chat, `visitor` has the visitor details: `country` (two letters, null when unknown), `city`, `region`, `timeZone`, the `page` they're on and this visit's `pages` (`{ url, title, at }`, the last 20, addresses without query strings), `referrer`, `campaign` tags (`source`, `medium`, `campaign`, `term`, `content`), `earlierChats`, `browser`, `os`, `device` (`computer`, `phone` or `tablet`), `language`, `screen`, `ip`, and `limited` when the chat keeps only the country. Null for other channels. A web chat conversation's `visitor` in lists and on its card has `country`, `city`, `timeZone` and `page`. ### PATCH /api/inbox/conversations/{id} Who: Members, not guests. Close or reopen it with `status`, or give it to someone in its channel with `assigneeId` (null: nobody). The inbox bot says so in the thread and mentions the new owner. Request: ```json { "assigneeId": "01JB8R…" } ``` ### POST /api/inbox/conversations/{id}/suggest Who: Members, not guests. An answer to the customer the AI drafts from the conversation, its notes and the saved replies, as `text`. Nothing is sent. Counts toward the workspace's AI allowance. ### POST /api/inbox/conversations/{id}/agent Who: Members, not guests. Where the [AI agent](/docs/customer-inbox/#ai-agent) answers in the conversation's inbox: `action: "pause"` stops it answering there, and `action: "resume"` lets it answer again (it answers the customer's last message if nobody has). A conversation's `ai` says how it stands: `state` (`answering`, `handed_off` or `paused`) and the agent's `name`; null where the agent doesn't answer. Answers the conversation and its deliveries. Request: ```json { "action": "resume" } ``` ### POST /api/inbox/conversations/{id}/block Who: Members, not guests. [Blocks](/docs/customer-inbox/#block-a-visitor) a web chat conversation's visitor from all the workspace's web chats, and with `ip: true` their IP address too for `days` (1, 7 or 30; null until someone unblocks it). Closes the conversation and notes it in the thread. 400 for other kinds of conversation. Answers the conversation and its deliveries. Request: ```json { "ip": true, "days": 30 } ``` ### GET /api/workspaces/{w}/inbox/blocks Who: Admins. Who's blocked from the workspace's web chats: `visitors` (`id`, `contactId`, `name`, `email`, `conversationId`, `ip` as `{ shown, expiresAt }` or null when only the visitor is blocked, `blockedBy` and `createdAt`), the IP addresses and `ranges` admins blocked (`id`, `cidr`, `blockedBy`, `createdAt`, `expiresAt`), and the blocked `countries` (two-letter codes, `XX` for an unknown location such as a VPN or Tor). Answer: ```json { "visitors": [{ "id": "01J…", "contactId": "01J…", "name": "Rex", "email": "rex@spam.example", "conversationId": "01J…", "ip": { "shown": "192.0.2.61", "expiresAt": 1795000000000 }, "blockedBy": "Sam Chen", "createdAt": 1792400000000 }], "ranges": [{ "id": "01J…", "cidr": "198.51.100.0/24", "blockedBy": "Maya Patel", "createdAt": 1792100000000, "expiresAt": null }], "countries": ["XX"] } ``` ### POST /api/workspaces/{w}/inbox/blocks Who: Admins. Blocks an IP address or range (`cidr`, IPv4 or IPv6, no wider than a /8 or an IPv6 /16) from the web chats until it's unblocked. 400 when it isn't one. Answers the list, 201. Request: ```json { "cidr": "203.0.113.0/24" } ``` ### DELETE /api/workspaces/{w}/inbox/blocks/{id} Who: Admins. Unblocks a visitor (and their IP address) or an address or range. Answers the list. ### PUT /api/workspaces/{w}/inbox/block-countries Who: Admins. The countries where the web chats don't open, replacing the list: two-letter codes, and `XX` for an unknown location. Answers the list. Request: ```json { "countries": ["XX"] } ``` ### GET /api/workspaces/{w}/inbox/agent Who: Admins. The [AI agent](/docs/customer-inbox/#ai-agent)'s `settings` (`enabled`, `name`, `instructions`, `model` (a model `id`, or `auto`), `strongModel`, `when` (`always`, `outside_hours` or `after_wait`), `waitMinutes`, `sourceIds`, `handoffTopics`, `maxAnswers` and `guardrails`: `maskPii`, `safety`, `stayOnTopic`, `neverPromise`), its `knowledge` (web pages, notes, files and canvases: `id`, `kind` (`url`, `text`, `file` or `canvas`), `title`, `url`, `chars`, `status`, `error`, and `cabinetItemId` for a file from Files or `channelId` for a canvas), the `models` to pick from (`id`, `name`, `maker`, `runsIn`, `weight`, `tier`, `available`), the inbox `sources`, whether the workspace has the add-on (`included`), this month's conversations `used` of `limit`, and the latest `events`: hand-offs and what the guardrails stopped (never the customer's words). ### PUT /api/workspaces/{w}/inbox/agent Who: Admins. Change any of the settings above. Turning it on needs the AI agent add-on (400 `invalid` otherwise), and a model whose provider isn't set up on the server can't be picked. Answers like GET. Request: ```json { "enabled": true, "sourceIds": ["01J…"], "when": "outside_hours", "handoffTopics": ["Refunds"] } ``` ### POST /api/workspaces/{w}/inbox/agent/knowledge Who: Admins. Something for the AI agent to answer from: a page of the website (`kind: "url"` and its public `url`, which Freesbe reads now), a note (`kind: "text"`, a `title` and the `text`, up to 20,000 characters), a file in Files (`kind: "cabinet"` and its `itemId`; it reads the newest version) or a canvas (`kind: "canvas"` and its `canvasId`; it reads it again when it's edited, at most every 10 minutes). Only a file or canvas the admin can open. Up to 50 in all. Answers 201 with the agent, like GET. Request: ```json { "kind": "url", "url": "https://northwind.example/shipping" } ``` ### POST /api/workspaces/{w}/inbox/agent/knowledge/file Who: Admins. A file for the AI agent to learn from, as the request's body, named by `?name=`: PDF, Word (.docx), text, Markdown, HTML or CSV, up to 10 MB. It keeps the file's words, not its pictures. Kept in the workspace's region and deleted when it's removed from the agent. Answers 201 with the agent, like GET; a file it couldn't read is listed with its `error`. ### GET /api/workspaces/{w}/inbox/agent/canvases Who: Admins. The canvases the admin can pick for the AI agent: those of public channels and private channels they're in (`id`, `channelId`, `name`, `isPrivate`, `updatedAt`). ### POST /api/workspaces/{w}/inbox/agent/knowledge/{id}/refresh Who: Admins. Reads a web page, file or canvas in the agent's knowledge again. Answers the agent, like GET. ### DELETE /api/workspaces/{w}/inbox/agent/knowledge/{id} Who: Admins. Removes something from the agent's knowledge. An uploaded file is deleted with it; a file in Files stays there. Answers the agent, like GET. ### POST /api/workspaces/{w}/inbox/agent/try Who: Admins. Ask the AI agent a `question` as a customer would, with its saved settings. Nothing is sent to anyone. Answers its `answer` (null when it would hand off), `handoff`, the `reason`, the `model` that answered, the scoring step's `difficulty` (1 to 3, in automatic mode) and the titles of the `sources` it used. Request: ```json { "question": "Do you ship to Canada?" } ``` ### POST /api/inbox/conversations/{id}/whatsapp/template Who: Members, not guests. Sends an approved WhatsApp template into the conversation (its `name`, `language` and the `params` that fill its blanks): how to reach a customer more than 24 hours after their last message, when WhatsApp doesn't take a free-form reply. It shows in the thread as your message. Answers the conversation and its deliveries. Request: ```json { "name": "follow_up", "language": "en_US", "params": ["Dana", "your order NW-48231"] } ``` ### GET /api/workspaces/{w}/inbox/replies Who: Members, not guests. The team's saved replies: `id`, `title`, `body`. ### POST /api/workspaces/{w}/inbox/replies Who: Members, not guests. Save a reply with a `title` and a `body`. Answers 201. Request: ```json { "title": "Shipping times", "body": "We ship across the EU in 3 to 5 days." } ``` ### PATCH /api/workspaces/{w}/inbox/replies/{id} Who: Members, not guests. Change its `title` and `body`. ### DELETE /api/workspaces/{w}/inbox/replies/{id} Who: Members, not guests. Delete it: admins, and whoever saved it. Answers 204. ### GET /api/workspaces/{w}/inbox/message-log?from=2026-10-01&to=2026-10-31&tz=America/New_York&kind=&direction=&status=&q=&page=1&limit=50 Who: Admins. Every message to and from customers on the workspace's inboxes for a range of dates (counted in `tz`, a year at most), newest first, with how it went. `kind` is `webchat`, `email`, `sms` or `whatsapp`; `direction` is `in` or `out`; `status` is `received`, `sent`, `delivered`, `read` or `failed`; `q` matches the customer's number, address or name. `preview` is the start of the message, or `null` when it's in a private channel the admin isn't in. `limit` is up to 500 a page; `more` says another `page` has older messages. Answer: ```json { "messages": [{ "id": "01J…", "at": 1793536200000, "kind": "sms", "sourceName": "Support line", "direction": "out", "contact": { "name": "Dana Ruiz", "address": "+14155550142" }, "authorId": "01J…", "status": "failed", "error": "Number can't receive texts", "test": false, "preview": "Your order ships tomorrow.", "conversationId": "01J…", "channelId": "01J…", "messageId": "01J…", "rootMessageId": "01J…" }], "more": false } ``` ### GET /api/chat/{key} Who: No token. The web chat's `title`, `greeting`, `color`, whether it asks for an email (`askEmail`), the `questions` it asks before the chat, and the header's `logoUrl` (null for none). Also whether it's `open` now (inside its hours, and with someone online when it shows who is), when it `opensAt` next (null when it doesn't say) in which `timezone`, the first names of who's `available` (null when it doesn't show them), the `awayMessage` shown while it isn't open, and whether it hides “Chat by Freesbe” (`hideBranding`). `blocked` is true when the workspace blocked this visitor's IP address or country, and `checkKey` is the bot check's site key when starting a chat needs one (null when it doesn't). 404 when it's off or gone. ### POST /api/chat/{key}/start Who: No token. A visitor's first message (`text`, with `name` and `email` when the chat asks, and `answers` to its questions as `[{ questionId, answer }]`; 400 when a required one is missing or a `choice` answer isn't one of its choices): starts a conversation and returns the visitor's secret as `visitorToken`, to send as `Authorization: Bearer` from then on, and their `messages`. Optional `visit` tells the team about the visit, when the chat collects visitor details: `pages` (`[{ url, title, at }]`, oldest first), `referrer`, `campaign` (`{ source, medium, campaign, term, content }`), `timeZone`, `language` and `screen` (like `1440×900`). Addresses that aren't http(s) are dropped, and query strings are cut. When the chat gave a `checkKey`, `check` is the token from the bot check; 403 `check_failed` when it doesn't pass. 403 `blocked` for a blocked visitor (every visitor call answers that once they're blocked), and 429 past 5 new chats from one IP address in 10 minutes or 100 new chats an hour for the chat. Answers 201. ### GET /api/chat/{key}/messages Who: No token. With the visitor's token: their side of the latest conversation (`from` is `visitor` or `team`; notes never show) after `?after=`, whether the team `closed` it, and whether the visitor gave an email (`hasEmail`). Automatic replies carry `auto`: `busy` or `no_agent`. With `&typing=1`, `typing` lists the first names of the team members typing a reply right now (never a note), and is left out when nobody is. ### POST /api/chat/{key}/details Who: No token. With the visitor's token: their `email` and optional `name`, when they give them after the chat started (the second automatic reply asks). Answers 204. ### POST /api/chat/{key}/messages Who: No token. With the visitor's token: another message (`text`, and optionally `visit` as on start). After the team closed the conversation, it starts a new one. ### POST /api/chat/{key}/visit Who: No token. With the visitor's token: the `visit` (as on start) when a page with the chat loads while they're chatting, so the team sees the page they're on. New pages are added to the open conversation's. Answers 204. ### POST /api/chat/{key}/typing Who: No token. With the visitor's token: the visitor is typing. The team sees it in the conversation for a few seconds; send it again every 3 seconds or so while they type. Nothing is saved. Answers 204. ### POST /api/chat/{key}/human Who: No token. With the visitor's token: the visitor asks for a person. Where the AI agent answers, it hands the conversation to the team. Messages the agent wrote come back with `ai: true`. Answers 204. ## Pipedrive The Pipedrive app: a connected Pipedrive company, its deal and lead alerts and link previews. See the [Pipedrive guide](/docs/pipedrive/). `/pipedrive` and **Log to Pipedrive** run like any app's [slash command and message action](#ref-apps). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/pipedrive Who: Admins. Whether the server can connect Pipedrive (`available`), the `connection` (`companyId`, `companyName`, `companyUrl`, `botUserId`, `receiving` when Pipedrive sends changes to Freesbe, `unfurls`, `lastEventAt`, `brokenAt` when Pipedrive stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/pipedrive/connect Who: Admins. The `url` of Pipedrive's page that asks to allow Freesbe. Send the browser there; Pipedrive sends it back to Freesbe, which finishes connecting and opens the app with `?pipedrive=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/pipedrive Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/pipedrive Who: Admins. Disconnect: Freesbe removes its webhooks from Pipedrive, forgets Pipedrive's access, and the app's command and action go away. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/pipedrive/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`deal_created`, `deal_stage`, `deal_won`, `deal_lost`, `lead_created`) and an optional `minAmount` for deals. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 } ``` ### PATCH /api/workspaces/{w}/pipedrive/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/pipedrive/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/pipedrive/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/crm/pipedrive/{token} Who: No token. Where the webhooks Freesbe adds in Pipedrive deliver deal and lead changes, signed in with HTTP Basic auth and a password only Freesbe knows. Answers 200 to signed-in deliveries, 401 to a wrong password and 404 after a disconnect. ## Zoho CRM The Zoho CRM app: a connected Zoho CRM organization, in any Zoho data center, its deal, lead and contact alerts and link previews. See the [Zoho CRM guide](/docs/zoho-crm/). `/zoho` and **Log to Zoho CRM** run like any app's [slash command and message action](#ref-apps). Alerts come from Zoho CRM's change notifications, which Freesbe sets up and renews itself. Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/zoho-crm Who: Admins. Whether the server can connect Zoho (`available`), the `connection` (`orgId`, `orgName`, `dataCenter`, `crmUrl`, `botUserId`, `unfurls`, `listening` while Zoho CRM sends changes, `brokenAt` when Zoho stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/zoho-crm/connect Who: Admins. The `url` of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with `?zoho-crm=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/zoho-crm Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/zoho-crm Who: Admins. Disconnect: Zoho CRM stops sending changes, Freesbe's access is revoked in Zoho, and the app's command and action go away. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/zoho-crm/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`deal_created`, `deal_stage`, `deal_won`, `deal_lost`, `lead_created`, `contact_created`) and an optional `minAmount` for deals. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 } ``` ### PATCH /api/workspaces/{w}/zoho-crm/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount`, or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/zoho-crm/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/zoho-crm/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/crm/zoho-crm/{token} Who: No token. Where Zoho CRM delivers one organization's change notifications. The body's `token` has to be the one Freesbe gave Zoho with the channel. Answers 200 and posts afterwards, 401 to a wrong token, and 404 after a disconnect. ## Zendesk A workspace's Zendesk connection and its ticket alerts. See the [Zendesk guide](/docs/zendesk/). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/zendesk Who: Admins. The `connection` (`subdomain`, `auth` (`oauth` or `token`), `email`, whether it's `receiving` ticket events, `needsReconnect` and `lastEventAt`), or null, its `alerts`, and whether `oauth` sign-in is available. ### POST /api/workspaces/{w}/zendesk/oauth Who: Admins. Start signing in with Zendesk for `subdomain`. Answers with the `url` to send the browser to; Zendesk comes back to the app with `?zendesk=connected` or `?zendesk_error=`. Request: ```json { "subdomain": "northwind" } ``` ### POST /api/workspaces/{w}/zendesk/token Who: Admins. Connect with an agent's `email` and Zendesk `apiToken` instead. The token is checked with Zendesk and stored encrypted. Request: ```json { "subdomain": "northwind", "email": "agent@northwind.example", "apiToken": "…" } ``` ### DELETE /api/workspaces/{w}/zendesk Who: Admins. Disconnect: removes Freesbe's webhook from Zendesk, the alerts and the Zendesk app. Answers 204. ### GET /api/workspaces/{w}/zendesk/groups Who: Admins. The Zendesk groups an alert can be limited to, as `groups` with `id` and `name`. ### POST /api/workspaces/{w}/zendesk/alerts Who: Admins. Add a ticket alert: `channelId`, `events` (`created`, `status`, `comment`, `priority`, `assignment`) and an optional `groupId`. Answers 201. Request: ```json { "channelId": "01J…", "events": ["created", "status"], "groupId": "7" } ``` ### PATCH /api/workspaces/{w}/zendesk/alerts/{id} Who: Admins. Change `channelId`, `events` or `groupId`, or turn it off and on with `disabled`. ### DELETE /api/workspaces/{w}/zendesk/alerts/{id} Who: Admins. Delete the alert. Answers 204. ### POST /api/workspaces/{w}/zendesk/alerts/{id}/test Who: Admins. Post a sample ticket into its channel. Answers 204. ### POST /hooks/support/zendesk/{token} Who: No token. Where Zendesk delivers ticket events, signed with `X-Zendesk-Webhook-Signature` and `X-Zendesk-Webhook-Signature-Timestamp`. Freesbe adds this webhook in Zendesk when you connect. Answers 200 to signed events and 401 to a bad or old signature. ## Zoho Desk The Zoho Desk app: a connected Zoho Desk help desk, in any Zoho data center, its ticket alerts and link previews. See the [Zoho Desk guide](/docs/zoho-desk/). **Create Zoho Desk ticket** runs like any app's [message action](#ref-apps). Alerts come from a webhook Freesbe adds to the help desk when an administrator connects it. Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/zoho-desk Who: Admins. Whether the server can connect Zoho (`available`), the `connection` (`orgId`, `orgName`, `portalName`, `dataCenter`, `deskUrl`, `botUserId`, `unfurls`, `listening` once Freesbe's webhook is in Zoho Desk, `lastEventAt`, `brokenAt` when Zoho stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/zoho-desk/connect Who: Admins. The `url` of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with `?zoho-desk=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/zoho-desk Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/zoho-desk Who: Admins. Disconnect: Freesbe removes its webhook from Zoho Desk, its access is revoked in Zoho, and the app's action goes away. Alerts are kept for a reconnect. Answers 204. ### GET /api/workspaces/{w}/zoho-desk/departments Who: Admins. The help desk's `departments`, each with its `id` and `name`, for alerts that take one. ### POST /api/workspaces/{w}/zoho-desk/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`created`, `status`, `reply`, `priority`, `assignment`) and an optional `departmentId`. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["created", "reply"], "departmentId": "1234000000006907" } ``` ### PATCH /api/workspaces/{w}/zoho-desk/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `departmentId` (null for every department), or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/zoho-desk/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/zoho-desk/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/support/zoho-desk/{token} Who: No token. Where Freesbe's webhook in Zoho Desk delivers one help desk's new tickets, ticket changes and customer replies. The token is random and only Zoho Desk has it, and every event has to be for the connected help desk. Answers 200 and posts afterwards, 403 to another help desk's events, and 404 after a disconnect. ## Zoho Books and Zoho Invoice The Zoho Books and Zoho Invoice apps: a connected organization in either, in any Zoho data center, its invoice and payment alerts and link previews. See the [Zoho Books and Zoho Invoice guide](/docs/zoho-books/). The two have the same routes, under `zoho-books` and `zoho-invoice`; `{product}` below is one of them. Neither lets an app add a webhook, so Freesbe checks the organization every ten minutes while an alert is on. Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/zoho-{product} Who: Admins. Whether the server can connect Zoho (`available`), the `connection` (`orgId`, `orgName`, `currency`, `dataCenter`, `appUrl`, `botUserId`, `unfurls`, `checkedAt` when Freesbe last checked, `brokenAt` when Zoho stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/zoho-{product}/connect Who: Admins. The `url` of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with `?zoho-books=connected` or `?zoho-invoice=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/zoho-{product} Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/zoho-{product} Who: Admins. Disconnect: Freesbe's access is revoked in Zoho and checking stops. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/zoho-{product}/alerts Who: Admins. Add an alert: `channelId`, `kinds` (`sent`, `payment`, `paid`, `overdue`) and an optional `minAmount`. Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["payment", "overdue"], "minAmount": 500 } ``` ### PATCH /api/workspaces/{w}/zoho-{product}/alerts/{id} Who: Admins. Change `channelId`, `kinds` or `minAmount` (null for any amount), or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/zoho-{product}/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/zoho-{product}/alerts/{id}/test Who: Admins. Post a sample alert, of the first kind it takes, into its channel. Answers 204. ## Freshdesk A workspace's Freshdesk connection and its ticket alerts. See the [Freshdesk guide](/docs/freshdesk/). `/freshdesk`, **Create Freshdesk ticket** and **Add to Freshdesk ticket** run like any app's [slash command and message actions](#ref-apps). Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/freshdesk Who: Admins. The `connection` (`subdomain`, the agent's `email` and `agentName`, the secret `hookUrl` Freshdesk's automation rules call, `needsReconnect` and `lastEventAt`), or null, and its `alerts`. ### POST /api/workspaces/{w}/freshdesk Who: Admins. Connect, or connect again, with an agent's Freshdesk `apiKey`. The key is checked with Freshdesk and stored encrypted; connecting again keeps the `hookUrl`. Answers like GET. Request: ```json { "subdomain": "northwind", "apiKey": "…" } ``` ### DELETE /api/workspaces/{w}/freshdesk Who: Admins. Disconnect: forgets the API key, the alerts and the Freshdesk app, and the `hookUrl` stops working. Answers 204. ### GET /api/workspaces/{w}/freshdesk/groups Who: Admins. The Freshdesk groups an alert can be limited to, as `groups` with `id` and `name`. ### POST /api/workspaces/{w}/freshdesk/alerts Who: Admins. Add a ticket alert: `channelId`, `events` (`created`, `status`, `reply`, `priority`, `assignment`) and an optional `groupId`. Answers 201. Request: ```json { "channelId": "01J…", "events": ["created", "status"], "groupId": "7" } ``` ### PATCH /api/workspaces/{w}/freshdesk/alerts/{id} Who: Admins. Change `channelId`, `events` or `groupId`, or turn it off and on with `disabled`. ### DELETE /api/workspaces/{w}/freshdesk/alerts/{id} Who: Admins. Delete the alert. Answers 204. ### POST /api/workspaces/{w}/freshdesk/alerts/{id}/test Who: Admins. Post a sample ticket into its channel. Answers 204. ### POST /hooks/support/freshdesk/{token} Who: No token. Where Freshdesk's automation rules say a ticket was made (`{"freesbe_event":"created","ticket_id":"…"}`) or changed (`"updated"`). Freesbe reads the ticket from Freshdesk and posts what changed since it last saw it. Answers 200, 400 without a ticket number and 404 after a disconnect. ## Monitoring Monitoring tools posting problems into a channel: Zabbix, Grafana, Prometheus Alertmanager, PagerDuty, Opsgenie, Datadog, Sentry, Better Stack, New Relic, Uptime Kuma, UptimeRobot, and Freesbe's own status page. Each connection has its own secret URL the tool sends to, and posts one card per problem (or alert) that later updates and the recovery change in place. See the [Zabbix](/docs/zabbix/), [Grafana and Alertmanager](/docs/grafana/), [PagerDuty](/docs/pagerduty/), [Opsgenie](/docs/opsgenie/), [Datadog](/docs/datadog/), [Sentry](/docs/sentry/), [Better Stack](/docs/better-stack/), [New Relic](/docs/new-relic/), [Uptime Kuma](/docs/uptime-kuma/), [UptimeRobot](/docs/uptimerobot/) and [Freesbe status](/docs/status-page/) guides. Admins see the connections for public channels and the private channels they're in. ### GET /api/workspaces/{w}/monitoring Who: Admins. The `connections`: `provider`, `name`, `channelId`, `minSeverity` (`info`, `warning`, `average`, `high` or `critical`), `replyOnResolve`, `serverUrl`, `hasApiToken`, `openProblems`, `lastEventAt`, `board` (the live problems board is pinned), `reportSchedule` (`daily`, `weekly` or null), `reportTimeZone` and `nextReportAt`. ### POST /api/workspaces/{w}/monitoring Who: Admins. Connect a tool: `provider` (`zabbix`, `grafana`, `alertmanager`, `pagerduty`, `opsgenie`, `datadog`, `sentry`, `betterstack`, `newrelic`, `uptimekuma`, `uptimerobot` or `freesbe-status`), `name`, `channelId`, and optionally `minSeverity`, `replyOnResolve`, `serverUrl`, `apiToken`, `board` (default true), `reportSchedule` and `reportTimeZone` (reports post at 9:00 there). Freesbe checks the token with the tool before saving it, encrypted. Answers 201 with the `hookUrl` to give the tool, shown only now. For `freesbe-status` there is nothing to paste: Freesbe registers the URL with [status.freesbe.net](https://status.freesbe.net/) itself. Request: ```json { "provider": "zabbix", "name": "Production Zabbix", "channelId": "01J…", "minSeverity": "warning" } ``` ### PATCH /api/workspaces/{w}/monitoring/{id} Who: Admins. Change `name`, `channelId`, `minSeverity`, `replyOnResolve`, `serverUrl`, `apiToken` (null removes it, and the buttons), `board`, `reportSchedule` or `reportTimeZone`, or turn it off and on with `disabled`. Request: ```json { "minSeverity": "high" } ``` ### DELETE /api/workspaces/{w}/monitoring/{id} Who: Admins. Disconnect it. Cards already posted stay. Answers 204. ### POST /api/workspaces/{w}/monitoring/{id}/test Who: Admins. Post a sample problem card into its channel. Answers 204. ### POST /hooks/monitoring/zabbix/{token} Who: No token. Where Freesbe's Zabbix media type sends each problem, update and recovery, as JSON with Zabbix's macros filled in (`event_id`, `event_value`, `event_update_status`, `event_nseverity`, `event_name`, `host_name`…). Answers 200 whether or not it posted, 404 to an unknown or switched-off connection, and 410 when its channel is gone. ### POST /hooks/monitoring/grafana/{token} Who: No token. Where a Grafana webhook contact point sends: Alertmanager's webhook format (version 4) with Grafana's extra fields. Each alert in `alerts` is one card, named by its `fingerprint` and `startsAt`; a `resolved` alert turns its card green. Same answers as Zabbix's. ### POST /hooks/monitoring/alertmanager/{token} Who: No token. Where Prometheus Alertmanager's `webhook_configs` send, in its webhook format (version 4). Each alert is one card; the `severity` label sets its severity (high when there's none). Same answers as Zabbix's. ### POST /hooks/monitoring/pagerduty/{token} Who: No token. Where a PagerDuty generic webhook (V3) subscription sends incident events. Each incident is one card, named by its `id`: `incident.triggered` posts it, acknowledgements, reassignments, escalations, priority changes and notes update it, and `incident.resolved` turns it green. The priority (P1 to P5) sets its severity, or the urgency when there's none. Same answers as Zabbix's. ### POST /hooks/monitoring/opsgenie/{token} Who: No token. Where an Opsgenie Webhook integration sends alert actions. Each alert is one card, named by its `alertId`: `Create` posts it, `Acknowledge`, `UnAcknowledge`, `AddNote`, `AssignOwnership` and `UpdatePriority` update it, and `Close` turns it green. The priority (P1 to P5) sets its severity. Same answers as Zabbix's. ### POST /hooks/monitoring/datadog/{token} Who: No token. Where a Datadog webhook sends monitor notifications, with the payload shown when connecting (Datadog's `$ALERT_CYCLE_KEY`, `$ALERT_ID`, `$ALERT_TRANSITION`, `$ALERT_PRIORITY`, `$EVENT_TITLE`, `$HOSTNAME`, `$ALERT_SCOPE`, `$TAGS`, `$LINK` and more). Each alert cycle is one card: `Triggered`, `Warn` and `No Data` post or update it, and `Recovered` turns it green. Same answers as Zabbix's. ### POST /hooks/monitoring/sentry/{token} Who: No token. Where a Sentry internal integration sends its webhooks. Issue webhooks (`created`, `resolved`, `unresolved`, `archived`, `assigned`) and issue alerts (`triggered`) are one card per issue, named by its id; an issue that's `unresolved` after it was resolved gets a new card. Metric alerts (`critical`, `warning`, `resolved`) are cards of their own. The issue's `level` sets its severity. Same answers as Zabbix's. ### POST /hooks/monitoring/betterstack/{token} Who: No token. Where a Better Stack Uptime webhook sends incidents (`{ data: { id, type: "incident", attributes } }`). Each incident is one card, named by its id: `Started` posts it, `Acknowledged` updates it, and `Resolved` turns it green. Same answers as Zabbix's. ### POST /hooks/monitoring/newrelic/{token} Who: No token. Where a New Relic workflow's webhook sends issues, with the payload shown when connecting (the workflow's `issueId`, `issuePageUrl`, `priority`, `state`, `entitiesData.names`, `accumulations.conditionName` and more). Each issue is one card: `CREATED` and `ACTIVATED` post it, `ACKNOWLEDGED` updates it, and `CLOSED` turns it green. The priority sets its severity. Same answers as Zabbix's. ### POST /hooks/monitoring/uptimekuma/{token} Who: No token. Where an Uptime Kuma Webhook notification sends (`{ heartbeat, monitor, msg }`, as application/json). Each outage is one card per monitor: `heartbeat.status` 0 posts it, and 1 turns it green. Going down again after being back up is a new card. Kuma's test message posts nothing. Same answers as Zabbix's. ### POST /hooks/monitoring/uptimerobot/{token} Who: No token. Where an UptimeRobot webhook alert contact sends, with the payload shown when connecting (`monitorID`, `name`, `url`, `alertType`, `details`, `duration`, `time`). `alertType` 1 posts a card for the monitor and 2 turns it green with how long it was down; other alerts post nothing. Same answers as Zabbix's. ### POST /hooks/monitoring/freesbe-status/{token} Who: No token. Where status.freesbe.net posts incidents and updates for a Freesbe status connection, signed with `X-Status-Signature`. Freesbe registers it itself when the connection is made. ## News feeds RSS, Atom and JSON feeds whose new items Freesbe posts as cards into a channel. Freesbe checks each feed itself, every 15 minutes and less often while it's quiet or failing, and posts only items it hasn't seen. See the [news feeds guide](/docs/rss-feeds/). Admins see the feeds for public channels and the private channels they're in. ### GET /api/workspaces/{w}/feeds Who: Admins. The `feeds`: `url`, `siteUrl`, `name`, `iconUrl`, `channelId`, `keywords`, `lastCheckedAt`, `lastItemAt`, `nextCheckAt`, `error` (while it keeps failing) and `brokenAt` (paused after 3 days of failing), and the plan's `limit` (null for none). ### POST /api/workspaces/{w}/feeds/preview Who: Admins. Find and read a feed before adding it: `url` is the feed's address or a website's whose page links to its feed. Answers with the feed's own `url`, `title`, `siteUrl`, `iconUrl` and its newest 3 `items`, or 400 when there's no feed there. Request: ```json { "url": "blog.northwind.example" } ``` ### POST /api/workspaces/{w}/feeds Who: Admins. Add a feed: `url`, `channelId`, and optionally `name` (the feed's title), `keywords` (only items with one of them in the title or summary post) and `postLatest` (default true: post the newest item now). Everything already in the feed counts as seen. Answers 201, or 409 at the plan's limit. Request: ```json { "url": "https://blog.northwind.example/feed.xml", "channelId": "01J…", "keywords": ["tea"] } ``` ### PATCH /api/workspaces/{w}/feeds/{id} Who: Admins. Change `name`, `channelId` or `keywords`, or turn it off and on with `disabled`. Saving a paused feed checks it again soon. Request: ```json { "keywords": ["tea", "coffee"] } ``` ### DELETE /api/workspaces/{w}/feeds/{id} Who: Admins. Stop checking the feed. Cards already posted stay. Answers 204. ### POST /api/workspaces/{w}/feeds/{id}/check Who: Admins. Check the feed now and post anything new. Answers 204. ## Zoho Recruit The Zoho Recruit app: a connected Zoho Recruit organization, in any Zoho data center, its candidate, job opening and interview alerts and link previews. See the [Zoho Recruit guide](/docs/zoho-recruit/). Alerts come from Zoho Recruit's change notifications, which Freesbe sets up and renews itself. Admins see the alerts for public channels and the private channels they're in. ### GET /api/workspaces/{w}/zoho-recruit Who: Admins. Whether the server can connect Zoho (`available`), the `connection` (`orgId`, `orgName`, `dataCenter`, `recruitUrl`, `botUserId`, `unfurls`, `listening` while Zoho Recruit sends changes, `brokenAt` when Zoho stopped accepting it) or null, and the `alerts`. ### POST /api/workspaces/{w}/zoho-recruit/connect Who: Admins. The `url` of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with `?zoho-recruit=connected`. Only the admin who started it can finish it, within 15 minutes. ### PATCH /api/workspaces/{w}/zoho-recruit Who: Admins. Turn link previews on or off with `unfurls`. Request: ```json { "unfurls": false } ``` ### DELETE /api/workspaces/{w}/zoho-recruit Who: Admins. Disconnect: Zoho Recruit stops sending changes and Freesbe's access is revoked in Zoho. Alerts are kept for a reconnect. Answers 204. ### POST /api/workspaces/{w}/zoho-recruit/alerts Who: Admins. Add an alert: `channelId` and `kinds` (`candidate_created`, `candidate_status`, `candidate_hired`, `job_created`, `interview_scheduled`). Answers 201. Request: ```json { "channelId": "01J…", "kinds": ["candidate_hired", "interview_scheduled"] } ``` ### PATCH /api/workspaces/{w}/zoho-recruit/alerts/{id} Who: Admins. Change `channelId` or `kinds`, or turn it off and on with `disabled`. Request: ```json { "disabled": true } ``` ### DELETE /api/workspaces/{w}/zoho-recruit/alerts/{id} Who: Admins. Delete it. Answers 204. ### POST /api/workspaces/{w}/zoho-recruit/alerts/{id}/test Who: Admins. Post a sample alert into its channel. Answers 204. ### POST /hooks/recruiting/zoho-recruit/{token} Who: No token. Where Zoho Recruit delivers one organization's change notifications. The body's `token` has to be the one Freesbe gave Zoho with the channel. Answers 200 and posts afterwards, 401 to a wrong token, and 404 after a disconnect. ## Workflows Workflows run steps for people: post a message, send a form, add someone to a channel, give a to-do, wait, ask for approval, ask a question with buttons, set a status, or branch with `if`. Guests can't make or run them. A workflow's maker and workspace admins can change it and see its runs; everyone else sees only the button workflows that are on. Text in steps can use `{{variables}}` such as `{{trigger.person}}`. See [Workflows](/docs/workflows) for what each trigger and step does. ### GET /api/workspaces/{w}/workflows Who: Members. The workflows you manage, plus the button workflows that are on, by name. `canCreate` says whether you can make one. People who can't edit a workflow don't get its steps. Answer: ```json { "workflows": [ { "id": "01JBK4…", "name": "IT help request", "emoji": "🛠️", "status": "on", "trigger": { "type": "manual", "channelIds": ["01JB9S…"] }, "runCount": 12, "canEdit": false, … } ], "canCreate": true } ``` ### POST /api/workspaces/{w}/workflows Who: Members, not guests. Make a workflow: `name`, optional `description` and `emoji`, a `trigger`, up to 30 `steps`, and `status` `on` or `off`. Only admins can when the workspace's `workflowBuilders` is `admins`. A workflow with problems can be saved, but stays off. Triggers: `manual`, `schedule`, `reaction`, `member_joined`, `message`, `webhook`, `clock`, `booking`, `meeting_ended`, `huddle_ended`, `missed_call`, `todo_done`. Steps: `message`, `form`, `add_to_channel`, `todo`, `wait`, `approval`, `ask`, `set_status`, `branch` (with `conditions`, `then` and `else`). Answers 201. Request: ```json { "name": "IT help request", "emoji": "🛠️", "status": "on", "trigger": { "type": "manual", "channelIds": ["01JB9S…"] }, "steps": [ { "id": "f1", "type": "form", "person": { "var": "trigger.person" }, "title": "Ask IT for help", "text": "", "fields": [ { "id": "what", "label": "What do you need help with?", "type": "paragraph", "required": true, "options": [] } ] }, { "id": "s2", "type": "message", "to": { "kind": "channel", "channelId": "01JB9S…" }, "text": "*New IT request* from {{f1.person}}\n{{f1.what}}" } ] } ``` Answer: ```json { "workflow": { "id": "01JBK4…", "name": "IT help request", "status": "on", "canEdit": true, … } } ``` ### GET /api/workflows/{id} Who: Members. One workflow you can see. Answer: ```json { "workflow": { "id": "01JBK4…", "name": "IT help request", "trigger": { "type": "manual", … }, "steps": [ … ], … } } ``` ### PATCH /api/workflows/{id} Who: Creator or admins. Change any of the fields above. Turning it `on` needs it free of problems; a schedule's next run is worked out again. Request: ```json { "status": "off" } ``` ### DELETE /api/workflows/{id} Who: Creator or admins. Delete it and stop its runs that are still going. Answers 204. ### POST /api/workflows/{id}/run Who: Members, not guests. Run a workflow that starts from a button. Pass `channelId` to start it from a channel. Its maker can try it while it's off. A form for you comes back as `request`, ready to fill in. Up to 200 runs an hour each. Request: ```json { "channelId": "01JB9S…" } ``` Answer: ```json { "run": { "id": "01JBK7…", "status": "waiting", … }, "request": { "id": "01JBK8…", "title": "Ask IT for help", … } } ``` ### GET /api/workflows/{id}/runs Who: Creator or admins. The latest runs, newest first, each with its status and a log of what it did. Answer: ```json { "runs": [ { "id": "01JBK7…", "status": "done", "startedBy": "01JB8Q…", "startedAt": 1761238800000, "log": [ { "at": 1761238800000, "stepId": null, "ok": true, "text": "Started by {person}", "vars": { "person": "01JB8Q…" } }, … ] } ] } ``` ### POST /api/workflows/{id}/hook-url Who: Creator or admins. A new secret URL for a workflow that starts from a webhook, as the workflow's `hookUrl`. The old one stops working at once. ### POST /api/workflow-runs/{id}/cancel Who: Creator or admins. Stop a run that is still going. Answers 204. ### GET /api/workspaces/{w}/workflow-requests Who: Members. Forms, approvals and questions that workflows sent you and that are still open. `kind` is `form`, `approval` or `ask`. Questions posted in a channel for anyone to answer aren't listed. Answer: ```json { "requests": [ { "id": "01JBK7…", "workflowName": "Monthly check-in", "title": "How was your month?", "fields": [ … ], "status": "open", "createdAt": 1761238800000 } ] } ``` ### GET /api/workflow-requests/{id} Who: Its owner. One request sent to you, open or not, or a question in a channel you're in. ### POST /api/workflow-requests/{id} Who: Its owner. Answer it, and the workflow carries on at once. A form takes `answers` by question id; an approval takes `decision` (`approved` or `rejected`) and an optional `comment`; a question takes one of its `choices` as `choice`. Answers 204, or 409 when someone already answered. Request: ```json { "answers": { "what": "My laptop won't connect to the office printer", "urgency": "Today, please" } } { "decision": "approved", "comment": "Enjoy the trip" } { "choice": "Tacos" } ``` ### DELETE /api/workflow-requests/{id} Who: Its owner. Close a form without sending it, which stops the workflow that sent it. Only forms can be closed this way. Answers 204. ### POST /hooks/workflows/{token} Who: No token. Start a workflow that starts from a webhook, with a JSON object of its variables. The token in the URL is the secret. Answers `{ "ok": true }`, or `ok: false` with an `error`. Request: ```json { "order": "NW-10482", "customer": "Acme Corp", "days": "3" } ``` ## Phone SIP credentials for desk phones and softphones, connecting your own sip.io account, and the phone system from Freesbe with its numbers. Until an admin sets up either, `provider` is `mock` and the credentials are for testing. ### GET /api/workspaces/{w}/phone Who: Members. Your SIP account, if it's set up. Add `?reveal=1` for the password. Guests get `provider: "off"`. On the phone system from Freesbe (`inApp: true`), `account` is always null: the apps sign in on their own, and admins set up desk phones. Answer: ```json { "provider": "sipio", "account": { "username": "dana.whitfield", "domain": "northwind.sip.example", "createdAt": 1761200000000, "rotatedAt": null } } ``` ### POST /api/workspaces/{w}/phone Who: Members, not guests. Set up your SIP account. The answer includes the password. Answers 403 on the phone system from Freesbe, where admins set up desk phones. ### POST /api/workspaces/{w}/phone/rotate Who: Members, not guests. A new SIP password. The old one stops working. Answers 403 on the phone system from Freesbe. ### POST /api/workspaces/{w}/phone/webphone Who: Members, not guests. A short-lived login for calling from Freesbe itself, on a phone system from Freesbe: a SIP over WebSocket `server`, the `uri`, `username` and `password`, `iceServers` for the call's audio, and when it runs out (`expiresAt`). One at a time per person: a new one ends the last. The mobile and desktop apps send `install` (`{ id, name }`: an id the app keeps while installed, starting with `desk_` for the desktop app, and the phone's or computer's name) to get a line of their own instead, which rings alongside the others and stays until the app signs out; only the Freesbe apps can. A computer's line gets a new password at each login, and one whose app hasn't logged in for 8 hours gets a new password nobody has, so it works only while the app keeps coming back. A person has at most 10 computer lines: a new one replaces the one used longest ago. 409 when the workspace has no phone system from Freesbe or it's turned off. `inApp` on `GET /phone` says whether to use it. Answer: ```json { "server": "wss://webrtc.sip.example", "uri": "sip:wrtc_7@northwind.sip.example", "username": "wrtc_7", "password": "…", "extension": "1001", "iceServers": [{ "urls": ["stun:turn.sip.example:3478"] }], "expiresAt": 1761228800000 } ``` ### DELETE /api/workspaces/{w}/phone/webphone/{install} Who: Members, not guests. Signs the mobile or desktop app's own line out (on sign-out): calls stop ringing it. 204, also when it was already gone. ### POST /api/workspaces/{w}/phone/answered Who: Members, not guests. Says this device just answered a call from `number`, with `kind` (`desktop`, `mobile` or `web`) and the device's `name`. Your other devices get a `phone.answered` event (`number`, `on: { kind, name }`, `at`), so where the call rang too they show where it was answered instead of a missed call. Not for apps. 204. ### GET /api/workspaces/{w}/phone/calls Who: Members, not guests. Your calls on the phone system from Freesbe from the last 30 days, newest first, 50 at a time (`?page=2` for older ones while `more` is true). `direction` is from your side; `userId` is the colleague on the other end, and `number` is null for them. `seconds` is time talking, 0 when unanswered. `transcriptId` is the recording id of the call's transcript when it was transcribed (read it with `/phone/recordings/{id}/transcript`), or null. Each call that ends after that reaches your devices as a `phone.call_logged` event with the same fields (`call`), so read this once and add those to it. 409 without a phone system from Freesbe. Answer: ```json { "calls": [{ "id": "c2", "direction": "in", "number": "+16465550111", "name": "Acme Corp", "userId": null, "startedAt": 1761228800000, "answered": false, "seconds": 0, "transcriptId": null }], "more": false } ``` ### GET /api/workspaces/{w}/phone/missed Who: Members, not guests. When you last looked at your Recent calls (`seenAt`, null if never): missed calls after it are new. The apps count those on the phone, leaving out ones the caller left a voicemail for. Answer: ```json { "seenAt": 1761228800000 } ``` ### PUT /api/workspaces/{w}/phone/missed Who: Members, not guests. You've seen your missed calls, now. Your other devices hear it as a `phone.missed_seen` event. Answers like `GET`. ### POST /api/workspaces/{w}/phone/transcribe Who: Members, not guests. Transcribe the call you're on now (`on: true`), or stop (`on: false`). This records the call; when it ends, you and the colleagues on it get its transcript and AI notes in a message from Phone. Counts toward the plan's transcript hours, shared with huddles and meetings. 403 when an admin hasn't turned on transcribing your own calls (`canTranscribe` on `GET /phone`), 409 when you're not on a call. Request: ```json { "on": true } ``` Answer: ```json { "on": true } ``` ### GET /api/workspaces/{w}/phone/recordings/{id}/transcript Who: Members, not guests. A transcribed call you were on: who it was `with`, when it started and how long it was, and the `transcript` (a summary, the caller's mood, key points, action items and every line, with `side` `agent` for your side and `caller` for theirs). Admins can read any. 404 for a call you weren't on. Answer: ```json { "call": { "with": "Contoso", "startedAt": 1761228800000, "seconds": 402 }, "transcript": { "state": "done", "summary": "…", "sentiment": "Friendly", "keyPoints": ["…"], "actionItems": ["…"], "lines": [{ "side": "agent", "speaker": null, "start": 2, "text": "Hi Kenji…" }] } } ``` ### GET /api/workspaces/{w}/phone/recordings/{id}/audio Who: Members, not guests. The recording of a transcribed call you were on, as audio. 404 for a call you weren't on. ### POST /api/workspaces/{w}/phone/recordings/{id}/share Who: Members, not guests. Post a transcribed call's notes (the summary and action items, with a link to the transcript) to a channel or DM you can write in, as you. Answers 204. Request: ```json { "channelId": "01J…" } ``` ### POST /api/workspaces/{w}/phone/calls Who: Members, not guests. App scope: `phone:call`. Click-to-call: ring someone's Freesbe phone, and once they pick up, dial `to` from the workspace's number. People start calls for themselves; an app with the `phone:call` scope names whose phone rings with `userId` or `email` (for example a CRM's Call button, from its server). `leadId` and `vars` (up to 20 short values) come back in the `phone.call_ended` event, with how it went and how long it lasted. 409 with `call_unreachable` when the person isn't signed in to Freesbe's phone anywhere (the app on a computer or phone), `call_busy` when they're on a call, `call_duplicate` for the same call twice within 5 seconds, and `conflict` without a phone system from Freesbe or while it's off. 400 when `to` isn't a US or Canadian number or is premium-rate (900, 976), and 429 past the workspace's calls to outside numbers this hour (30 on Free or a trial, 300 on paid plans). Request: ```json { "to": "+14155550142", "email": "dana@northwind.example", "leadId": "lead-2291", "vars": { "deal": "Acme renewal" } } ``` Answer: ```json { "id": "c2c_01JC9A2B3C4D5E6F7G8H9J0K1M", "userId": "01JB8Z6Y0QH3W8T2M5N7P9R1S3" } ``` ### GET /api/workspaces/{w}/sip-connection Who: Admins. Whether the workspace is connected to sip.io, and how. ### PUT /api/workspaces/{w}/sip-connection Who: Admins. Connect sip.io: `apiBase`, `accountId`, `apiKey`, and `linkByEmail` to put each new device on the sip.io user with the same email (off by default). Freesbe checks the key first. Request: ```json { "apiBase": "https://northwind.sip.example/api", "accountId": "4411", "apiKey": "…", "linkByEmail": false } ``` ### DELETE /api/workspaces/{w}/sip-connection Who: Admins. Disconnect sip.io. Answers 204. ### GET /api/workspaces/{w}/phone-system Who: Admins. The phone system from Freesbe. `available` says whether this server offers one, `included` whether the workspace has the Phone system add-on, and `enabled` whether it's on. `status` is `active`, `outbound_off` while a payment is a week overdue (incoming calls still ring), or `off`, with `offReason` (`admin` or `unpaid`) and `releaseAt`, when the kept numbers are released. Answer: ```json { "available": true, "included": true, "enabled": true, "country": "US", "sipDomain": "northwind.sip.example", "enabledAt": 1792540320000, "enabledBy": "01JB8Q…", "status": "active", "offReason": null, "offAt": null, "releaseAt": null } ``` ### POST /api/workspaces/{w}/phone-system Who: Admins. Turn the phone system on, for a `country` (a two-letter code; the United States or Canada for now). Answers like `GET`. Request: ```json { "country": "US" } ``` ### DELETE /api/workspaces/{w}/phone-system Who: Admins. Turn the phone system off. Calls, texts and phone logins stop, and the numbers are kept until `releaseAt`. Answers like `GET`. ### POST /api/workspaces/{w}/phone-system/turn-on-again Who: Admins. Turn a phone system that was turned off back on, with the numbers it kept. When it was turned off for an overdue payment, pay the invoice on the Billing page first, or this answers 409. ### GET /api/workspaces/{w}/phone-system/identity Who: Admins. Where the workspace is with identity verification, which buying numbers needs once: `required`, and `status`: `none`, `waiting` (the `link` is ready for a phone, until `linkExpiresAt`), `started`, `checking`, `review`, `approved`, `rejected` or `expired`. `POST` starts a check, or answers the one that's open. Answer: ```json { "required": true, "status": "waiting", "link": "https://verify.example/s/…", "linkExpiresAt": 1792731600000, "startedBy": "01JB8Q…", "startedAt": 1792729800000, "verifiedAt": null, "verifiedBy": null } ``` ### GET /api/workspaces/{w}/phone-system/numbers/search?country=US&type=local&areaCode=415 Who: Admins. Numbers for sale. `type` is `local` (the default) or `tollfree`; `areaCode` is optional. Each offer has its monthly and setup price in `currency`, and `needsDocuments` when the carrier needs documents before it works. `included` is how many local numbers the workspace can still get at no extra cost (one comes with each person on the Phone system add-on). Answer: ```json { "offers": [ { "offer": "…", "e164": "+14155550134", "city": "San Francisco", "region": "CA", "areaCode": "415", "numberType": "local", "capabilities": ["voice", "sms"], "monthlyPrice": 5, "setupPrice": 0, "currency": "USD", "needsDocuments": false } ], "included": 3 } ``` ### GET /api/workspaces/{w}/phone-system/numbers Who: Admins. The workspace's numbers: each one's `status` (`active`, `pending`, `documents_needed`, `releasing` or `other`), `label`, `country` (two letters), place, type (`numberType`: `geographic`, `tollfree`, `national` or `mobile`), who it rings (`ringsUserIds`, everyone it rings at once; `ringsUserId` is set when that's one person), whether it can send and receive `texts`, and whether it goes to voicemail after hours (`afterHoursVoicemail`). Answer: ```json { "numbers": [ { "id": "01JC5N…", "e164": "+14155550134", "status": "active", "label": "Main line", "country": "US", "city": "San Francisco", "region": "CA", "numberType": "local", "ringsUserId": "01JB8Q…", "ringsUserIds": ["01JB8Q…"], "texts": true, "afterHoursVoicemail": false } ] } ``` ### POST /api/workspaces/{w}/phone-system/numbers Who: Admins. Buy a number: send back an `offer` from the search, and optionally `ringsUserId`, the person its calls ring. Needs identity verification first. `status` is `active`, `pending` or `documents_needed`. Answers 201. Request: ```json { "offer": "…", "ringsUserId": "01JB8Q…" } ``` Answer: ```json { "number": { "id": "01JC5N…", "e164": "+14155550134", "status": "active", … }, "status": "active" } ``` ### PATCH /api/workspaces/{w}/phone-system/numbers/{id} Who: Admins. Change who a number rings: `ringsUserIds`, 1 to 20 people, who all ring at once and the first to answer takes the call (`ringsUserId` still works for one person). The first time a number rings someone, Freesbe makes their phone account. `afterHoursVoicemail: true` sends its calls outside the business hours to the voicemail of the first person it rings (set the hours first). When several people ring and nobody answers, the call goes to the first person's voicemail. `menuId` answers it with a phone menu instead, `queueId` with a call queue (both turn voicemail after hours off), and `ringGroupId` with a ring group. Send what changes. Answers the number, with its `menuId`, `queueId` and `ringGroupId`. Request: ```json { "ringsUserIds": ["01JB8R…", "01JB8S…"], "afterHoursVoicemail": true } ``` ### GET /api/workspaces/{w}/phone-system/numbers/{id}/settings Who: Admins. One number's line settings: its `label`, whether callers who hide their number ring through (`allowAnonymous`), whether it follows the business hours (`followsHours`) and where calls go when it's closed (`afterHours`) and on closed days (`holidays`, null is the same as after hours): `voicemail`, `menu` (`menuId`), `queue` (`queueId`) or `hangup`. `callerId.calls` and `callerId.texts` say who may call or text out with it: `anyone`, `selected` (`userIds`) or `nobody`. `texts` says whether the carrier can deliver texts to it, whether texting is on, and the customer inbox that takes them. `fax` has whether fax machines are answered, the `method` (`t38` or `passthrough`), the `emails` faxes go to, a `webhookUrl`, and whether a PDF of each is kept. Answer: ```json { "number": { "id": "did_1", "e164": "+12065550134", "label": "Main line", "country": "US", … }, "label": "Main line", "allowAnonymous": true, "followsHours": true, "afterHours": { "kind": "voicemail" }, "afterHoursName": "Maya Chen", "holidays": null, "holidaysName": null, "callerId": { "calls": { "mode": "anyone", "userIds": [] }, "texts": { "mode": "selected", "userIds": ["01JB8R…"] } }, "texts": { "available": true, "enabled": true, "inbox": { "id": "01JD…", "name": "Texts" } }, "fax": { "on": true, "method": "t38", "emails": ["fax@northwind.example"], "webhookUrl": null, "keep": true } } ``` ### PATCH /api/workspaces/{w}/phone-system/numbers/{id}/settings Who: Admins. Change a number's line settings. Send what changes: `label` (up to 120 characters, null clears it), `allowAnonymous`, `afterHours` (a route as above to follow the business hours, null to stop), `holidays` (null sends closed days where after hours does), `callerId.calls` and `callerId.texts`, and any of `fax.on`, `fax.method`, `fax.emails`, `fax.webhookUrl` (https) and `fax.keep`. Voicemail goes to the first person the number rings. Answers the settings as they now are. Request: ```json { "label": "Main line", "afterHours": { "kind": "menu", "menuId": "ivr_8f…" }, "fax": { "on": true, "emails": ["fax@northwind.example"] } } ``` ### GET /api/workspaces/{w}/phone-system/port-ins Who: Admins. Requests to move numbers in from another provider, newest first. `status` is `sent` (waiting for Freesbe to check it), `checked`, `submitted` (with the old provider), `needs_fix` (see `message`, then fix it and send it again), `done` or `cancelled`. `moveAt` is the move day once the old provider gives one. The account number comes back as its last 4 characters (`accountEnding`) and the PIN never does. Answer: ```json { "requests": [ { "id": "01JD2P…", "numbers": ["+18135550142", "+18135550143"], "provider": "Contoso Telecom", "accountEnding": "4821", "accountName": "Northwind Traders", "serviceAddress": "100 Harbor St, Tampa, FL 33602", "hasPin": true, "billName": "contoso-bill.pdf", "answers": { "kind": "menu", "menuId": "ivr_8f…" }, "moveOn": null, "status": "submitted", "moveAt": "2026-11-12", "message": null, "signerName": "Maya Chen", "signerTitle": "Operations lead", "signedAt": 1761868800000, "createdBy": "01JB8Q…", "createdAt": 1761868800000, "updatedAt": 1761955200000, "doneAt": null } ] } ``` ### POST /api/workspaces/{w}/phone-system/port-ins Who: Admins. Ask to move numbers in from another provider. Send a multipart form: `numbers` (a JSON array of up to 100 US or Canadian numbers, all with the same provider), `provider`, `accountNumber`, `pin` (if the provider set one for moving numbers), `accountName` and `serviceAddress` exactly as the provider has them, `answers` (JSON: `{ "kind": "people", "userIds": […] }`, `{ "kind": "menu", "menuId": … }` or `{ "kind": "queue", "queueId": … }`, what answers the numbers once they arrive), `moveOn` (YYYY-MM-DD, at least 7 days out, or left out for as soon as possible), `bill` (a PDF or picture of a recent bill, 10 MB at most), and the letter of authorization's signature: `signerName`, `signerTitle` and `agree` set to `true`. Needs the Phone system add-on and identity verification. Free. Answers 201 with the request; once the numbers arrive they answer as asked and are billed like other numbers. Request: ```json numbers=["+18135550142","+18135550143"] provider=Contoso Telecom accountNumber=88-4821 pin=1234 accountName=Northwind Traders serviceAddress=100 Harbor St, Tampa, FL 33602 answers={"kind":"menu","menuId":"ivr_8f…"} signerName=Maya Chen signerTitle=Operations lead agree=true bill=@bill.pdf ``` ### PATCH /api/workspaces/{w}/phone-system/port-ins/{id} Who: Admins. Fix a request and send it again, signed again: every field like `POST`, except `bill` (the one sent before is kept unless a new one comes) and `pin` (kept when left out). Only before it's with the old provider (`sent`, `checked` or `needs_fix`). Answers the request. ### DELETE /api/workspaces/{w}/phone-system/port-ins/{id} Who: Admins. Cancel a request that hasn't finished. Answers the request, now `cancelled`. ### GET /api/workspaces/{w}/phone-system/menus Who: Admins. The phone menus (IVR): each one's `greeting` (typed `text` read in a `voice`, or an uploaded recording with `text` null), the `options` callers press, `noChoice` (where the call goes when nobody presses a key, after the greeting plays twice), `waitSeconds`, `dialExtensions` (whether callers can dial someone's extension during the greeting: `"everyone"`, a list of user ids, or null), and the `numberIds` it answers. An action is `people` (`userIds`, all ringing at once, then the first person's voicemail), `ringGroup` (`ringGroupId`), `voicemail` (a person's `userId`), `menu` (`menuId`), `queue` (`queueId`) or `hangup`. Answer: ```json { "menus": [{ "id": "ivr_8f…", "name": "Main menu", "waitSeconds": 5, "numberIds": ["did_1"], "greeting": { "recordingId": "media_3a…", "text": "Thanks for calling Northwind. For sales, press 1.", "voice": "1|en-US|Neural2-F" }, "options": [{ "key": "1", "action": { "kind": "people", "userIds": ["01JB8R…", "01JB8S…"] } }, { "key": "2", "action": { "kind": "menu", "menuId": "ivr_9c…" } }], "noChoice": { "kind": "voicemail", "userId": "01JB8R…" } }] } ``` ### POST /api/workspaces/{w}/phone-system/menus Who: Admins. Make a phone menu: a `name`, a `greeting` (`text` and a `voice` from `GET …/voices`, a `recordingId` from uploading one, or null for none), up to 12 `options` with a `key` (0 to 9, * or #) and an `action`, a `noChoice` action, `waitSeconds` (3 to 30), and optionally `dialExtensions` (`"everyone"`, user ids, or null, the default; keys win when they clash). Answers 201 with the menu. A workspace can have 30. Request: ```json { "name": "Main menu", "greeting": { "text": "Thanks for calling Northwind. For sales, press 1.", "voice": "1|en-US|Neural2-F" }, "options": [{ "key": "1", "action": { "kind": "people", "userIds": ["01JB8R…"] } }], "noChoice": { "kind": "hangup" }, "waitSeconds": 5 } ``` ### PATCH /api/workspaces/{w}/phone-system/menus/{id} Who: Admins. Change a phone menu. Send it whole, like `POST`. Answers the menu. ### DELETE /api/workspaces/{w}/phone-system/menus/{id} Who: Admins. Delete a phone menu. Answers 204, or 409 while a number answers with it or another menu sends calls to it. ### GET /api/workspaces/{w}/phone-system/call-us-links Who: Admins. The Call us links: pages customers open to call from the browser, free. Each has its `url` (call.freesbe.net/…), the `title` customers see, up to 5 `buttons` (a `label`, a `description` and an `action`, as a phone menu's key takes, but never `hangup`), `askName` (customers type their name, shown as the caller's name), up to 5 `questions` (`label`, `rule`: `letters_only`, `long_text`, `numbers_only`, `decimal`, `email` or `phone`, and `required`), and `hours` with its `closedMessage` (buttons follow the business hours). Answer: ```json { "links": [{ "id": "01JC…", "name": "Main call menu", "title": "How can we help?", "url": "https://call.freesbe.net/k7Qm2x…", "buttons": [{ "id": "wgr_1…", "label": "Sales", "description": "New orders and quotes", "action": { "kind": "queue", "queueId": "q_4…" } }], "askName": true, "questions": [{ "label": "Order number", "rule": "numbers_only", "required": false }], "hours": true, "closedMessage": "We're open weekdays 9 to 6.", "createdAt": 1793450000000 }] } ``` ### POST /api/workspaces/{w}/phone-system/call-us-links Who: Admins. Make a Call us link: `name`, `title`, `buttons`, `askName`, `questions`, `hours` and `closedMessage`, as `GET` shows them. `hours` needs the business hours set first. Answers 201 with the link. A workspace can have 20. Calls from them are free for customers and cost the workspace $0.01 a minute. Request: ```json { "name": "Main call menu", "title": "How can we help?", "askName": true, "hours": false, "closedMessage": null, "buttons": [{ "label": "Sales", "description": "New orders and quotes", "action": { "kind": "people", "userIds": ["01JB8R…"] } }], "questions": [{ "label": "Order number", "rule": "numbers_only", "required": false }] } ``` ### PATCH /api/workspaces/{w}/phone-system/call-us-links/{id} Who: Admins. Change a Call us link. Send it whole, like `POST`; keep each button's `id` so it stays the same button. The link's address doesn't change. Answers the link. ### DELETE /api/workspaces/{w}/phone-system/call-us-links/{id} Who: Admins. Delete a Call us link. Its address stops working, and a Telegram bot offering it stops. Answers 204. ### GET /api/call/{key} Who: No token. A Call us link's public page, no sign-in: the workspace's `name` and `logoUrl`, the `title`, the `buttons` with whether each is `open` now, `askName`, the `questions`, `closedMessage`, and `phone`, where the browser gets its login for the call. 404 when there's no such link. ### GET /api/workspaces/{w}/phone-system/extensions Who: Admins. Everyone's extension on the phone system from Freesbe (null for someone still getting theirs, which happens on its own) and `firstExtension`, where new ones start. Answer: ```json { "firstExtension": 101, "people": [{ "userId": "01JB8R…", "extension": "101" }, { "userId": "01JB8S…", "extension": "102" }] } ``` ### PATCH /api/workspaces/{w}/phone-system/extensions Who: Admins. Set where new extensions start: 101 to 899 for three digits, 1001 to 8999 for four, not starting with 9. People who have one keep it. Answers like `GET`. Request: ```json { "firstExtension": 1001 } ``` ### PUT /api/workspaces/{w}/phone-system/extensions/{userId} Who: Admins. Change someone's extension: 2 to 6 digits, not starting with 0 or 9. Answers 409 when it's someone else's or the phone system uses it already. Request: ```json { "extension": "120" } ``` Answer: ```json { "userId": "01JB8R…", "extension": "120" } ``` ### GET /api/workspaces/{w}/phone-system/extensions/{userId}/desk-phone Who: Admins. Someone's SIP login for a desk phone or softphone app, like `GET /phone` answers them (`account` is null until they have one). Add `?reveal=1` for the password, which the audit log records. The Freesbe apps don't need it: they sign in on their own. Answer: ```json { "provider": "sipio", "account": { "username": "northwind1011", "domain": "northwind.sip.example", "createdAt": 1761200000000, "rotatedAt": null } } ``` ### POST /api/workspaces/{w}/phone-system/extensions/{userId}/desk-phone Who: Admins. Set up someone's desk phone login, or answer the one they have. The answer includes the password. ### GET /api/workspaces/{w}/phone-system/extensions/{userId}/call-handling Who: Admins. How someone's calls ring, like `GET /phone/call-handling` answers them. ### PUT /api/workspaces/{w}/phone-system/extensions/{userId}/call-handling Who: Admins. Set how someone's calls ring, like they can, and `locked` so they can't change it. They get a notice, and the audit log records it. Request: ```json { "handling": null, "locked": true } ``` ### PUT /api/workspaces/{w}/phone-system/extensions/{userId}/call-handling/devices/{id} Who: Admins. Turn one of someone's devices' ringing on or off, such as a lost phone. Request: ```json { "rings": false } ``` ### GET /api/workspaces/{w}/phone-system/call-handling Who: Admins. How calls ring for everyone who hasn't chosen their own: `handling`, like a person's. ### PUT /api/workspaces/{w}/phone-system/call-handling Who: Admins. Change the default. Its `noAnswer` is always `voicemail`. Everyone on it gets the new ring time. Request: ```json { "handling": { "mode": "all", "othersAfter": 10, "ringSeconds": 30, "steps": [], "noAnswer": { "kind": "voicemail" }, "inMeeting": "ring" } } ``` ### POST /api/workspaces/{w}/phone-system/extensions/{userId}/desk-phone/rotate Who: Admins. Give someone's desk phone login a new password. The old one stops working, so enter the new one on their phone. ### GET /api/workspaces/{w}/phone-system/ring-groups Who: Admins. The ring groups: each one's `memberIds` (who it rings, in order), `ring` (`all` at once, `inOrder` from the top, or `turns` starting after whoever answered last), `ringSeconds` (for everyone with `all`, for each person otherwise), `noAnswer` (where the call goes when nobody answers, an action like a menu's), `askFirst` (the person presses 1 to take the call), `callersHear` while it rings (`default` for the workspace's music on hold, `custom` for its uploaded music, a hold music track, or `ringing`), its `extension` (or null), the `numberIds` it answers and `usedBy`, what else sends calls to it. People who are busy or away are skipped. Answer: ```json { "ringGroups": [{ "id": "rg_3c…", "name": "Front desk", "memberIds": ["01JB8R…", "01JB8S…"], "ring": "inOrder", "ringSeconds": 15, "noAnswer": { "kind": "voicemail", "userId": "01JB8R…" }, "askFirst": false, "callersHear": "default", "extension": "601", "numberIds": ["did_1"], "usedBy": ["+14155550100", "Main menu, key 1"] }] } ``` ### POST /api/workspaces/{w}/phone-system/ring-groups Who: Admins. Make a ring group, with every field above but `id`, `numberIds` and `usedBy`: 1 to 20 `memberIds`, `ringSeconds` 5 to 120, and an `extension` of 2 to 6 digits not starting with 0 or 9 (409 when it's taken). Answers 201 with the group. A workspace can have 50. Request: ```json { "name": "Front desk", "memberIds": ["01JB8R…", "01JB8S…"], "ring": "inOrder", "ringSeconds": 15, "noAnswer": { "kind": "voicemail", "userId": "01JB8R…" }, "askFirst": false, "callersHear": "default", "extension": "601" } ``` ### PATCH /api/workspaces/{w}/phone-system/ring-groups/{id} Who: Admins. Change a ring group. Send it whole, like `POST`; an `extension` of null takes its extension away. Answers the group. ### DELETE /api/workspaces/{w}/phone-system/ring-groups/{id} Who: Admins. Delete a ring group and its extension. Answers 204, or 409 while a number, menu, queue, Call us link or another ring group sends calls to it. ### GET /api/workspaces/{w}/phone-system/desk-phones Who: Admins. The desk phones that set themselves up: each one's `mac` (12 lowercase hex digits), `modelId` (like `yealink-t48u`), who it belongs to (`userId`, or `sharedName` and its own `extension` for a shared phone), its `keys`, its `status` (`state` is `online`, `offline`, `waiting` for a phone not plugged in yet, or `needsYou`; when it last signed in and fetched its settings, its address and firmware, and `redirect`, how the brand's cloud redirect stands) and `claimUntil`, until when a new phone can first fetch its settings. `provisioningUrl` is the address for DHCP option 66 or for typing on a phone. Answer: ```json { "phones": [{ "id": "dp_2f…", "mac": "805ec03a127f", "modelId": "yealink-t48u", "userId": "01JB8R…", "sharedName": null, "extension": "104", "keys": [{ "index": 1, "type": "line", "label": "James" }, { "index": 2, "type": "person", "label": "Maya", "userId": "01JB8S…" }], "status": { "state": "online", "registeredAt": 1767600000000, "fetchedAt": 1767600000000, "ip": "203.0.113.24", "firmware": "66.86.0.15", "redirect": "registered" }, "claimUntil": null, "createdAt": 1767500000000 }], "provisioningUrl": "https://provision.freesbe.net/a/…/" } ``` ### POST /api/workspaces/{w}/phone-system/desk-phones Who: Admins. Add a desk phone by its `mac` (any common spelling) and `modelId`, for a person (`userId`, who must be on the phone system) or as a shared phone (`shared` with its `name` and an `extension` nobody has). `keys` are optional: each has an `index` from 1, a `type` (`line`, `person` with a `userId`, `shared` with an `extension`, `speedDial` with a `number`, `queue` with a `queueId`, `voicemail`, `dnd`, or `park` with a `slot` 1 to 9) and a `label` of up to 24 characters. Answers 201 with the phone, or 409 when the phone is already on the list or set up with another phone service. A workspace can have 500. Request: ```json { "mac": "80:5E:C0:3A:12:7F", "modelId": "yealink-t48u", "userId": "01JB8R…", "keys": [{ "index": 1, "type": "line", "label": "James" }] } ``` ### PATCH /api/workspaces/{w}/phone-system/desk-phones/{id} Who: Admins. Change a desk phone's `keys` (sent whole), or who it belongs to (`userId` or `shared`). Moving a phone to someone else gives it a new password. Answers the phone. ### POST /api/workspaces/{w}/phone-system/desk-phones/{id}/{action} Who: Admins. Ask a desk phone to `resync` (fetch its settings now), `restart`, take a `newLogin` (signs out and back in with a new password), or `extendClaim` (another hour to fetch its settings for the first time). Answers the phone. ### DELETE /api/workspaces/{w}/phone-system/desk-phones/{id} Who: Admins. Remove a desk phone: it signs out and stops getting settings. Answers 204. ### GET /api/workspaces/{w}/phone-system/queues Who: Admins. The call queues: each one's `memberIds` (who takes its calls, in order), `ring` (`longestIdle`, `all`, `turns`, `inOrder`, `fewestCalls` or `random`), `greeting` (like a menu's), `holdMusicId` (null for the standard music), `announcePosition` and `announceEverySeconds`, `ringSeconds`, `wrapUpSeconds`, `maxWaitMinutes` and `maxCallers` (null for no limit), where callers go `afterWait` and when `noOneAvailable` is signed in (actions, like a menu's), and the `numberIds` it answers. Answer: ```json { "queues": [{ "id": "q_4d…", "name": "Sales", "memberIds": ["01JB8R…", "01JB8S…"], "ring": "longestIdle", "greeting": { "recordingId": "media_7e…", "text": "Please hold, we'll be with you soon.", "voice": "1|en-US|Neural2-F" }, "holdMusicId": null, "announcePosition": true, "announceEverySeconds": 60, "ringSeconds": 20, "wrapUpSeconds": 15, "maxWaitMinutes": 10, "maxCallers": null, "afterWait": { "kind": "voicemail", "userId": "01JB8R…" }, "noOneAvailable": { "kind": "voicemail", "userId": "01JB8R…" }, "numberIds": ["did_1"] }] } ``` ### POST /api/workspaces/{w}/phone-system/queues Who: Admins. Make a call queue, with every field above but `id` and `numberIds`: 1 to 100 `memberIds`, `announceEverySeconds` 30 to 600, `ringSeconds` 5 to 120, `wrapUpSeconds` 0 to 600, `maxWaitMinutes` 1 to 120 and `maxCallers` 1 to 500 (or null). `holdMusicId` is a `recordingId` from uploading a file. New members are signed in to take its calls. Needs the Call center add-on (403 without it). Answers 201 with the queue. A workspace can have 30. Request: ```json { "name": "Sales", "memberIds": ["01JB8R…"], "ring": "longestIdle", "greeting": null, "holdMusicId": null, "announcePosition": true, "announceEverySeconds": 60, "ringSeconds": 20, "wrapUpSeconds": 15, "maxWaitMinutes": 10, "maxCallers": null, "afterWait": { "kind": "voicemail", "userId": "01JB8R…" }, "noOneAvailable": { "kind": "hangup" } } ``` ### PATCH /api/workspaces/{w}/phone-system/queues/{id} Who: Admins. Change a call queue. Send it whole, like `POST`. Answers the queue. ### DELETE /api/workspaces/{w}/phone-system/queues/{id} Who: Admins. Delete a call queue. Answers 204, or 409 while a number answers with it or a menu or another queue sends calls to it. ### GET /api/workspaces/{w}/phone-system/break-reasons Who: Admins. The reasons people in call queues pick for a break, each with a `name`, a `color` ("#RRGGBB", or null for gray) and a `limitMinutes` time limit (null for none). Answer: ```json { "reasons": [{ "id": "pr_2a…", "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 }] } ``` ### POST /api/workspaces/{w}/phone-system/break-reasons Who: Admins. Add a break reason: a `name` (40 characters at most), a `color`, and an optional `limitMinutes` (1 to 480; people are told when a break runs over it). Needs the Call center add-on. A workspace can have 20. Answers 201 with the reason. Request: ```json { "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 } ``` ### PATCH /api/workspaces/{w}/phone-system/break-reasons/{id} Who: Admins. Rename a break reason or change its color or time limit. Send the `name` and `color`; leaving out `limitMinutes` takes the limit off. Answers the reason. ### DELETE /api/workspaces/{w}/phone-system/break-reasons/{id} Who: Admins. Delete a break reason. Answers 204. ### GET /api/workspaces/{w}/phone-system/supervisors Who: Admins. Who besides admins can open the live Call center board and nudge people in call queues. Answer: ```json { "userIds": ["01J…"] } ``` ### PUT /api/workspaces/{w}/phone-system/supervisors Who: Admins. Replace the supervisors: up to 50 people in the workspace, not guests. Needs the Call center add-on. Answers like `GET`. Request: ```json { "userIds": ["01J…"] } ``` ### GET /api/workspaces/{w}/phone-system/call-center-report?from=2026-10-01&to=2026-10-31&tz=America/New_York&queues= Who: Admins. Call center numbers for a range of dates (up to 90 days, counted in `tz`): each queue's calls, answers, callers who hung up or timed out, service level, waits and talk time; calls per day; each person's answered calls and time talking, in wrap-up, on a break and signed in; and break time per reason. `queues` (comma-separated ids) narrows the queue numbers and days. Needs the Call center add-on. Answer: ```json { "from": "2026-10-01", "to": "2026-10-31", "allQueues": [{ "id": "q_1", "name": "Support" }], "queues": [{ "id": "q_1", "name": "Support", "calls": 2264, "answered": 1982, "hungUp": 226, "timedOut": 56, "serviceLevel": 0.83, "avgWaitSec": 41, "longestWaitSec": 287, "avgTalkSec": 356, "people": 4 }], "total": { "id": null, … }, "days": [{ "date": "2026-10-01", "calls": 211, "answered": 190, "missed": 21 }], "people": [{ "userId": "01J…", "answered": 412, "avgTalkSec": 291, "talkSec": 115800, "wrapUpSec": 3600, "breakSec": 14700, "signedInSec": 139800, "busy": 0.83 }], "breaks": [{ "reason": "Lunch", "minutes": 860, "people": 6 }] } ``` ### GET /api/workspaces/{w}/phone-system/call-log?from=2026-10-01&to=2026-10-31&tz=America/New_York&people=&direction=&result=&q=&minSeconds=&page=1&limit=50 Who: Admins. Every call on Freesbe's phone system for a range of dates (up to 90 days, counted in `tz`), newest first: who called whom, in, out or between colleagues, who answered a call that rang a queue or several people, and how long they talked. `people` (comma-separated user ids) keeps calls those people made, took or were rung for. `direction` is `in`, `out` or `internal`; `result` is `answered` or `missed`; `q` matches any number (three digits or more) on either end, or a name; `minSeconds` keeps longer calls. With `direction`, `result`, `q` or `minSeconds`, up to the newest 5,000 calls in the range are searched, and `searchedCalls` says when the search stopped there. `limit` is up to 500 a page; `more` says another `page` may have older calls. `recordingId` is set on recorded calls. Answer: ```json { "calls": [{ "id": "cdr_1", "startedAt": 1793536200000, "direction": "in", "from": { "userId": null, "name": null, "number": "+14155550142" }, "to": { "userId": "01J…", "name": "Support", "number": "+18135550111" }, "answeredBy": "01J…", "answered": true, "seconds": 184, "recordingId": "rec_1" }], "more": false } ``` ### GET /api/workspaces/{w}/phone-system/recordings/{id}/audio Who: Admins. A recorded call's audio (usually Ogg Opus), the whole file. 409 `not_ready` while the recording is still being processed. ### GET /api/workspaces/{w}/phone-system/recordings/{id}/transcript Who: Admins. A recorded call's transcript and summary, or `null` before one is made. `side` is `agent` for the person in the workspace and `caller` for the other end; `start` is seconds into the call. Answer: ```json { "transcript": { "state": "done", "summary": "The caller asked to move their delivery…", "sentiment": "positive", "keyPoints": ["…"], "actionItems": ["…"], "lines": [{ "side": "caller", "speaker": null, "start": 0.4, "text": "Hi, I'm calling about my order." }] } } ``` ### POST /api/workspaces/{w}/phone-system/recordings/{id}/transcript Who: Admins. Make the transcript and summary now, then answer it like the GET. 409 `transcription_disabled` until transcripts are turned on under Phone → Call recording. ### GET /api/workspaces/{w}/phone-system/flows Who: Admins. Each number's call flow in short: the steps right after the number (`first`), how many steps it has, and who changed it last and when. Answer: ```json { "flows": [{ "numberId": "did_4…", "number": "+12065550191", "label": "Customer line", "first": [{ "kind": "menu", "name": "Main menu" }], "steps": 10, "updatedAt": "2026-10-30T14:02:11.000Z", "updatedBy": "Maya Chen" }] } ``` ### GET /api/workspaces/{w}/phone-system/flows/{numberId} Who: Admins. What happens when someone calls the number, drawn from its routing now. Each step has a `key` (`ivr:`, `queue:`, `ring_group:`, `user:`, `voicemail:`, `announcement:`, `hangup:`), a `kind` (`number`, `menu`, `queue`, `people`, `message`, `voicemail`, `hangup` or `other`) and its `exits`; each link joins an exit to the next step. `version` changes whenever the routing does. Answer: ```json { "numberId": "did_4…", "number": "+12065550191", "label": "Customer line", "version": ""9f…"", "steps": [{ "key": "did:did_4…", "kind": "number", "name": "+12065550191", "exits": [{ "id": "default", "label": "Open" }, { "id": "closed", "label": "After hours" }, { "id": "holiday", "label": "Closed days" }], "position": null }, { "key": "ivr:ivr_1…", "kind": "menu", "name": "Main menu", "refId": "ivr_1…", "keys": true, "exits": [{ "id": "digit:1", "label": "Key 1" }, { "id": "timeout", "label": "No key pressed" }], "position": { "x": 320, "y": 0 } }], "links": [{ "from": "did:did_4…", "exit": "default", "to": "ivr:ivr_1…" }] } ``` ### PUT /api/workspaces/{w}/phone-system/flows/{numberId} Who: Admins. Change where the number's calls go: send every step's `key`, every link, where each step sits on the canvas, and the `version` you read. Live at once. A link from a menu's new `digit:` exit adds that key. 409 when the routing changed since `version`; 400 when a step has no way in or a line can't go where it's drawn. Answers the flow as saved. Request: ```json { "version": ""9f…"", "steps": ["did:did_4…", "ivr:ivr_1…", "hangup:"], "links": [{ "from": "did:did_4…", "exit": "default", "to": "ivr:ivr_1…" }, { "from": "ivr:ivr_1…", "exit": "timeout", "to": "hangup:" }], "positions": { "ivr:ivr_1…": { "x": 320, "y": 0 } } } ``` ### GET /api/workspaces/{w}/phone-system/flow-steps Who: Admins. A menu or queue to add to a flow, `?key=ivr:` or `?key=queue:`, with every step after it as it routes now, so saving the flow keeps its keys and exits. Answer: ```json { "steps": [{ "key": "ivr:ivr_2…", "kind": "menu", "name": "Deliveries", "keys": true, "exits": [{ "id": "digit:1", "label": "Key 1" }, { "id": "timeout", "label": "No key pressed" }], "position": null }], "links": [{ "from": "ivr:ivr_2…", "exit": "digit:1", "to": "user:u_7…" }] } ``` ### POST /api/workspaces/{w}/phone-system/flow-steps Who: Admins. Make a new step for a flow: `people` rings `userIds` at once, `voicemail` takes a message for `userId`, and `message` reads out `text` in `voice`, then goes on. Answers 201 with the step, to place in a flow and save. Request: ```json { "kind": "message", "text": "We're closed for the holiday.", "voice": "1|en-US|Neural2-F" } ``` Answer: ```json { "key": "announcement:ann_3…", "kind": "message", "name": "We're closed for the holiday.", "exits": [{ "id": "default", "label": "Then" }], "position": null } ``` ### GET /api/workspaces/{w}/phone-system/recording Who: Admins. Which calls the phone system records: `record` is `off`, `everyone` or `some` (the people in `userIds`), `calls` says which of their calls, and `queues` records every call queue call. Also whether recordings are transcribed and summarized with AI (counted toward the plan's transcript hours), whether people may press Transcribe in their own calls (`selfTranscribe`), how many days they're kept (`0` for good), and `lineUserIds`, the people with a phone line who can be picked. Answer: ```json { "record": "everyone", "calls": { "incoming": true, "outgoing": true, "internal": false }, "queues": true, "userIds": [], "transcribe": true, "summarize": true, "selfTranscribe": true, "keepDays": 365, "lineUserIds": ["01J…"] } ``` ### PUT /api/workspaces/{w}/phone-system/recording Who: Admins. Change the call recording settings, sending them all as GET answers them, without `lineUserIds`. `keepDays` is one of 30, 90, 365, 730 or 0, and applies to calls recorded from then on. Answers the settings as saved. Request: ```json { "record": "some", "calls": { "incoming": true, "outgoing": false, "internal": true }, "queues": false, "userIds": ["01J…"], "transcribe": true, "summarize": false, "selfTranscribe": false, "keepDays": 90 } ``` ### GET /api/workspaces/{w}/phone-system/hold-music Who: Admins. The music on hold callers hear: `track` is `lofi` (until someone picks), `jazz`, `classical`, `bossa`, `opus`, `pop`, `nature`, `quiet` (a soft hiss, no music), or `custom` for the workspace's own upload. `custom` is that upload (kept while a track plays), or null. People who picked their own play theirs on their calls. Answer: ```json { "track": "custom", "custom": { "recordingId": "media_…", "name": "Summer jingle.mp3" } } ``` ### PUT /api/workspaces/{w}/phone-system/hold-music Who: Admins. Change the workspace's music on hold: a `track`, or `custom` with a `recordingId` uploaded with `POST /phone-system/greetings` and its file `name`. A new upload replaces (and deletes) the last one. It plays on every call from then on. Answers like `GET`. Request: ```json { "track": "classical" } ``` ### GET /api/workspaces/{w}/phone-system/voices Who: Admins. The voices a typed greeting can be read in, in US and Canadian English, US Spanish and Canadian French. Answer: ```json { "voices": [{ "id": "1|en-US|Neural2-F", "language": "en-US", "name": "Neural2-F", "gender": "female" }] } ``` ### GET /api/workspaces/{w}/phone-system/greetings Who: Admins. Hear a typed greeting before saving it: `?text=` read in `?voice=`. Answers the audio (WAV). ### POST /api/workspaces/{w}/phone-system/greetings Who: Admins. Upload a recorded greeting as a WAV file in the multipart field `audio` (10 MB at most). Answers 201 with the `recordingId` to save in a menu, or as a queue's greeting or hold music. Answer: ```json { "recordingId": "media_4b…", "seconds": 8 } ``` ### GET /api/workspaces/{w}/phone-system/greetings/{id}/audio Who: Admins. Play a menu's greeting. Answers the audio. ### GET /api/workspaces/{w}/phone-system/hours Who: Admins. The workspace's business hours, or `null` until they're set: a `timezone`, open windows by `day` (0 = Sunday) with `open` and `close` times, and `holidays`: each a `date` with its `name`, closed all day when `open` and `close` are null, otherwise open only then. `closedDates` lists the holidays closed all day. Answer: ```json { "hours": { "timezone": "America/New_York", "days": [{ "day": 1, "open": "09:00", "close": "17:00" }], "closedDates": ["2026-12-25"], "holidays": [{ "date": "2026-12-24", "name": "Christmas Eve", "open": "09:00", "close": "13:00" }, { "date": "2026-12-25", "name": "Christmas Day", "open": null, "close": null }] } } ``` ### PUT /api/workspaces/{w}/phone-system/hours Who: Admins. Set the business hours, replacing them all. `close` can be `24:00` for midnight. Send `holidays` (up to 100) to name days and give them special hours; without it, `closedDates` become unnamed holidays closed all day. Answers the hours. Request: ```json { "timezone": "America/New_York", "days": [{ "day": 1, "open": "09:00", "close": "17:00" }], "closedDates": ["2026-12-25"], "holidays": [{ "date": "2026-12-25", "name": "Christmas Day", "open": null, "close": null }] } ``` ### GET /api/workspaces/{w}/phone-system/hours/public-holidays?country=US Who: Admins. A country's nationwide public holidays in the next 12 months, to add to the business hours: each a `date`, its English `name` and its `localName`. An empty list for a country the holiday calendar doesn't cover. Answer: ```json { "country": "US", "holidays": [{ "date": "2026-12-25", "name": "Christmas Day", "localName": "Christmas Day" }] } ``` ### GET /api/workspaces/{w}/phone-system/emergency Who: Admins. Emergency addresses (E911): the workspace's `locations` with how many `people` work from each, the `defaultLocationId` used for everyone who hasn't picked one, and `alertChannelId`, the channel told when someone dials 911 (null: each admin gets a message from Phone). A location's `status` is `saved` until it's registered with the carrier (`registration` false means that isn't turned on yet), then `pending`, `confirmed`, or `needs_fixing` with a `problem`. Answer: ```json { "registration": true, "defaultLocationId": "01JC…", "alertChannelId": null, "locations": [{ "id": "01JC…", "name": "Main office", "ownerId": null, "status": "confirmed", "problem": null, "people": 12, "address": { "line1": "500 Market St", "line2": "Floor 4", "city": "San Francisco", "region": "CA", "postalCode": "94105", "country": "US" } }] } ``` ### PUT /api/workspaces/{w}/phone-system/emergency Who: Admins. Change the `defaultLocationId` (one of the workspace's locations) or `alertChannelId` (a channel, or null for the admins). Answers like `GET`. Request: ```json { "alertChannelId": "01JC…" } ``` ### POST /api/workspaces/{w}/phone-system/emergency/locations Who: Admins. Add an emergency address in the United States or Canada: a `name` and the street `address` with its number, two-letter `region` and ZIP or postal code. The first one becomes the default. Answers 201 with the location. Request: ```json { "name": "Main office", "address": { "line1": "500 Market St", "line2": "Floor 4", "city": "San Francisco", "region": "CA", "postalCode": "94105", "country": "US" } } ``` ### PATCH /api/workspaces/{w}/phone-system/emergency/locations/{id} Who: Admins. Change a location's name or address, like `POST`. It's checked with the carrier again. ### DELETE /api/workspaces/{w}/phone-system/emergency/locations/{id} Who: Admins. Remove a location. 409 for the default, or while people work from it. Answers 204. ### GET /api/workspaces/{w}/phone/queue-status Who: Members, not guests. Your place in the call queues: the `queues` you're in (empty when none, or without the Call center add-on), `status` (`taking`, `break` or `out`), the `breakReasonId` of a break, `since` when, `phoneOffline` when you take calls but your Freesbe phone isn't open anywhere, and the `breakReasons` you can pick. Answer: ```json { "queues": [{ "id": "q_4d…", "name": "Support" }], "status": "break", "breakReasonId": "pr_2a…", "since": 1767000000000, "phoneOffline": false, "breakReasons": [{ "id": "pr_2a…", "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 }] } ``` ### PUT /api/workspaces/{w}/phone/queue-status Who: Members, not guests. Take your queues' calls (`taking`), take a `break` (with a `breakReasonId` or null), or sign out (`out`). Calls to your own number ring either way. 409 when you're in no queue. Answers your status. Request: ```json { "status": "break", "breakReasonId": "pr_2a…" } ``` ### GET /api/workspaces/{w}/phone/call-center Who: Members, not guests. The live Call center board, for admins and supervisors (403 for anyone else, or without the Call center add-on): the `queues` with their people, `breakReasons`, and `live`: each queue's callers waiting and numbers for today, everyone's state and how long they've been in it, and the callers waiting (times in seconds). `agentIds` maps the ids in `live` to people. `socket` is a one-time, one-minute `wss://` URL that sends a fresh `live` as a `snapshot` frame whenever something changes; null when live updates aren't available. Answer: ```json { "queues": [{ "id": "q_4d…", "name": "Support", "memberIds": ["01J…"] }], "agentIds": { "us_9f…": "01J…" }, "breakReasons": [], "live": { "as_of": 1767000000000, "queues": [], "total": {}, "agents": [], "waiting": [] }, "socket": { "url": "wss://…", "expiresAt": 1767000060000 } } ``` ### POST /api/workspaces/{w}/phone/call-center/socket Who: Members, not guests. A new one-time `socket` URL for the Call center board, to reconnect. Admins and supervisors only. Answer: ```json { "socket": { "url": "wss://…", "expiresAt": 1767000060000 } } ``` ### POST /api/workspaces/{w}/phone/call-center/nudge Who: Members, not guests. Nudge someone in a call queue with a direct message (`message`, 500 characters at most), `from` you (`me`) or from the Call center bot (`bot`). Admins and supervisors only. Answers 204. Request: ```json { "userId": "01J…", "message": "Callers are waiting. Can you come back from your break?", "from": "bot" } ``` ### POST /api/workspaces/{w}/phone/call-center/supervise Who: Members, not guests. Join someone's live call from the Call center board, listening: you hear the call and nobody hears you. `callId` is the call in `live.agents[].current.call_id`. The phone service rings your Freesbe phone, which answers on its own; switch to whispering or barging in with `/supervise/mode`. Admins and supervisors only, with a phone. 409 with a message when it can't be done, such as when calling from Freesbe isn't on, the call has ended, or you're already on another. Answers 204. Request: ```json { "callId": "c_1f…" } ``` ### POST /api/workspaces/{w}/phone/call-center/supervise/mode Who: Members, not guests. Switch how you're on a call you joined from the board, without leaving it: `listen` (nobody hears you), `whisper` (only the person in the queue hears you) or `barge` (everyone hears you). Answers 204. Request: ```json { "callId": "c_1f…", "mode": "whisper" } ``` ### DELETE /api/workspaces/{w}/phone/call-center/supervise?callId=c_1f… Who: Members, not guests. Leave a call you joined from the board. The call goes on without you. Answers 204. ### GET /api/workspaces/{w}/phone/directory Who: Members, not guests. The colleagues with a phone, by name, with their `extension` (null when they have none) and Freesbe `userId` (null for a phone system user who isn't a member), and the call `queues` (empty without the Call center add-on): who the phone can call and pass calls to. Answer: ```json { "people": [{ "userId": "01J…", "name": "James Okafor", "extension": "102" }], "queues": [{ "id": "q_4d…", "name": "Support" }] } ``` ### POST /api/workspaces/{w}/phone/transfer Who: Members, not guests. Pass your current call on. `start` with `mode` `now` (the caller holds until the target answers, then goes to them; nobody answering leaves the caller with you) or `talk` (you talk to the target first while the caller holds), to a `target`: `{ kind: "person", userId }`, `{ kind: "queue", id }` (always `now`) or `{ kind: "number", number }` (a colleague's extension goes to them). Talking first answers a `transferId`; then `complete` hands the caller over and ends your side, `merge` makes it a 3-way call, and `cancel` goes back to the caller. 409 with a message when it can't be done, such as when you're not on a call or the target hasn't answered yet. Request: ```json { "action": "start", "mode": "talk", "target": { "kind": "person", "userId": "01J…" } } ``` Answer: ```json { "transferId": "call_7f…" } ``` ### GET /api/workspaces/{w}/phone/voicemails Who: Members, not guests. Your own voicemails, newest first: who called (`number`, and `name` for a colleague or contact; a colleague calling from Freesbe has their `userId` and no `number`), when (`at`), how long (`seconds`), whether it's `unread`, what they said (`transcript`, null until it's written out or when transcripts are off; `writing` while it's being written out), and the `unread` count. A new one reaches your devices as a `phone.voicemails_changed` event: read the list again. Answer: ```json { "voicemails": [{ "id": "vmsg_3c…", "number": "+14155550187", "name": null, "userId": null, "at": 1767000000000, "seconds": 42, "unread": true, "transcript": "Hi, it's Dana from Acme Corp…", "writing": false }], "unread": 1 } ``` ### PATCH /api/workspaces/{w}/phone/voicemails/{id} Who: Members, not guests. Mark one of your voicemails heard (`unread: false`) or not. Answers 204. Request: ```json { "unread": false } ``` ### DELETE /api/workspaces/{w}/phone/voicemails/{id} Who: Members, not guests. Delete one of your voicemails. The copy in your DM from Phone stays. Answers 204. ### GET /api/workspaces/{w}/phone/voicemails/{id}/audio Who: Members, not guests. The recording, as WAV. Playing it marks it heard, on all your devices. ### GET /api/workspaces/{w}/phone/emergency Who: Members, not guests. Where your 911 calls say you are: the workspace's `locations` and your own, the one you picked (`locationId`, null for the workspace's `defaultLocationId`). ### PUT /api/workspaces/{w}/phone/emergency Who: Members, not guests. Say where you work from: `locationId` (one of the workspace's or your own, null for the default), or `own` to add a place of yours, like Home, and pick it (up to 5). Answers like `GET`. Request: ```json { "own": { "name": "Home", "address": { "line1": "12 Oak Ave", "city": "Oakland", "region": "CA", "postalCode": "94610", "country": "US" } } } ``` ### GET /api/workspaces/{w}/phone/hold-music Who: Members, not guests. The music your callers hear on hold: your own `track`, or null for the workspace's, which is `workspaceTrack` (`custom` for its own upload, named in `workspaceCustomName`). Answer: ```json { "track": null, "workspaceTrack": "jazz", "workspaceCustomName": null } ``` ### PUT /api/workspaces/{w}/phone/hold-music Who: Members, not guests. Pick your own music on hold (one of the tracks the workspace's endpoint lists), or null to go back to the workspace's. Answers like `GET`. Request: ```json { "track": "bossa" } ``` ### GET /api/workspaces/{w}/phone/call-handling Who: Members, not guests. How your calls ring (Follow-Me). `handling.mode` is `working` (the device you're using first, the rest after `othersAfter` seconds), `all` or `order` (`steps`, each with the devices that ring together, `computer`, `mobile` or `desk`, and its `seconds`). `ringSeconds` is how long `working` and `all` ring. `noAnswer` is `voicemail`, a teammate (`person` and `userId`) or an outside `number` with its `seconds`, which asks you to press 1 to take the call. `inMeeting` is `ring` or `skipComputer`. `own` is false while you follow the workspace's `workspaceDefault`, `locked` when an admin locked it, and `devices` are the devices your calls ring, each with `rings`. Answer: ```json { "handling": { "mode": "working", "othersAfter": 10, "ringSeconds": 25, "steps": [], "noAnswer": { "kind": "voicemail" }, "inMeeting": "ring" }, "own": false, "locked": false, "workspaceDefault": { "mode": "working", … }, "noAnswerPerson": null, "devices": [{ "id": "computer", "kind": "computer", "name": "", "usedAt": null, "rings": true }] } ``` ### PUT /api/workspaces/{w}/phone/call-handling Who: Members, not guests. Choose how your calls ring, or `null` to follow the workspace's default again. Steps and ring times are 5 to 120 seconds, up to 5 steps and 3 minutes in all. 403 when an admin locked it. Answers like `GET`. Request: ```json { "handling": { "mode": "order", "othersAfter": 10, "ringSeconds": 25, "steps": [{ "ring": ["computer"], "seconds": 10 }, { "ring": ["mobile", "desk"], "seconds": 15 }], "noAnswer": { "kind": "number", "number": "+12065550142", "seconds": 20 }, "inMeeting": "ring" } } ``` ### PUT /api/workspaces/{w}/phone/call-handling/devices/{id} Who: Members, not guests. Turn one of your devices' ringing on or off: `computer`, `desk`, or a phone's `id` from `devices`. Answers like `GET`. Request: ```json { "rings": false } ``` ### GET /api/workspaces/{w}/phone/voicemail Who: Members, not guests. Your voicemail on the phone system from Freesbe, set up for you on its own: `ready` once your box takes messages, `email` when each voicemail is also emailed to you with the recording attached, the `address` it goes to, and your `greeting`: the `standard` one's words and `mine`, your own (`recordingId`, with `text` and `voice` when typed), or null for the standard one. Answer: ```json { "ready": true, "email": false, "address": "maya@northwind.example", "greeting": { "standard": "You've reached Maya Chen at extension 101. Maya isn't available right now. Please leave a message after the tone.", "mine": null } } ``` ### PUT /api/workspaces/{w}/phone/voicemail Who: Members, not guests. Have each voicemail emailed to you too (`email: true`), written in `locale` (English when left out), or keep them in Freesbe only. Voicemails always arrive in your DM from Phone and the Voicemail tab. Answers like `GET`. Request: ```json { "email": true, "locale": "en" } ``` ### PUT /api/workspaces/{w}/phone/voicemail/greeting Who: Members, not guests. Set what callers hear before the beep: typed words read in a voice (`text`, up to 1,000 characters, and a `voice` from `GET /phone/voices`), a recording you uploaded (`recordingId`), or null for the standard greeting. Typed and uploaded greetings count toward the plan's phone greetings a day. Answers like `GET /phone/voicemail`. Request: ```json { "greeting": { "text": "Hi, this is Maya. Leave a message after the tone.", "voice": "1|en-US|Neural2-F" } } ``` ### POST /api/workspaces/{w}/phone/voicemail/greeting Who: Members, not guests. Upload a recording for your greeting: multipart `audio`, a WAV file of up to 10 MB. Answers 201 with its `recordingId` to save with `PUT`. Answer: ```json { "recordingId": "media_01J…", "seconds": 9 } ``` ### GET /api/workspaces/{w}/phone/voicemail/greeting Who: Members, not guests. Hear typed words in a voice before saving them: `?text=…&voice=…`. Answers audio/wav. ### GET /api/workspaces/{w}/phone/voicemail/greeting/audio Who: Members, not guests. Your greeting as callers hear it now, or `?id=` a recording you just uploaded. Answers audio/wav. ### GET /api/workspaces/{w}/phone/voices Who: Members, not guests. The voices a typed greeting can be read in, each with its `id`, `language`, `name` and `gender`. Answer: ```json { "voices": [{ "id": "1|en-US|Neural2-F", "language": "en-US", "name": "Neural2-F", "gender": "female" }] } ``` ### DELETE /api/workspaces/{w}/phone/emergency/locations/{id} Who: Members, not guests. Remove one of your own places. If you worked from it, your calls use the workspace's default again. Answers like `GET`. ### DELETE /api/workspaces/{w}/phone-system/numbers/{id} Who: Admins. Release a number back to the carrier. Calls and texts to it stop, and you may not get it again. Answers 204. ## Slack import An admin uploads a Slack export zip in parts, then starts the import. Imported people get an account that waits for them; a sign-in link lets each one take theirs over. ### GET /api/workspaces/{w}/imports Who: Admins. Imports, newest first, with their status, phase and counts. ### POST /api/workspaces/{w}/imports Who: Admins. Start an upload: the zip's `fileName` and `size`. The answer's `partSize` is how big each part is. Request: ```json { "fileName": "Northwind Slack export.zip", "size": 734003200 } ``` ### GET /api/workspaces/{w}/imports/{id} Who: Admins. One import and its progress. ### PUT /api/workspaces/{w}/imports/{id}/parts/{part} Who: Admins. Upload part 1, 2, 3… of the zip as raw bytes. Every part but the last is exactly `partSize`. Answers `partNumber` and `etag`. ### POST /api/workspaces/{w}/imports/{id}/start Who: Admins. The upload is complete: send every part's `partNumber` and `etag`, and the import starts. Request: ```json { "parts": [ { "partNumber": 1, "etag": "…" }, { "partNumber": 2, "etag": "…" } ] } ``` ### POST /api/workspaces/{w}/imports/{id}/cancel Who: Admins. Stop an upload or a running import. ### POST /api/workspaces/{w}/claims Who: Admins. Sign-in links for imported people who haven't signed in: everyone, or `userIds`. `email: true` also emails each person theirs. Request: ```json { "email": true, "locale": "en" } ``` ### GET /api/claims/{token} Who: No token. What a sign-in link shows: the workspace, the person's name and email from Slack, and whether that email already has an account. ### POST /api/auth/claim-email Who: No token. Email a sign-in link if the address belongs to an imported account. Answers 204 either way. ### POST /api/auth/claim Who: No token. Take over an imported account: the link's `token`, a `password`, and an `email` when Slack had none. Answers like login. ### POST /api/claims/{token}/accept Who: Signed in. Add the imported account's history to the account you're signed in to. ## Realtime ### WS /api/workspaces/{w}/rt Who: Members. The workspace WebSocket: every change as it happens. See [Realtime events](#realtime) above.