Developers
Freesbe API
Everything a person or an admin can do in Freesbe, they can do from a script: post and read messages, run channels and DMs, invite people and guests, schedule meetings, share booking links, keep the punch clock, and manage the workspace. It's the same API the Freesbe apps use, plus Slack-compatible webhooks and a live event stream.
Overview
- Base URL
https://api.freesbe.net- Format
- JSON in and out, unless an endpoint says otherwise
- Auth
Authorization: Bearer <token>- Times
- Milliseconds since the Unix epoch
- IDs
- Opaque strings that sort by creation time
- Language
- Emails and calendar invites Freesbe sends for you are written in the language of
X-Freesbe-Locale(orAccept-Language):en,es,zh-CN,hi,ar,pt-BR,fr,de,ja,ru,he,it,nl,pl,tr,ko,id,vi,th,uk,sv,zh-TW,cs,roorms. Error messages are in English. - Endpoints
- 588, in the reference below
Paths below are relative to the base URL. In the paths, {w} is a workspace id, {c} a channel id (DMs are channels too) and{m} a message id. Message text uses Slack-style mrkdwn:*bold*, _italic_, `code`,<https://example.com|links>, <@userId> mentions and:emoji: shortcodes.
Authentication
For an integration, make an app: its bot token is an API key that posts as the app, only reaches what its scopes allow, and doesn't expire. A script can also sign in like a person does, then send the token it gets back as a bearer token; it then acts as that person.
curl -X POST https://api.freesbe.net/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email": "deploy-bot@northwind.example", "password": "…", "wantToken": true}'{
"user": { "id": "01J…", "email": "deploy-bot@northwind.example", "name": "Deploy bot", … },
"workspaces": [{ "id": "01J…", "name": "Northwind Traders", "slug": "northwind", "role": "member" }],
"canCreateWorkspaces": false,
"token": "kX3q…"
}Send the token on every request as Authorization: Bearer <token>. It lasts 30 days from the last time it was used, so a script that runs at least monthly stays signed in. POST /api/auth/logout with the token ends it, and so do changing the account's password and signing it out fromits sessions. An emailed code (/api/auth/email-code) works instead of a password, too.
A workspace can require more than a password. If it requires multi-factor authentication, set up an authenticator app for the account once (POST then PUT /api/me/totp), and after each sign-in send a code to POST /api/me/mfa/totp. If it requires Google or Microsoft sign-in, a password token can't use it; see Not in the API yet.
https://api.freesbe.net only accepts bearer tokens, never the app's sign-in cookie, so any website may call it from the browser: CORS is open to every origin. Keep tokens on a server when the page is public.
const res = await fetch("https://api.freesbe.net/api/workspaces", {
headers: { Authorization: `Bearer ${token}` },
});
const { workspaces } = await res.json();Who can call what
A token acts as its account, with that account's role in each workspace: owner, admin, member or guest. Each endpoint in the reference is labelled with who may call it.
| Label | Who |
|---|---|
| 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 link. |
| 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. |
| Freesbe staff | Only the people who run the server, signed in to the operator console. Everyone else gets 404. |
The owner can do everything an admin can, and can't be demoted or deactivated. Guests see only the channels they were invited to and the people in them; they can post, react, upload and DM those people, but can't create channels, invite anyone or schedule meetings.
Apps and bot tokens
An app is an integration that belongs to one workspace. Admins make one inWorkspace settings → Integrations & apps → Integrations & apps → Apps (or with POST /api/workspaces/{w}/apps) from a manifest shaped like Slack's, in YAML or JSON. A Slack app's manifest works as it is: Slack scope names are mapped to Freesbe's, and anything Freesbe doesn't have yet is listed and left out.
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:writeEach app gets a bot user, which shows up in the people list with an APP badge and posts the app's messages, and a bot token that starts with fsb-bot-. Add the bot to a private channel like a person, or let it join public channels itself with channels:join. Unlike a person's token, a bot token doesn't expire, isn't held to the workspace's Google, Microsoft or MFA sign-in rules, and doesn't take a seat. Regenerate it from the app's page if it leaks; the old one stops working at once.
curl https://api.freesbe.net/api/channels/01JB8S…/messages \
-H "Authorization: Bearer fsb-bot-…" \
-H "Content-Type: application/json" \
-d '{ "body": "New order from Acme Corp",
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "Order #4471" } },
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Customer*\nAcme Corp" },
{ "type": "mrkdwn", "text": "*Total*\n$1,240" } ] }
] }'A bot token only reaches the endpoints its scopes cover; each endpoint in the reference shows the scope it needs. Anything else answers 403 missing_scope, and endpoints apps can't use 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 |
phone:call | Start phone calls: ring someone's phone, then dial the number |
app_mentions:read | Hear about messages that mention the app |
App events
An app can get events too. Its manifest says where they go and which ones, the way a Slack manifest does; Slack's event names, such as message.channels,app_mention and team_join, map to Freesbe's. An app only hears what its bot could see: messages and reactions in channels and DMs it is in, changes to public channels, people, huddles in channels it can see, and meetings it is part of. It never hears about what its own bot did.
settings:
event_subscriptions:
request_url: https://orders.northwind.example/freesbe/events
bot_events:
- message.channels
- app_mention
- reaction_addedWhen the request URL is saved, Freesbe checks it like Slack does: it POSTs{ "type": "url_verification", "challenge": "…" }, and your server answers with the challenge, as the body or as { "challenge": "…" }. Then each event arrives in Slack's Events API envelope, with the event's fields insideevent:
POST https://orders.northwind.example/freesbe/events
Content-Type: application/json
Freesbe-Event: message.created
Freesbe-Signature: t=1792737000,v1=5f1c…
X-Slack-Request-Timestamp: 1792737000
X-Slack-Signature: v0=a2b9…
{
"type": "event_callback",
"team_id": "01JB8R…",
"api_app_id": "01JC2D…",
"event_id": "01JC2F…",
"event_time": 1792737000,
"authorizations": [{ "user_id": "01JC2E…", "team_id": "01JB8R…", "is_bot": true }],
"event": {
"type": "message.created",
"event_ts": "1792737000.000000",
"message": { "id": "01JC2F…", "channelId": "01JB8S…", "body": "Order #4471 is packed", … },
"channel": { "id": "01JB8S…", "name": "warehouse", "kind": "public", … }
}
}Every request is signed twice with the app's signing secret (in the app's menu, underShow signing secret): Freesbe-Signature the same way as outgoing webhooks, and X-Slack-Signature, the hex HMAC-SHA256 ofv0:<timestamp>:<raw body>, so Slack's SDKs check it for you. Retries and the delivery log work as for outgoing webhooks, atGET /api/workspaces/{w}/apps/{id}/deliveries.
Quick start
export FREESBE_TOKEN=… # from the login above
API=https://api.freesbe.net
# Your workspaces, then the channels in one of them
curl -H "Authorization: Bearer $FREESBE_TOKEN" $API/api/workspaces
curl -H "Authorization: Bearer $FREESBE_TOKEN" $API/api/workspaces/$WORKSPACE/channels
# Post a message
curl -X POST $API/api/channels/$CHANNEL/messages \
-H "Authorization: Bearer $FREESBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"body": "Release *2.4* is out :tada:"}'Errors and limits
Errors answer with a 4xx or 5xx status and this body:
{ "error": { "code": "forbidden", "message": "Only admins can do that" } }| 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. |
Sign-in allows 10 attempts a minute per address and per email. Each incoming webhook takes up to 60 posts a minute. Each person or token can send 60 messages, add 120 reactions, make 20 channels, meetings, webhooks, apps or workflows, upload 30 files and search 60 times a minute. All answer 429 when you go over. Emails Freesbe sends for you, such as invites and codes, have daily caps per address and per workspace. SeeUsage limits.
Messages and files
Reading
GET /api/channels/{c}/messages returns the newest 50 top-level messages, oldest first, with hasMore when there are older ones. Page with?before=<id> or ?after=<id>, jump to a message with?around=<id>, and set ?limit= from 1 to 100. Thread replies come from GET /api/messages/{m}/replies.
Posting
POST /api/channels/{c}/messages answers 201 with the newmessage. Only body is required. The account has to be in the channel: join a public one first with POST /api/channels/{c}/join.
{
"body": "Replying in the thread",
"threadRootId": "01J…", // optional: makes it a thread reply
"clientMsgId": "deploy-8812", // optional: retries with the same id post once
"fileIds": ["01J…"] // optional: files uploaded first, at most 10
}Files
Upload the raw bytes first, then attach the returned id to a message within a day. Files that are never attached are deleted.
curl -X POST "$API/api/workspaces/$WORKSPACE/files?name=report.pdf" \
-H "Authorization: Bearer $FREESBE_TOKEN" \
-H 'Content-Type: application/pdf' \
--data-binary @report.pdf
# → 201 { "file": { "id": "01J…", "name": "report.pdf", … } }Realtime events
To react to what happens in a workspace as it happens, open its WebSocket atwss://api.freesbe.net/api/workspaces/{w}/rt with the bearer token. It's how the apps stay live, and unlike outgoing webhooks it covers DMs and private channels the account is in, presence, statuses, huddles and the punch clock.
import WebSocket from "ws";
let seq = 0; // the newest event you've handled; start from /boot's `seq`
function connect() {
const ws = new WebSocket(
`wss://api.freesbe.net/api/workspaces/${WORKSPACE}/rt?since=${seq}`,
{ headers: { Authorization: `Bearer ${process.env.FREESBE_TOKEN}` } },
);
const ping = setInterval(() => ws.send('{"t":"ping"}'), 25_000);
ws.on("message", (raw) => {
const frame = JSON.parse(raw);
if (frame.t === "resync") return ws.close(); // too far behind: reload /boot, then reconnect
if (frame.t !== "event") return;
if (frame.seq) seq = frame.seq;
if (frame.e.type === "message.created") console.log(frame.e.message.body);
});
ws.on("close", () => {
clearInterval(ping);
setTimeout(connect, 2_000);
});
}
connect();The first frame is a hello with the newest event number,seq. Events come as { "t": "event", "seq": …, "e": … }. Reconnect with ?since=<seq> to get what you missed; if that's too far back you get { "t": "resync" }, so loadGET /api/workspaces/{w}/boot again and use its seq. Send{"t":"ping"} every 25 seconds: a socket that's quiet for 90 seconds is closed. Close code 4001 means the token was signed out and 4003 that the account left the workspace.
{ "t": "hello", "seq": 18234, "connId": "01JBF0…" }
{ "t": "event", "seq": 18235, "e": { "type": "message.created", "message": { … } } }
{ "t": "event", "e": { "type": "typing", "channelId": "01JB8S…", "userId": "01JB8R…", "threadRootId": null } }| Event | Holds |
|---|---|
message.created | message, and thread counters for a reply |
message.updated | message |
message.deleted | channelId, messageId, threadRootId, thread |
reaction.added / reaction.removed | channelId, messageId, emoji, userId |
pins.changed / bookmarks.changed | channelId: fetch its pins or bookmarks again |
channel.created / channel.updated | channel |
channel.prefs | Your own muted, notify and sound changed, on another device |
person.sound | You picked a sound for messages from userId, on another device |
member.joined / member.left | channel, userId |
workspace.member_added / workspace.member_updated | member |
presence.changed | userId, state (active, away, offline), inHuddle |
status.changed | status |
typing | channelId, userId, threadRootId. Not replayed. |
read.updated | You read channelId up to lastReadId on another device |
saved.changed | Your Later list changed, on another device |
reminder.due | A reminder went off: the saved item, with its message |
huddle.started / huddle.updated | huddle |
huddle.ended | huddleId, channelId |
huddle.ring | A DM call for you: huddleId, channelId, fromUserId, expiresAt |
huddle.declined | huddleId, channelId, userId |
time.changed | Someone's punch clock (sent to them and to admins) |
time.settings_changed | An admin changed the punch clock settings |
devices.changed | Which of your open apps alerts for messages. Not replayed. |
keywords.changed | keywords: your keyword alerts. Only to you. |
emoji.changed | nothing: fetch the workspace's custom emoji again |
sidebar.sections | sections: all your sidebar sections, in order. Only to you. |
channel.section | channelId, sectionId (null for none). Only to you. |
You only get events for what the account can see. A script can also send{ "t": "typing", "channelId": "…" } to show it's typing.
Incoming webhooks
An incoming webhook is a secret URL that posts to one channel. It accepts Slack's incoming-webhook format, so an existing Slack integration only needs the new URL. Admins create one in Workspace settings → Integrations & apps → Integrations & apps → Webhooks, or through the API, and get a URL like https://api.freesbe.net/hooks/<token>. The URL is shown once; rotating it gives a new one and turns the old one off.
curl -X POST https://api.freesbe.net/hooks/<token> \
-H 'Content-Type: application/json' \
-d '{"text": "Deploy finished :rocket:", "username": "CI", "icon_emoji": ":robot_face:"}'What it reads
| Field | What Freesbe does with it |
|---|---|
text | The message, in mrkdwn. Up to 40,000 characters. |
blocks | Block Kit isn't drawn, but when text is empty the text of section and context blocks is used. |
attachments | Legacy attachments, up to 20: color, pretext, author_name, author_link, title, title_link, text, fields, image_url, thumb_url, footer, ts, fallback. |
username | The sender name shown on the message. Defaults to the webhook's name. |
icon_emoji | The sender icon, as :shortcode:. |
icon_url | The sender icon as an image, when there's no icon_emoji. |
Send JSON, or a form (URL-encoded or multipart) with the JSON in a payloadfield, up to 1 MB. Like Slack, a webhook always posts to its own channel, so achannel field is ignored, and there's no response_url.
{
"text": "Build *#4812* failed on `main`",
"username": "CI",
"icon_url": "https://ci.northwind.example/logo.png",
"attachments": [
{
"color": "danger",
"title": "test / unit",
"title_link": "https://ci.northwind.example/builds/4812",
"text": "3 tests failed",
"fields": [
{ "title": "Branch", "value": "main", "short": true },
{ "title": "Author", "value": "dana", "short": true }
],
"footer": "CI",
"ts": 1760352000
}
]
}Answers
Answers are plain text, as on Slack, so tools that check for ok keep working.
| Status | Body | Meaning |
|---|---|---|
| 200 | ok | The message was posted. |
| 400 | invalid_payload | The body isn't a JSON object, or a form without a JSON payload. |
| 400 | no_text | No text, block text or attachments to show. |
| 404 | no_service | The URL is wrong, rotated, deleted or turned off. |
| 404 | channel_not_found | The channel the webhook posts to is gone. |
| 410 | channel_is_archived | The channel is archived. |
| 413 | payload_too_large | The body is over 1 MB. |
| 429 | rate_limited | More than 60 posts a minute to this webhook. |
Creating one with the API
curl -X POST $API/api/workspaces/$WORKSPACE/webhooks \
-H "Authorization: Bearer $FREESBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"kind": "incoming", "name": "CI", "channelId": "'$CHANNEL'"}'
# → 201 { "webhook": { … }, "hookUrl": "https://api.freesbe.net/hooks/…" }Webhooks act for the admin who made them. They turn off when that person is deactivated or leaves the private channel the webhook is bound to, and another admin can turn them back on.
Outgoing webhooks
An outgoing webhook POSTs JSON to your HTTPS endpoint when something happens. It covers every public channel, or only the one channel you bind it to with channelId. DMs are never sent; use the WebSocket for those.
curl -X POST $API/api/workspaces/$WORKSPACE/webhooks \
-H "Authorization: Bearer $FREESBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"kind": "outgoing", "name": "Support bot",
"url": "https://bot.northwind.example/freesbe",
"events": ["message.created", "reaction.added"]}'
# → 201 { "webhook": { … }, "secret": "…" }Events
Events about channels come from public channels, or only from the bound channel. The workspace's own events (people, meetings, bookings, recordings in meetings, and the punch clock) go to webhooks that aren't bound to a channel. Apps get the same events, in the envelope described under App events; the last column is the scope an app needs.
| 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, 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 |
The request
POST /freesbe HTTP/1.1
Host: bot.northwind.example
Content-Type: application/json
User-Agent: Freesbe-Webhooks/1.0
Freesbe-Event: message.created
Freesbe-Delivery: 01JA4…
Freesbe-Signature: t=1760352000,v1=5f2b…
{
"id": "01JA4…",
"type": "message.created",
"workspaceId": "01J…",
"createdAt": 1760352000123,
"data": {
"message": { "id": "01J…", "channelId": "01J…", "userId": "01J…", "body": "Can someone look at ticket 812?", … },
"channel": { "id": "01J…", "kind": "public", "name": "support", … }
}
}Freesbe-Delivery stays the same across retries, so use it to skip duplicates. Freesbe-Signature holds a Unix timestamp t andv1, the hex HMAC-SHA256 of <t>.<raw body> under the webhook's secret. The secret is shown once, when you create or rotate the webhook.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the request body exactly as received, before JSON.parse.
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
Answer with any 2xx within 10 seconds. A timeout, a network error, a redirect, a 5xx, a 408 or a 429 is retried 8 more times, from 30 seconds up to 8 hours apart. Any other 4xx stops it. Each webhook keeps a log of its deliveries for 14 days, in the workspace settings and at GET /api/workspaces/{w}/webhooks/{id}/deliveries.
Endpoint reference
Every endpoint, grouped the way the app is. Each one says who may call it (seeWho can call what); open Example for a request and an answer. The full request and response types live inpackages/shared/src/api.ts.
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/metaNo tokenThe server's name and version, the sign-up policy (
first,inviteoropen), the single sign-on providers it has, and whether it can send email.Example
Answer
{ "name": "Freesbe", "version": "…", "signup": "invite", "sso": ["google", "microsoft"], "email": true } - GET
/api/healthNo tokenAnswers
{ "ok": true }while the API is up. - POST
/api/auth/signupNo tokenCreate an account. Needs an
inviteTokenunless sign-up is open; the invite also joins its workspace.acceptTerms: truerecords 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.Example
Body
{ "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/loginNo tokenSign in with an email and password. Send
wantToken: trueto get a token in the answer. 10 tries a minute per address and per email.Example
Body
{ "email": "deploy-bot@northwind.example", "password": "…", "wantToken": true }Answer
{ "user": { … }, "workspaces": [ … ], "canCreateWorkspaces": false, "importedHistory": false, "authMethod": "password", "mfaVerifiedAt": null, "emailVerified": true, "token": "kX3q…" } - POST
/api/auth/logoutSigned inEnd the session the token belongs to. Answers 204.
- GET
/api/meSigned inThe signed-in person, their workspaces, how this session signed in and whether it has verified a second step.
Example
Answer
{ "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/meSigned inChange your
name(up to 80 characters) ortimezone(an IANA zone), set yourworkingHours(seven days, Sunday first, each a list of[start, end]minutes after midnight in your time zone;nullgoes back to Monday to Friday 9 to 5), or turn off the email about guests waiting in your meetings' lobbies (lobbyEmail). Answers likeGET /api/me.Example
Body
{ "name": "Dana Whitfield", "timezone": "America/New_York" } - PUT
/api/me/passwordSigned inChange your password, or set one on an account made with Google or Microsoft (leave out
currentPassword). Signs out every other session. Answers 204.Example
Body
{ "currentPassword": "…", "newPassword": "…" } - PUT
/api/me/avatarSigned inSet your picture: the raw PNG, JPEG, GIF or WebP bytes with their
Content-Type, up to 2 MB. Answers likeGET /api/me.Example
Request
curl -X PUT $API/api/me/avatar \ -H "Authorization: Bearer $FREESBE_TOKEN" \ -H 'Content-Type: image/png' --data-binary @dana.png - DELETE
/api/me/avatarSigned inRemove your picture, so your initials show. Answers like
GET /api/me. - GET
/api/me/avatar/{provider}Signed inThe photo Google or Microsoft (
googleormicrosoft) 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 withPUT /api/me/avatar. - POST
/api/me/photo-handoffsSigned inA one-time link for taking your picture with your phone, shown as a QR code.
urlopens 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 withPUT /api/me/avatar.Example
Answer
{ "code": "kq3V…", "url": "https://app.freesbe.net/snap#kq3V…", "expiresAt": 1761462600000 } - GET
/api/me/photo-handoffs/{code}Signed inWhether the phone sent the photo:
waiting,ready, orgone(expired, cancelled or already picked up).Example
Answer
{ "state": "ready" } - POST
/api/me/photo-handoffs/{code}/photoSigned inThe phone's photo as raw image bytes, once. The link is used up.
- DELETE
/api/me/photo-handoffs/{code}Signed inCancel the link, and drop a photo the phone already sent. Answers 204.
- GET
/api/photo-handoffs/{code}No tokenFor the phone's page: the first name of the person whose picture it takes. 404 once the link expired or was used.
Example
Answer
{ "name": "Dana", "expiresAt": 1761462600000 } - PUT
/api/photo-handoffs/{code}No tokenFor 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}Signed inSomeone's picture, if you share a workspace with them.
avatarUrlon people points here. - GET
/api/me/sign-inSigned inHow 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.
Example
Answer
{ "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}Signed inUnlink your
googleormicrosoftaccount. Answers 204. - POST
/api/me/claimsSigned inWhen
importedHistoryis 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-codeNo tokenEmail 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.
Example
Body
{ "email": "dana@northwind.example" } - POST
/api/auth/email-code/verifyNo tokenSign in with the emailed code. Answers like login.
Example
Body
{ "email": "dana@northwind.example", "code": "482913", "wantToken": true } - POST
/api/auth/verify-emailNo tokenVerify an address from the link in a verification email (its
token). Answers{ "email": "…" }. - POST
/api/me/email/verificationSigned inEmail yourself a new verification code and link. Answers 204.
- POST
/api/me/email/verifySigned inVerify your address with the emailed
code. Answers likeGET /api/me.Example
Body
{ "code": "482913" } - PUT
/api/me/emailSigned inStart changing your address: emails a code to the new one. Needs
currentPasswordwhen the account has a password. Answers 204.Example
Body
{ "email": "dana.whitfield@northwind.example", "currentPassword": "…" } - POST
/api/me/email/confirmSigned inFinish the change with the
codesent to the new address. Answers likeGET /api/me.Example
Body
{ "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/optionsApp onlyWebAuthn options for
navigator.credentials.get(). - POST
/api/auth/passkey/loginApp onlySign in with the passkey's assertion (
response). Answers like login. - GET
/api/me/passkeysSigned inYour passkeys:
id,name,backedUp,createdAt,lastUsedAt. - POST
/api/me/passkeys/optionsApp onlyWebAuthn options for
navigator.credentials.create(). Refused with a token: add passkeys from the app. - POST
/api/me/passkeysApp onlySave a new passkey from the browser's
response, with an optionalname. - PATCH
/api/me/passkeys/{id}Signed inRename a passkey.
Example
Body
{ "name": "Work laptop" } - DELETE
/api/me/passkeys/{id}Signed inRemove a passkey. Answers 204.
- GET
/api/auth/oauth/{provider}/startApp onlyOpen in the browser to sign in with
googleormicrosoft.?next=/pathreturns there,?invite=<token>signs up through an invite and?link=1links the account instead. - GET
/api/auth/oauth/{provider}/callbackApp onlyWhere Google and Microsoft send people back.
- POST
/api/auth/handoffApp onlyThe desktop app's sign-in through the system browser: a one-time code for a PKCE
challenge. - POST
/api/auth/handoff/redeemApp onlyThe desktop app trades the
codeand itsverifierfor 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/sessionsSigned inEverywhere you're signed in: device, app, sign-in method, address and rough location, when it started and was last used.
currentmarks this one.Example
Answer
{ "sessions": [ { "id": "…", "current": true, "method": "password", "createdAt": …, "lastActiveAt": …, "ip": "203.0.113.24", "location": "London, United Kingdom", … } ] } - DELETE
/api/me/sessionsSigned inSign out every other session, keeping this one. Answers 204.
- DELETE
/api/me/sessions/{id}Signed inSign out one session. Answers 204.
- POST
/api/me/termsSigned inAgree to the current Terms of Service and Privacy Policy. Send the
versionyou were shown; an older one answers 409. Signed-in browser sessions only.GET /api/mehastermsVersion, the version you last agreed to, or null. Answers likeGET /api/me.Example
Body
{ "version": "2026-10-28" } - GET
/api/me/security-activitySigned inYour 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), with what happened (detail: the sign-inmethod, a passkey'sname, theprovider), 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). Whenmoreis true, pass the last event'sidas?before=for the next page. Events are kept for a year.Example
Answer
{ "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/deletionSigned inWhether your account is being deleted (
pending, with the purge date), what stops you deleting it (blockers: workspaces you own,ownerwith other people in them oralone), and the workspaces you'd leave.Example
Answer
{ "pending": null, "blockers": [], "workspaces": [{ "id": "01JB…", "name": "Northwind Traders" }] } - POST
/api/me/deletionSigned inDeletes 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 403account_deleting.Example
Body
{ "reason": "Moving to another team" }Answer
{ "purgeAt": 1795322400000 } - DELETE
/api/me/deletionSigned inKeeps your account, if it's still waiting to be deleted. Workspaces' admins reactivate you. Answers 204.
- GET
/api/me/mfaSigned inWhen 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.setupmeans none yet: setting up a passkey or authenticator app counts.Example
Answer
{ "verifiedAt": null, "phone": false, "passkey": false, "totp": true, "email": true, "password": false, "setup": false } - POST
/api/me/mfa/totpSigned inVerify with a
codefrom your authenticator app. Each code works once. Answers likeGET /api/me.Example
Body
{ "code": "123456" } - POST
/api/me/mfa/emailSigned inEmail a code to verify with. Not after signing in with an emailed code. Answers 204.
- POST
/api/me/mfa/email/verifySigned inVerify with the emailed
code. Answers likeGET /api/me. - POST
/api/me/mfa/passwordSigned inVerify with your
password, only after signing in with an emailed code. Answers likeGET /api/me. - POST
/api/me/mfa/passkey/optionsApp onlyWebAuthn options to verify with one of your passkeys.
- POST
/api/me/mfa/passkeyApp onlyVerify with the passkey's assertion.
- POST
/api/me/mfa/phoneSigned inAsk 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.Example
Answer
{ "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "number": 47, "expiresAt": 1792771320000, "phones": 1 } - POST
/api/me/mfa/phone/{id}Signed inWhether the phone answered:
pending,denied,expired, orapprovedwithme(likeGET /api/me), which verifies this session once.DELETEstops waiting.Example
Answer
{ "status": "pending" } - POST
/api/auth/phoneNo tokenSign in with no password: ask that person's phones to approve. Show the
numberand keep thesecretfor the checks. Answers the same whether or not the address has an account or a phone, so it doesn't reveal who does.DELETEwith{ id, secret }stops waiting.Example
Body
{ "email": "dana@northwind.example" }Answer
{ "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "number": 63, "expiresAt": 1792771320000 } - POST
/api/auth/phone/checkNo tokenWhether the phone answered:
pendingordenied, orapprovedwithauth(like login), which signs in once. Counts as the second step. Stop asking atexpiresAt.Example
Body
{ "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "wantToken": true }Answer
{ "status": "pending" } - GET
/api/me/approver-devicesSigned inYour 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.Example
Answer
{ "devices": [{ "id": "01JC8Z…", "name": "iPhone", "platform": "ios", "createdAt": 1792540320000, "lastUsedAt": 1792760520000, "current": false }] } - POST
/api/me/mfa/launchApp onlyThe app calls this each time it opens. For workspaces that ask each time Freesbe opens, it forgets this session's verification.
- POST
/api/me/totpSigned inStart setting up an authenticator app: a new Base32
secretand anotpauth://urifor a QR code.Example
Answer
{ "secret": "JBSWY3DPEHPK3PXP…", "uri": "otpauth://totp/Freesbe:deploy-bot%40northwind.example?secret=…" } - PUT
/api/me/totpSigned inTurn the authenticator app on with a first
codefrom it. This counts as verifying. Answers likeGET /api/me.Example
Body
{ "code": "123456" } - DELETE
/api/me/totpSigned inRemove the authenticator app. Answers 204.
Workspaces and settings
- GET
/api/workspacesSigned inThe workspaces you belong to, with your
rolein each. - POST
/api/workspacesSigned inCreate a workspace and become its owner, when
canCreateWorkspacesis true.regionis where its data is stored:euorus(guaranteed to stay in the region), orapacoroc(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'sregionis null for workspaces made before regions. Answers 201.Example
Body
{ "name": "Northwind Traders", "region": "eu" }Answer
{ "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "requireMfa": true, "mfaReverifyHours": 168, "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } - GET
/api/workspaces/{w}Membersteam:readOne workspace.
Example
Answer
{ "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "requireMfa": true, "mfaReverifyHours": 168, "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } - PATCH
/api/workspaces/{w}AdminsChange the
name, require single sign-on (requireSso:google,microsoft,anyor null), require multi-factor authentication (requireMfa, andmfaReverifyHours: null, 0, 12, 24 or 168), allow huddle and meetingrecordings, give themtranscriptsand AI notes, transcribe every huddle (autoTranscribe), share meetings' notes outside the workspace by a private link (guestTranscripts, withguestTranscriptEmailsto email meeting guests theirs automatically,guestTranscriptDays: the longest a link works, 1, 3, 7, 14 or 30, andguestTranscriptVideoto let links play the recording; turning these on asks people with an approving phone again), and delete recordings afterrecordingRetentionDays(30, 90, 365 or null), choose who can start and stop recordings (recordingControl:everyone,starter, the default, orhosts), and turn AI summaries and translations on or off (ai).lobbyRemindMin(0, 1, 2, 3, 5 or 10) andlobbyEmailMin(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.lobbyMusicpicks what guests hear while they wait in a meeting's lobby:lofi(the default),drift,sunny,classic,bossaoroff.workflowBuildersismembersoradmins: 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,membersCanDeletelets members and guests delete their own messages, andadminsCanDeletelets owners and admins delete anyone's message in a channel.readReceiptsis where people see who read their messages:off(the default),small(DMs and channels of up to 20 people) orall(bigger channels show a count, not names);readReceiptsOptOutlets each person turn their own off.fileRulessets which filesmembersandguestscan share: each has apreset(any,safeoronly),kindsforonly, andallowandblockextension lists (lists andonlyneed 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.Example
Body
{ "requireMfa": true, "mfaReverifyHours": 168, "recordingRetentionDays": 90 }Answer
{ "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "requireMfa": true, "mfaReverifyHours": 168, "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } - PUT
/api/workspaces/{w}/read-receiptsMembersTurn 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 turnedreadReceiptsOptOutoff. Answers with the workspace, whoseshowReadReceiptsis your choice.Example
Body
{ "show": false }Answer
{ "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "slug": "northwind", "role": "admin", "requireSso": null, "requireMfa": true, "mfaReverifyHours": 168, "logoUrl": "/api/workspace-logos/01JB8Q…/01JBA…", "recordings": true, "recordingRetentionDays": 90, "recordingControl": "starter", "lobbyRemindMin": 2, "lobbyEmailMin": 5, "editWindowMin": null, "membersCanDelete": true, "adminsCanDelete": true, "plan": "business", "lobbyMusic": "lofi", "readReceipts": "small", "readReceiptsOptOut": true, "showReadReceipts": true } } - GET
/api/workspaces/{w}/message-deletionsAdminsThe last 100 messages owners and admins deleted for someone else, newest first: the channel, whose message it was (
authorId, orauthorNamefor 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.Example
Answer
{ "deletions": [ { "id": "01JB8ZK2W1E9R4M3T6Y7U8I9OP", "channelId": "01JB8ZJ4Q6S0D2F4G6H8J0K2LM", "channelName": "general", "messageId": "01JB8ZK0A2B4C6D8E0F2G4H6JK", "authorId": "01JB8ZH9N1P3Q5R7S9T1V3W5XY", "authorName": "Tom Becker", "deletedBy": "01JB8ZH7L9M1N3P5Q7R9S1T3UV", "deletedAt": 1792729800000, "inThread": false, "fileCount": 0 } ] } - GET
/api/workspaces/{w}/remote-control/sessionsAdminsThe remote control log: the last 100 requests to control someone's shared screen, newest first.
kindishuddleormeeting(a support session, wheresharerNameis the name the customer gave).startedAtis null when the request was declined or never answered, andendReasonisdeclined,expired,stopped,left,share_stoppedorturned_off.Example
Answer
{ "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}/exportsAdminsThe workspace's last 20 data exports, newest first: what each covers (
scopepublicorall,includeFiles,from,to), itsstatus(queued,running,ready,failedorexpired), progress, and its zips (parts) once ready.canExportAllsays whether you can include private channels and DMs, andallNeedswhy not (ownerorplan).Example
Answer
{ "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}/exportsAdminsStart an export in Slack's export format.
scopeall(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).fromandtolimit it to messages in that time.Example
Body
{ "scope": "public", "includeFiles": true, "from": null }Answer
{ "export": { "id": "01JC5E…", "status": "queued", … } } - POST
/api/workspaces/{w}/exports/{id}/parts/{n}/linkAdminsOnly for the admin who started the export: a download link for zip
nof a ready export, working for 15 minutes without signing in. Needs a second factor verified in the last 15 minutes:mfa_requiredotherwise. Each link is recorded in the audit log.Example
Answer
{ "url": "https://app.freesbe.net/api/export-downloads/kQ3…", "expiresAt": 1792730700000 } - GET
/api/workspaces/{w}/auditAdminsThe 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. Whenmoreis true, pass the last entry'sidas?before=for the next page. Entries are kept for a year.Example
Answer
{ "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-activityAdminsMembers' 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.Example
Answer
{ "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}/logoAdminsSet the logo: raw PNG, JPEG, GIF or WebP bytes with their
Content-Type, up to 2 MB. - DELETE
/api/workspaces/{w}/logoAdminsRemove the logo, so the workspace's initials show.
- GET
/api/workspace-logos/{w}/{version}No tokenThe logo image.
logoUrlpoints here; the version can't be guessed, so only people who saw the URL can fetch it. - GET
/api/workspaces/{w}/bootMembersEverything 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
seqfor the WebSocket, and which features are on. - GET
/api/workspaces/{w}/cache-keyMembersThe key the app encrypts its local cache of the workspace with. It changes when you lose access.
- GET
/api/workspaces/{w}/allowanceAdminsThis month's video and storage use against the workspace's limits, and why cameras and screen sharing are paused, if they are (
paused,cap,allowanceorbudget, the workspace's spending limit).Example
Answer
{ "month": "2026-10", "videoGb": 41.2, "videoGbLimit": 200, "storageGb": 3.8, "storageGbLimit": 50, "videoPaused": null } - GET
/api/workspaces/{w}/billingAdminsThe workspace's plan and subscription, add-ons (
addOns) and this month's usage past the plan's allowances (usage, charged at the start of next month):status(none,trialing,active,past_due,canceledorcomped), yearly or monthlyinterval,seats(active members, not guests) andseatsBilled, what the workspace gets (limitsandfeatures, 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.enforcedsays whether the plan's limits apply yet.Example
Answer
{ "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 }], "usageMonth": "2026-10" } - PUT
/api/workspaces/{w}/billing/add-ons/{addOn}AdminsAdds 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
quantity0. 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.Example
Body
{ "quantity": 3 }Answer
{ "plan": "business", "addOns": [...], ... } - POST
/api/workspaces/{w}/billing/checkoutAdminsWhere to send an admin to pay for
plan(a plan in the catalog that's bought by card, likebusiness) yearly or monthly (interval:yearormonth): Stripe Checkout, or, when the workspace already pays, Stripe's page to confirm the switch.returnPathis the app page to come back to. 503 until payments are set up on the server.Example
Body
{ "plan": "business", "interval": "year", "returnPath": "/northwind/c/general" }Answer
{ "url": "https://checkout.stripe.com/c/pay/cs_…" } - POST
/api/workspaces/{w}/billing/portalAdminsA link to Stripe's billing portal for the workspace: card, billing details, invoices and cancelling. 409 when the workspace has never paid.
Example
Body
{ "returnPath": "/northwind/c/general" }Answer
{ "url": "https://billing.stripe.com/p/session/…" } - GET
/api/catalogNo tokenThe 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.Example
Answer
{ "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/service-statusNo tokenWhat [freesbe.net/status](/status/) shows: the
overallstatus and each component's (operational,degraded,outageormaintenance), 90 days of dailyuptime(null for components that aren't checked automatically) and the worst incidentimpactper day, plus open, scheduled and recently resolved incidents with their updates. Cached for a minute.Example
Answer
{ "overall": "operational", "checkedAt": 1761238800000, "components": [{ "id": "app", "name": "App and messages", "status": "operational", "uptime": 0.9999, "days": [{ "day": "2026-10-27", "uptime": 1, "impact": null }, ...] }, ...], "incidents": [] }
People and roles
- GET
/api/workspaces/{w}/membersMembersusers:readEveryone in the workspace, deactivated people included. Guests get the people they share channels with.
Example
Answer
{ "members": [ { "userId": "01JB8R…", "name": "Priya Raman", "role": "member", "title": "Support lead", … } ] } - PATCH
/api/workspaces/{w}/members/{userId}MembersAnyone can set their own
title. Admins change anyone's title, make someone anadminormember(a guest made a member joins the whole workspace), and setdeactivated. The owner's role can't change this way (see/owner), and nobody can deactivate themselves.Example
Body
{ "role": "admin" }Answer
{ "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 } } - POST
/api/workspaces/{w}/ownerIts ownerHands 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.Example
Body
{ "userId": "01JB8S…" } - GET
/api/workspaces/{w}/closureAdminsWhether the workspace is being closed (
closure:stageclosinguntilclosesAt, thenclosedand read-only untilpurgeAt), when the paid period ends, and the add-ons and phone system closing would end.Example
Answer
{ "closure": null, "periodEnd": 1795322400000, "addOns": ["Phone system"], "phoneSystem": true } - POST
/api/workspaces/{w}/closureIts ownerCloses 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.
namemust 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 403workspace_closed; reading, exports, the billing portal and marking read still work. A workspace Freesbe's team holds answers the same way with 403workspace_on_hold(waiting for documents) orworkspace_suspended; under review only invites, phone numbers and customer inbox changes answer 403workspace_under_review(see the account status guide).Example
Body
{ "name": "Northwind Traders", "reason": "Moving to another tool" }Answer
{ "closure": { "stage": "closing", "requestedAt": 1794112800000, "closesAt": 1795322400000, "closedAt": null, "purgeAt": null, "by": "01JB8R…" }, "periodEnd": 1795322400000, "addOns": [], "phoneSystem": false } - DELETE
/api/workspaces/{w}/closureIts ownerReopens 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}/profilesMembersusers:readProfile 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.
Example
Answer
{ "fields": [ { "id": "01JC2…", "preset": "department", "label": null, "kind": "text", "options": [], "locked": true, "hiddenFromGuests": true } ], "profiles": { "01JB8R…": { "01JC2…": "Engineering" } } } - GET
/api/workspaces/{w}/profile-fieldsMembersusers:readThe workspace's profile fields, in the order profiles show them.
presetispronouns,department,location,manager,phoneor null for a field admins added;kindistext,link,date,person,optionsorphone. - PUT
/api/workspaces/{w}/profile-fieldsAdminsSaves every field, in order: keep a field by its
id, add one without anid, and leave one out to delete it with its values. Setlabel,options(foroptionsfields),locked(only admins fill it in) andhiddenFromGuests. Adding your own fields and locking fields need Business, Corporate or Enterprise.Example
Body
{ "fields": [ { "id": "01JC2…", "kind": "text", "locked": true }, { "kind": "options", "label": "Region", "options": ["EMEA", "Americas"] } ] } - PUT
/api/workspaces/{w}/members/{userId}/profileMembersSets 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.
Example
Body
{ "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } }Answer
{ "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } } - POST
/api/workspaces/{w}/profiles/bulkAdminsSets values for up to 500 people at once, as
people: [{ userId, values }]. Nothing is saved if any value doesn't fit its field.Example
Body
{ "people": [ { "userId": "01JB8R…", "values": { "01JC2…": "Engineering" } } ] }Answer
{ "updated": 1 }
Invites
- GET
/api/workspaces/{w}/invitesAdminsOpen invite links and emailed invites, guest invites included.
- POST
/api/workspaces/{w}/invitesAdminsCreate an invite link to share:
role(memberoradmin),expiresInDays(default 7, at most 30),maxUses(null for unlimited). Thetokenandurlare only in this answer.Example
Body
{ "role": "member", "expiresInDays": 7, "maxUses": 10 }Answer
{ "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/emailAdminsEmail 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.Example
Body
{ "emails": ["sam@acme.example", "priya@northwind.example"], "role": "member" }Answer
{ "sent": ["sam@acme.example"], "skipped": ["priya@northwind.example"], "failed": [], "limited": [] } - DELETE
/api/workspaces/{w}/invites/{id}AdminsRevoke an invite. Answers 204.
- GET
/api/invites/{token}No tokenWhat 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.
Example
Answer
{ "workspace": { "id": "01JB8Q…", "name": "Northwind Traders", "logoUrl": null }, "invitedBy": "Dana Whitfield", "valid": true, "guest": false, "channels": [], "needsApproval": false } - POST
/api/invites/{token}/acceptSigned inJoin 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.
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-invitesChannel membersAn invite link that makes people guests in this channel.
approval(default true) means someone in the channel lets each guest in. AlsoexpiresInDaysandmaxUses. Not for DMs, and not for guests.Example
Body
{ "approval": true, "expiresInDays": 14, "maxUses": null }Answer
{ "invite": { "role": "guest", "channelIds": ["01JB8S…"], "needsApproval": true, "url": "https://app.freesbe.net/invite/…", … } } - POST
/api/channels/{c}/guest-invites/emailChannel membersEmail up to 50 people their own guest invite to the channel, with
approval. Answers like the workspace email invites.Example
Body
{ "emails": ["jordan@acme.example"], "approval": false } - GET
/api/invites/{token}/requestSigned inWhere your request to join through a guest invite stands:
none,pending,approved(with theworkspace) ordeclined. - POST
/api/guest-requests/{id}/approveChannel membersLet a waiting guest in. The id is the
requestIdof theguest_requestsystem message posted in the channel. Answers 204, or 409 when someone already answered. - POST
/api/guest-requests/{id}/declineChannel membersTurn 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}/channelsMemberschannels:readEvery public channel, plus your private channels and DMs, each with your unread state. Guests get only their own.
Example
Answer
{ "channels": [ { "id": "01JB8S…", "kind": "public", "name": "deploys", "unreadCount": 3, … } ] } - POST
/api/workspaces/{w}/channelsMembers, not guestschannels:manageCreate a channel:
name,kind(publicorprivate), an optionaltopicand thememberIdsto add besides you. Answers 201, or 409 when the name is taken.Example
Body
{ "name": "acme-rollout", "kind": "private", "topic": "Acme Corp onboarding", "memberIds": ["01JB8R…"] }Answer
{ "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}Channel memberschannels:readOne channel, with your unread state and notification settings.
Example
Answer
{ "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}Channel memberschannels:manageChange the
topic(channel members, up to 250 characters) or thename(its creator or admins). Posts a note in the channel.Example
Body
{ "topic": "Release notes, rollbacks and on-call" } - POST
/api/channels/{c}/joinChannel memberschannels:joinJoin a public channel.
- POST
/api/channels/{c}/leaveChannel memberschannels:joinLeave a channel. Not DMs, and not the general channel. Answers 204.
- POST
/api/channels/{c}/archiveCreator or adminschannels:manageArchive a channel: it becomes read-only. Not the general channel.
- POST
/api/channels/{c}/unarchiveCreator or adminschannels:manageReopen an archived channel.
- GET
/api/channels/{c}/membersChannel memberschannels:readThe user ids of everyone in the channel.
Example
Answer
{ "userIds": ["01JB8Q…", "01JB8R…"] } - POST
/api/channels/{c}/membersChannel memberschannels:manageAdd people (
userIds, up to 500) to a channel you're in. Not DMs; guests can't add people.Example
Body
{ "userIds": ["01JB8T…"] } - DELETE
/api/channels/{c}/members/{userId}Creator or adminschannels:manageRemove someone from the channel. Answers 204.
- PUT
/api/channels/{c}/prefsChannel membersYour own settings for the channel:
muted,notify(default,all,mentionsornone) and the notificationsound(null for your default).Example
Body
{ "muted": false, "notify": "mentions", "sound": "marimba" } - PUT
/api/channels/{c}/sectionChannel membersPut the conversation in one of your sidebar sections, or back under Channels or Direct messages with null. It comes back as
sectionIdon the channel. Answers 204. Apps can't use this.Example
Body
{ "sectionId": "01JC2…" } - POST
/api/channels/{c}/readChannel membersMark 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.Example
Body
{ "lastReadId": "01JBC4…" } - POST
/api/channels/{c}/unreadChannel membersMark the channel unread from
messageIdon (a message in the channel, not a thread reply): the read cursor moves back to just before it. Answers with the channel and its newunreadCountandmentionCount, and your other devices get aread.updatedevent withcounts. Read receipts don't move back. Apps can't use this.Example
Body
{ "messageId": "01JBC4…" }Answer
{ "channel": { "id": "01JBC2…", "lastReadId": "01JBC3…", "unread": true, "unreadCount": 3, "mentionCount": 1, … } } - GET
/api/channels/{c}/receiptsChannel membersRead receipts: how far the others in the conversation have read.
modeisnames,count(a big channel: show how many, not who) oroff(the workspace doesn't show receipts here, you turned yours off, or you aren't a member), with an emptyreaderslist. A message was seen by everyone whoselastReadIdis the same or newer. People who turned their receipts off aren't listed. Changes arrive asreceipt.updatedevents.Example
Answer
{ "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}/dmsMembersim:writeFind 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.Example
Body
{ "userIds": ["01JB8R…", "01JB8T…"] }Answer
{ "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 <!channel>. Archived channels are read-only.
- GET
/api/channels/{c}/messagesChannel memberschannels:historyA page of top-level messages, oldest first: the newest 50 by default. Page with
?before=<id>,?after=<id>or?from=<id>, jump with?around=<id>, and set?limit=from 1 to 100.hasMoresays there are more in that direction.Example
Answer
{ "messages": [ { "id": "01JBC4…", "body": "Release *2.4* is out :tada:", … } ], "hasMore": true } - POST
/api/channels/{c}/messagesChannel memberschat:writePost a message, or a thread reply with
threadRootId. Attach up to 10 uploadedfileIds. Sending the sameclientMsgIdagain returns the first message instead of posting twice. In a customer inbox conversation's thread the reply goes to the customer, unlessnoteis true. You have to be in the channel. Apps can also send a card as Slackblocksorattachments, withbodyas its notification text. Answers 201.Example
Body
{ "body": "Rollback done, <@01JB8R…> can you confirm?", "threadRootId": "01JBC4…", "clientMsgId": "deploy-8812", "fileIds": ["01JBC9…"] }Answer
{ "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}Channel memberschannels:historyOne message, with its reactions, files and thread counts.
Example
Answer
{ "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}Channel memberschat:writeEdit your own message's
body, within the workspace'seditWindowMin. SetseditedAt.Example
Body
{ "body": "Release *2.4.1* is out :tada:" } - DELETE
/api/messages/{m}Channel memberschat:writeDelete 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 whenadminsCanDeleteis off). A thread's first message stays as a placeholder while it has replies, and a message an admin deleted stays as a placeholder withdeletedByAdmin: true; admin deletions are logged. Answers 204. - GET
/api/messages/{m}/repliesChannel memberschannels:historyA thread: its first message and up to 1,000 replies, oldest first.
Example
Answer
{ "root": { "id": "01JBC4…", "replyCount": 2, … }, "replies": [ { … }, { … } ] } - POST
/api/messages/{m}/forwardChannel memberschat:writeForward a message you can read to a conversation you're in (
channelId), with your owncommentabove it. The copy carries the original as an attachment withforwarded(messageId,channelId,threadRootId,userId,channelName,createdAt,fileNames); files stay in the original conversation. Forwarding a forward with no comment passes the original on.Example
Body
{ "channelId": "01JB8T…", "comment": "Can we do this for Acme Corp too?" }Answer
{ "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-anywayChannel membersNotify 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}Channel membersreactions:writeReact with one emoji: the character itself, URL-encoded (
%F0%9F%8E%89for 🎉). Doing it twice is harmless. Answers 204, or 409 when the message has all the different reactions it can take.Example
Request
curl -X PUT "$API/api/messages/$MESSAGE/reactions/%F0%9F%8E%89" \ -H "Authorization: Bearer $FREESBE_TOKEN" - DELETE
/api/messages/{m}/reactions/{emoji}Channel membersreactions:writeRemove 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}/keywordsMembersYour keywords.
Example
Answer
{ "keywords": ["Northwind", "price list"] } - PUT
/api/workspaces/{w}/keywordsMembersReplace them. Blanks and repeats are dropped. Answers the saved list.
Example
Body
{ "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}Channel membersRemove 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}/imageMembersA preview's picture, fetched by Freesbe.
thumbUrlandimageUrlpoint 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}/emojiMembersteam:readEvery custom emoji, by name. The
urlloads the picture without signing in.Example
Answer
{ "emoji": [ { "name": "northwind", "url": "/api/custom-emoji/01JB…/01JC…", "createdBy": "01JB8R…", "createdAt": 1767225600000 } ] } - POST
/api/workspaces/{w}/emoji?name={name}MembersAdd 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.Example
Request
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}MembersRemove 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}No tokenThe picture. Its id can't be guessed, and the picture behind it never changes.
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}/soundsMembersThe sounds you picked for messages from particular people, by user id.
Example
Answer
{ "sounds": { "01JB8R…": "bell" } } - PUT
/api/workspaces/{w}/sounds/{userId}MembersPick the sound for messages from one person, or null for your default. Answers the whole list.
Example
Body
{ "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=Membersfiles:writeUpload the raw bytes with their
Content-Typeand aContent-Length, up to 50 MB. Add&width=&height=for images when you know them. Answers 201, 403file_blockedwhen 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.Example
Request
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.pdfAnswer
{ "file": { "id": "01JBC9…", "name": "q3-report.pdf", "mime": "application/pdf", "size": 482113, "url": "/api/files/01JBC9…" } } - GET
/api/files/{id}Channel membersfiles:readDownload a file from a channel you can see. Images, audio, video and plain text open inline; add
?download=1to always download. SupportsRange. - GET
/api/channels/{c}/filesChannel membersfiles:readThe files shared in a channel or DM you can see, threads included, newest first: what its Files tab shows. Filter with
kind(images,mediafor video and audio,documents,other) andq(part of the name). Up tolimit(default 50, at most 100) at a time; passnextasbeforefor older ones. Files of deleted messages aren't listed. Apps needfiles:read.Example
Answer
{ "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 }
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}/statusMembersusers:writeSet 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) andmanualAway.Example
Body
{ "text": "At the Acme Corp warehouse", "emoji": "🚚", "expiresAt": 1761238800000, "dndUntil": null }Answer
{ "status": { "userId": "01JB8Q…", "text": "At the Acme Corp warehouse", "emoji": "🚚", "expiresAt": 1761238800000, "dndUntil": null, "manualAway": false } } - GET
/api/workspaces/{w}/search?q=Memberssearch:readSearch the messages you can see for 2 to 200 characters, newest first, optionally in one channel with
&channelId=.qcan hold filters:from:@name(orfrom:me),in:#channel,after:yyyy-mm-dd,before:yyyy-mm-dd,on:yyyy-mm-ddandhas:file; after and before leave out the day itself. Answers the matchingresults, thechannelsthey're in, and thefiltersas understood, with any names that matched nobody inunknown.Example
Answer
{ "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}/askMembersAsk Freesbe: an answer to a
question(2 to 200 characters, filters allowed) written by AI from the messages you can see, optionally only inchannelId, inlanguagewhen the question doesn't make it clear. Each of thesentenceslists thesourcesit comes from, numbered from 1 in the order ofsources.statusisunknownwhen the messages found don't answer it.searchis 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.Example
Body
{ "question": "When is the Northwind launch?", "language": "en" }Answer
{ "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=MembersMentions of you (including
@channeland@here), huddles in your DMs that you missed (system messages of typehuddle_missed) and replies in threads you started or replied in, newest first, 50 at a time, with thechannelsthey're in. Page back withbefore, the oldest message id you have.Example
Answer
{ "items": [ { "id": "01JBC9…", "channelId": "01JB8S…", "body": "<@01JB8Q…> can you approve this?", … } ], "channels": [ { "id": "01JB8S…", "name": "support", … } ] } - GET
/api/workspaces/{w}/sent?before=MembersYour own messages and replies, newest first, 50 at a time, in conversations you're still in, with the
channelsthey're in. Page back withbefore, the oldest message id you have. The app's Drafts & sent page shows these under Sent.Example
Answer
{ "items": [ { "id": "01JBD2…", "channelId": "01JB8S…", "body": "Shipped the Acme Corp order", … } ], "channels": [ { "id": "01JB8S…", "name": "sales", … } ] } - GET
/api/workspaces/{w}/calls?before=MembersYour calls: huddles in your DMs that have ended, newest first, 50 at a time.
directionisoutwhen you called;answeredsays whether you joined (incoming) or someone else did (outgoing). Page back withbefore, the oldest call'sstartedAt. Activity → Calls in the app lists these with your phone system calls. Not for apps.Example
Answer
{ "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}/todosMemberstodos:readYour open to-dos, the soonest due first and undated ones after, then the 50 you finished most recently.
Example
Answer
{ "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}/todosMemberstodos:writeAdd a to-do:
text(the one-line title) up to 500 characters, optionalnotesup to 10,000 characters in message formatting (mrkdwn, line breaks kept), optionaldueAt, and optionalmessageIdto make it from a message you can see. With amessageId,textcan be empty and the message's text is used; a long or multi-line message also goes whole intonotes. Up to 500 open to-dos each.Example
Body
{ "text": "Approve spring catalog prices", "notes": "Check the *Acme Corp* discount first.", "dueAt": 1761325199999 }Answer
{ "todo": { "id": "01JBF3…", "text": "Approve spring catalog prices", "dueAt": 1761325199999, "doneAt": null, … } } - POST
/api/workspaces/{w}/todos/aiMembersAI 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).
actioniswrite(notes from the title),plan(numbered steps, plus adueDatewhen the to-do says when it's needed; passtodayas yyyy-mm-dd) orsummarize(what the message a to-do was made from and its thread say; needstodoId). Sendtitleand the currentnotes. Nothing is saved: put the answer in the to-do with PATCH.Example
Body
{ "action": "plan", "title": "Ask Tom about a second forklift", "notes": "Before the budget meeting Thursday", "today": "2026-10-23" }Answer
{ "notes": "1. Count the hours both shifts wait for the forklift\n2. …", "dueDate": "2026-10-29" } - PATCH
/api/workspaces/{w}/todos/{id}Its ownertodos:writeChange
text,notesordueAt(null clears either), or tick it off withdone: true(falsereopens it).Example
Body
{ "done": true }Answer
{ "todo": { "id": "01JBF3…", "doneAt": 1761242400000, … } } - DELETE
/api/workspaces/{w}/todos/{id}Its ownertodos:writeDelete 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-messagesMemberschat:writeYour scheduled messages, soonest first, including ones that failed.
Example
Answer
{ "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-messagesMemberschat:writeSchedule a message in a channel, DM or thread (
threadRootId) you can post in.bodyis written like a message's;fileIdsare uploads that aren't attached yet. Up to 100 each.Example
Body
{ "channelId": "01JB8S…", "body": "Weekly sync is at 10:00 today", "sendAt": 1761440400000 }Answer
{ "scheduled": { "id": "01JBG1…", "status": "scheduled", … } } - PATCH
/api/workspaces/{w}/scheduled-messages/{id}Its ownerchat:writeChange the
bodyor movesendAt. A failed one given a newsendAtis scheduled again.Example
Body
{ "sendAt": 1761526800000 }Answer
{ "scheduled": { "id": "01JBG1…", "sendAt": 1761526800000, … } } - POST
/api/workspaces/{w}/scheduled-messages/{id}/sendIts ownerchat:writeSend it now instead. Answers 201 with the posted message.
Example
Answer
{ "message": { "id": "01JBG4…", "body": "Weekly sync is at 10:00 today", … } } - DELETE
/api/workspaces/{w}/scheduled-messages/{id}Its ownerchat:writeDelete 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}/pinsChannel memberschannels:historyThe conversation's pinned messages, newest pin first.
Example
Answer
{ "pins": [ { "message": { "id": "01JBA2…", "body": "Q4 launch plan for Northwind", … }, "pinnedBy": "01JB8Q…", "pinnedAt": 1761238800000 } ] } - PUT
/api/messages/{m}/pinChannel memberspins:writePin a message in its conversation. Pinning one that's pinned does nothing. Answers 204.
- DELETE
/api/messages/{m}/pinChannel memberspins:writeUnpin it. Answers 204.
- GET
/api/channels/{c}/bookmarksChannel memberschannels:historyThe links kept at the top of the conversation, left to right.
Example
Answer
{ "bookmarks": [ { "id": "01JBB4…", "channelId": "01JB8S…", "title": "Price list", "url": "https://northwind.example/prices", "createdBy": "01JB8Q…", "createdAt": 1761238800000 } ] } - POST
/api/channels/{c}/bookmarksChannel membersbookmarks:writeAdd a link at the end.
urlmust be a web address (https://is added when it's missing); without atitlethe link's address names it.Example
Body
{ "title": "Price list", "url": "https://northwind.example/prices" }Answer
{ "bookmark": { "id": "01JBB4…", … } } - PATCH
/api/channels/{c}/bookmarks/{id}Channel membersbookmarks:writeRename it, change its link, or move it to
position(0 is first).Example
Body
{ "title": "Northwind price list", "position": 0 }Answer
{ "bookmark": { "id": "01JBB4…", … } } - DELETE
/api/channels/{c}/bookmarks/{id}Channel membersbookmarks:writeRemove 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=openMembersIn progress (
open): reminders that went off first, then upcoming ones soonest first, then the rest newest first.donelists the newest 100 completed.messageis null for one that was deleted or is in a conversation you left.Example
Answer
{ "items": [ { "messageId": "01JBA2…", "channelId": "01JB8S…", "message": { … }, "createdAt": 1761238800000, "remindAt": 1761296400000, "remindedAt": null, "doneAt": null } ], "channels": [ { "id": "01JB8S…", "name": "sales", … } ] } - PUT
/api/messages/{m}/saveChannel membersSave a message you can read, or change its reminder. Saving a completed one brings it back.
Example
Body
{ "remindAt": 1761296400000 }Answer
{ "item": { "messageId": "01JBA2…", "remindAt": 1761296400000, … } } - PATCH
/api/messages/{m}/saveIts ownerChange the reminder (
remindAt, null to take it off) or mark itdone. A new time sets the reminder off again.Example
Body
{ "done": true }Answer
{ "item": { "messageId": "01JBA2…", "doneAt": 1761300000000, … } } - DELETE
/api/messages/{m}/saveIts ownerTake 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}/draftsMembersYour drafts, newest first.
Example
Answer
{ "drafts": [ { "channelId": "01JB8S…", "threadRootId": null, "body": "Can you check the Northwind order before", "updatedAt": 1761238800000 } ] } - PUT
/api/workspaces/{w}/draftsMembersSave the draft for a conversation you can see, replacing the one there.
bodyis written like a message's. An emptybodydeletes it and answers{ "draft": null }.Example
Body
{ "channelId": "01JB8S…", "threadRootId": null, "body": "Can you check the Northwind order before" }Answer
{ "draft": { "channelId": "01JB8S…", "updatedAt": 1761238800000, … } } - DELETE
/api/workspaces/{w}/drafts?channelId={c}&threadRootId={m}Its ownerForget a draft, for example once it's sent. Leave out
threadRootIdfor 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}/canvasMembersThe conversation's canvas, or
nullbefore anyone has opened it to write, and whether you can edit it.Example
Answer
{ "canvas": { "id": "01JBG1…", "channelId": "01JB8S…", "createdBy": "01JB8Q…", "createdAt": 1761238800000, "updatedAt": 1761242400000, "updatedBy": "01JB8R…" }, "canEdit": true } - GET
/api/channels/{c}/canvas/wsMembersOpens 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/joinChannel membersJoin the channel's huddle, starting one if needed.
connIdis from the WebSocket's hello, so a dropped connection leaves the call. Up to 20 people.Example
Body
{ "connId": "01JBF0…" }Answer
{ "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/leaveMembersLeave the huddle. Works even if you were just removed from the channel. Answers 204.
- POST
/api/workspaces/{w}/call-qualityMembersHow a huddle sounded for you, sent by the apps as you leave: the
huddleIdand areportmeasured from the connection (packet loss, made-up audio, jitter, round trip, an estimated MOS). Nothing about what was said. Answers 204.Example
Body
{ "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}/ratingNo tokenThumbs up or down after a huddle or meeting:
rating1 or -1 and, for -1,issues(echo,noise,choppy,quiet,delay). The report's randomidis the permission, for an hour after it was sent. Answers 204.Example
Body
{ "rating": -1, "issues": ["choppy"] } - POST
/api/channels/{c}/huddle/declineChannel membersTurn down a DM call that's ringing for you. Answers 204.
- PUT
/api/huddles/{id}/stateChannel membersYour state in the call:
muted, and the publishedtrack(microphone),videoandscreen, or null when they stop. After reconnecting, send the newconnId.Example
Body
{ "muted": true } - ANY
/api/partytracks/{huddleId}/*App onlyThe media proxy to Cloudflare Realtime. Only the huddle's participants get through.
- POST
/api/huddles/{id}/recordingApp onlyStart recording the huddle, in
video/webm,audio/webm,video/mp4oraudio/mp4. Everyone in the call is told. 403 when an admin turned recordings off, or when the workspace'srecordingControlishostsand you aren't the huddle's host (whoever started it), a co-host or an admin. Guests can't. AtranscriptOnlystart in a workspace withautoTranscribeskips that rule. With anaudioMime,audioin the answer says whether to also upload an audio-only track for the transcript. WithtranscriptOnly, nothing is recorded:mimeis 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.Example
Body
{ "mime": "video/webm", "audioMime": "audio/webm;codecs=opus" }Answer
{ "recording": { "id": "01JBF4…", … }, "audio": true } - PUT
/api/recordings/{id}/chunks/{seq}App onlyThe recorder's app uploads chunk 0, 1, 2… as the body, up to 32 MB each.
- PUT
/api/recordings/{id}/audio/{seq}App onlyThe audio-only track for the transcript, chunk by chunk like the video, when the start answered
audio: true. - POST
/api/recordings/{id}/stopChannel membersStops 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) orhosts(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}/cohostsChannel membersA 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
cohostslists them.Example
Body
{ "userId": "01JBF2…", "cohost": true }Answer
{ "huddle": { "id": "01JBF4…", "cohosts": ["01JBF2…"], … } } - PUT
/api/huddles/{id}/drawingChannel membersA 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
drawingOffByis the host who turned it off and lines people draw aren't passed on. Each participant'sdrawSlotis their pen colour for the whole huddle. The lines themselves go over the realtime socket (drawframes out,huddle.drawevents in) and are never stored.Example
Body
{ "on": false }Answer
{ "huddle": { "id": "01JBF4…", "drawingOffBy": "01JBF2…", … } } - POST
/api/recordings/{id}/finishApp onlyThe recorder uploaded every chunk (
durationMs).speakersis 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}/recordingsMembersrecordings:readRecordings of huddles and meetings you can see, newest first, up to 100. A meeting's recording has the meeting's
attendees.stateissavingfrom when a recording stops until its file is ready, thensaved, orfailedwhen nothing usable arrived (fileis null until it's saved).transcriptisqueued,working,ready,failed,skipped(with askippedreason:not_set_up,off,allowance,caporempty) or null for recordings made before transcripts.Example
Answer
{ "recordings": [ { "id": "01JBF4…", "kind": "meeting", "title": "Weekly sync", "state": "saved", "startedAt": 1729303200000, "durationMs": 1601000, "file": { "url": "…" }, "transcript": "ready", "skipped": null, … } ] } - GET
/api/recordings/{id}Membersrecordings:readA recording with its
notes(summary, decisions and action items with their owner) and its transcript assegments: who spoke, their user id when they're a member, start and end in ms, and the text.Example
Answer
{ "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}/transcribeMembersTry a transcript again after it failed, or after the workspace had used its plan's transcripts. Answers the recording.
- GET
/api/recordings/{id}/guest-linksMembersA 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;emailis 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.canRevokeis true for the meeting's organizer, whoever recorded it and admins, and only they geturl, the link itself, for links made with Share.Example
Answer
{ "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}AdminsRevoke 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}/sharingMembersWho the recording is shared with in the workspace (
shares: auserIdor achannelId, 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, otherwiseoff,not_allowed,no_transcript,huddleorguest),maxDaysandvideoAllowed.Example
Answer
{ "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}/sharesMembers, not guestsShare 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 optionalcomment. Each message carries an attachment with the recording's title, a link to its page, the start of its summary, andrecording: itsid,kind,startedAt,durationMsand whether it has avideo. The people it's shared with, and everyone in a shared channel, can then see the recording. Answers the sharing, 201.Example
Body
{ "userIds": ["01JBC4…"], "channelIds": ["01JBC9…"], "comment": "Notes from the Acme call" } - DELETE
/api/recordings/{id}/shares/{share}MembersTake 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}/linksMembersMake 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.daysis 1, 3, 7, 14 or 30, up to the workspace'sguestTranscriptDays;videoplays the recording too, whenguestTranscriptVideoallows it. With anemail(not a member of the workspace), the link is emailed there. Answers the link, with itsurl, and the sharing, 201.Example
Body
{ "days": 7, "video": false, "email": "pat@contoso.example" }Answer
{ "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; past it, or when an admin turned AI off, they answer 403. Up to 20 a minute per person (429).
- GET
/api/workspaces/{w}/aiMembers, not guestsWhether AI features are on for the workspace (
enabled) and set up on the server (available), and this month's requests against the allowance (limit, null for no limit).Example
Answer
{ "enabled": true, "available": true, "used": 412, "limit": 800 } - POST
/api/channels/{c}/summaryChannel membersSummarize a thread (
threadRootId) or the channel over aspan:unread(since you last read, the default),dayorweek, up to the newest 400 messages.languageis an app language code; it defaults to yours. Each point links to its message (messageId, andthreadRootIdfor 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. SendAccept: text/event-streamto 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.Example
Body
{ "span": "day", "language": "en" }Answer
{ "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}/translateChannel membersTranslate a message into
language(an app language code; yours by default).sameis true when it already is in that language. Guests get 403.Example
Body
{ "language": "de" }Answer
{ "text": "Neue Bestellung von Contoso …", "same": false } - POST
/api/workspaces/{w}/ai/rewriteMembers, not guestsWriting help: a better version of a draft (
text, up to 4,000 characters) in astyle:fix(spelling and grammar),clearer,shorter,friendlierorformal. It only rewrites the draft, in the draft's language, keeping mentions, links and code. Nothing is sent.Example
Body
{ "text": "hey contoso, thx for the order!! we can deliver by nov 6", "style": "formal" }Answer
{ "text": "Hi Contoso team, thank you for your order. We can deliver by November 6." } - GET
/api/workspaces/{w}/ai/photo-touchupMembers, not guestsHow many profile photo touch-ups you have left today (3 a day, across your workspaces).
Example
Answer
{ "left": 2, "limit": 3 } - POST
/api/workspaces/{w}/ai/photo-touchupMembers, not guestsImprove with AI for a profile photo: send
imageas 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 answersok: 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.Example
Body
{ "image": "/9j/4AAQSkZJRgABAQ…" }Answer
{ "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}/meetingsMembersmeetings:readUpcoming meetings, and the last week's, that you organize, are invited to or joined.
attendeeslists who joined the call, in the order they came in:{ userId, name }, withuserIdnull for a guest. While a call can be going,livelists who's in it and who's waiting in the lobby. Once everyone has left a call that took place,live.callEndedAtsays when, andlive.rejoinUntilhow long the link still lets them back in.lastCallAtis when its last call ended: the link works until 30 days after, andcallslists its calls that are over, oldest first (startedAt,endedAt,attendees). Last week's means scheduled or called in the last 7 days.chatMessagescounts the messages in its chat.emailsays whether invites are emailed.Example
Answer
{ "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}/meetingsMembers, not guestsmeetings:writeSchedule one:
title,startsAt,durationMin(15, 30, 45, 60, 90, 120, 180 or 240), and optionallydescription,timeZone, members byuserIds(those also inoptionalUserIdsare invited as optional) and anyone else byemails(up to 50 people), andwaitingRoom(default true). Invites are emailed unlessnotifyis false;unsentlists addresses that couldn't be. Answers 201.Example
Body
{ "title": "Quarterly review with Acme Corp", "startsAt": 1761310800000, "durationMin": 45, "timeZone": "Europe/London", "userIds": ["01JB8R…"], "emails": ["jordan@acme.example"], "waitingRoom": true }Answer
{ "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 } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } - POST
/api/workspaces/{w}/support-sessionsMembers, not guestsStart a support session: a one-hour meeting starting now, with a waiting room, whose link and
codeyou 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.Example
Answer
{ "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 } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } - PATCH
/api/meetings/{id}Organizer or adminsmeetings:writeChange 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}Organizer or adminsmeetings:writeCancel it and email the cancellation. Anyone in the call is disconnected. Answers 204.
- POST
/api/meetings/{id}/endOrganizer or adminsmeetings:writeEnd the call for everyone; the link stops working. Answers 204.
- POST
/api/meetings/{id}/admitMembers, not guestsLet someone in from the lobby (
admit: true) or turn them away.peerIdis fromlive.waiting. The organizer, invited members and admins can. Answers 204.Example
Body
{ "peerId": "01JBE9…", "admit": true } - POST
/api/meetings/{id}/new-linkOrganizer or adminsmeetings:writeReplace the link; the old one stops working and invitees get the new one.
Example
Answer
{ "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 } ], "waitingRoom": true, "createdAt": 1761200000000, "canceledAt": null, "endedAt": null }, "unsent": [] } - GET
/api/meet/{code}No tokenWhat a meeting link shows before joining: title, time, organizer, workspace, whether it's
early,open,closedorcanceled, and whether guests wait in a lobby. - POST
/api/meet/{code}/joinApp onlyKnock or join, as a member or a guest with a
name(or the personalinvitetoken from their email). Answers atokenfor the call's socket and media, and whether you'readmittedor waiting. - WS
/api/meet/{code}/ws?t=App onlyThe meeting call's socket.
- ANY
/api/meet/{code}/media/{token}/*App onlyThe meeting's media proxy to Cloudflare Realtime.
- POST
/api/meet/{code}/qualityApp onlyHow the call sounded for you, sent as you leave, with the
tokenfrom joining: the samereportas a huddle's. Answers 204. - GET
/api/meet/{code}/chat/{token}App onlyThe call's chat so far, oldest first, for anyone let into the call (members and guests, with the
tokenfrom 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}.Example
Answer
{ "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}App onlySend a message (
body, up to 4,000 characters) to everyone in the call. Answers 201 withmessage.Example
Body
{ "body": "The price list is at https://northwind.example/wholesale" } - POST
/api/meet/{code}/chat/{token}/files?name=App onlySend a file to the call's chat: the file is the body, with
Content-TypeandContent-Length, andwidth/heightfor pictures,textfor a caption. Guests from outside follow the workspace's file rules for guests (403file_blocked). Answers 201 withmessage. - GET
/api/meetings/{id}/chatMembersThe 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}/recordingApp onlyStart 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.Example
Body
{ "mime": "video/webm", "audioMime": "audio/webm;codecs=opus" }Answer
{ "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/calendarSigned inYour connected calendar, if any, and the providers this server can connect to:
google,microsoftandzoho.needsReconnectmeans the provider stopped accepting Freesbe's access.Example
Answer
{ "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/calendarSigned inTurn the automatic "In a meeting" status on or off, and change the Calendar app:
app.workspaceIdturns it on (messages go to your DM with yourself there) or off withnull;agendaMinuteis minutes after midnight in your time zone;reminderMinutesis 1, 5, 10, 15, 30 ornull. Setting your status by hand takes it back from the calendar.Example
Body
{ "autoStatus": false, "app": { "agendaMinute": 450, "reminderMinutes": 5 } } - DELETE
/api/me/calendarSigned inDisconnect the calendar and clear a status it set. Answers 204.
- GET
/api/auth/oauth/{provider}/start?calendar=1App onlyOpen in the browser to connect a calendar. Comes back to
?next=with?calendar=connectedor?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/startApp onlyOpen in the browser to connect Zoho Calendar, in any Zoho data center. Takes the same
next,events=1andworkspace={w}as the Google and Microsoft one and comes back the same way, through Zoho's shared callback. Zoho Calendar events havecanAnswer: false: Freesbe can't answer them, sorsvpanswers 400. - GET
/api/me/calendar/agenda?day=YYYY-MM-DDSigned inCalendar app: your events on a day (today when left out) in your time zone, without declined or canceled ones.
responseisorganizer,pending,accepted,tentativeordeclined;joinUrlis a video call link when the event has one.Example
Answer
{ "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/sendSigned inCalendar app: send today's agenda to your DM with yourself now. Answers 204.
- POST
/api/me/calendar/events/{id}/rsvpSigned inCalendar app: answer an invitation in your calendar; the organizer hears about it. With
messageId, the Calendar message it came from shows the answer.Example
Body
{ "answer": "accepted", "messageId": "01JBF7…" }Answer
{ "event": { "id": "7k2…", "response": "accepted", … } } - GET
/api/workspaces/{w}/calendar-appAdminsWhether 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.Example
Answer
{ "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-appAdminsTurn 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.Example
Body
{ "enabled": false, "addins": true } - GET
/api/me/calendar-addinsSigned inThe 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.
Example
Answer
{ "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-addinsApp onlyWhat 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 withPOST /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.Example
Body
{ "client": "outlook", "workspaceId": "01JB8Q…" } - DELETE
/api/me/calendar-addins/{id}Signed inDisconnect an add-in; its token stops working at once. Meetings it scheduled stay. Answers 204.
- POST
/api/calendar-addins/meetingsApp onlyThe 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 (byeventId, or thecodeof 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'slocale.Example
Body
{ "title": "Quarterly review", "startsAt": 1761912000000, "endsAt": 1761915600000, "timeZone": "America/New_York", "attendees": [ { "email": "jordan@acme.example", "name": "Jordan Lee" } ], "locale": "en-US" }Answer
{ "meeting": { "id": "01JBP9…", "code": "abc-defg-hij", "source": "outlook", … }, "joinUrl": "https://meet.freesbe.net/abc-defg-hij", "workspaceName": "Northwind Traders", "reused": false, "inviteHtml": "<div id="freesbe-meeting">…</div>", "inviteText": "…" } - GET
/api/calendar-addins/meApp onlyWith the add-in's token: who it belongs to and in which workspace.
Example
Answer
{ "name": "Maya Patel", "email": "maya@northwind.example", "workspaceName": "Northwind Traders" } - POST
/api/workspaces/{w}/availabilityMembers, not guestsWhat it takes to find a time for up to 20
userIdsbetweenfromandto, at most 8 days apart.busycomes from each person's connected calendar (calendarisconnected,noneorerror);meetingsare their Freesbe meetings in any workspace, leaving outexceptMeetingIdwhen you're moving one.workingHours(Sunday first, minutes after midnight) are in theirtimezone, anddndUntilis when their paused notifications resume.Example
Body
{ "userIds": ["01JB8R…", "01JB8T…"], "from": 1761292800000, "to": 1761379200000 }Answer
{ "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": [], … } ] }
Booking links
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-linksMembersYour booking links in the workspace, the
baseUrlthey start with (the workspace'scustomDomainwhen it has one), whether your calendar is checked, and whether bookings are emailed. - POST
/api/workspaces/{w}/booking-linksMembers, not guestsMake a link:
slug(3 to 40 lowercase letters, digits and dashes),title,durations, weeklyhours(Sunday first, as minutes after midnight) andtimeZone, and optionallydescription,minNoticeMin,maxDaysAhead,bufferMin,showPhotoandenabled. Answers 201, or 409 when the name is taken.Example
Body
{ "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 }Answer
{ "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}Its ownerChange one of your links. Send the whole link, as when making it;
enabled: falsepauses it. - DELETE
/api/booking-links/{id}Its ownerDelete one of your links. Meetings already booked stay. Answers 204.
- GET
/api/booking-slugs/{slug}Signed inWhether a link name is free.
Example
Answer
{ "slug": "dana-intro", "available": true } - GET
/api/book/{slug}No tokenWhat the booking page shows: title, description, owner, workspace, the lengths offered and the owner's time zone.
- GET
/api/book/{slug}/slots?duration=&from=&to=No tokenThe free start times for one length, between
fromandto(at most 42 days apart).Example
Answer
{ "durationMin": 30, "slots": [1761300000000, 1761301800000, 1761303600000] } - POST
/api/book/{slug}No tokenBook a time:
durationMin,startsAt,name,email, and optionally anoteand the booker'stimeZone. Answers the booker's own meeting link, whether an invite was emailed, andmanageUrl, the booker's page to move or cancel it.Example
Body
{ "durationMin": 30, "startsAt": 1761301800000, "name": "Jordan Lee", "email": "jordan@acme.example", "note": "Pricing for 40 seats", "timeZone": "America/Chicago" }Answer
{ "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=…" } - GET
/api/book/manage/{meetingId}?t=No tokenThe booker's own booking, by the secret
tfrom theirmanageUrl(the part after the dot inmanage=<meetingId>.<secret>): 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). - GET
/api/book/manage/{meetingId}/slots?t=&from=&to=No tokenThe free times the booking can move to, at its own length. Its current time counts as free.
- POST
/api/book/manage/{meetingId}/rescheduleNo tokenMove the booking to a free time:
token(the secret) andstartsAt, and optionally the booker'stimeZone. Both sides get an updated invite. Answers like booking does.Example
Body
{ "token": "…", "startsAt": 1761309000000, "timeZone": "America/Chicago" } - POST
/api/book/manage/{meetingId}/cancelNo tokenCancel the booking:
token, and optionally areasonfor the host. Both sides get a cancellation. Answers 204.Example
Body
{ "token": "…", "reason": "Our budget review moved to November" } - GET
/api/book/{slug}/photoNo tokenThe owner's photo, when the link shows it.
Custom domain
On the Corporate and Enterprise plans, a workspace can serve its booking links, 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-domainAdminsThe workspace's domain, if any, with its
status(pending,activeorfailed), what it's still waiting for, and the DNS records to add. Also the workspace'splanand whether itincludeda custom domain.Example
Answer
{ "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-domainAdminsConnect a subdomain you own (
hostname; a bare domain likenorthwind.examplecan't be used). 403 when the plan doesn't include it, 409 when the workspace already has one or another workspace uses it.Example
Body
{ "hostname": "meetme.northwind.example" } - POST
/api/workspaces/{w}/custom-domain/checkAdminsCheck again whether the DNS record and certificate are ready.
- DELETE
/api/workspaces/{w}/custom-domainAdminsDisconnect 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/clockMembersYour clock: the settings, whether the clock covers you, and whether you're
working, on abreakoroff.Example
Answer
{ "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/clockMembersaction:in,break,resumeorout, with an optionalnoteand your currenttimezone.Example
Body
{ "action": "in", "note": "Opening the Acme Corp warehouse" }Answer
{ "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/entriesMembersEntries between
fromandto(milliseconds, at most 93 days): your own, or for admins everyone's, or one person's withuserId. - POST
/api/workspaces/{w}/time/entriesMembersAdd time after the fact:
kind(workorbreak),startedAt,endedAt,noteand areason. Admins can add it for someone withuserId.Example
Body
{ "kind": "work", "startedAt": 1761116400000, "endedAt": 1761145200000, "note": "Stocktake at the Acme Corp warehouse", "reason": "Forgot to clock in" }Answer
{ "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}MembersChange an entry: your own, or anyone's for admins. Needs a
reason.Example
Body
{ "endedAt": 1761148800000, "reason": "Stayed for the delivery" } - DELETE
/api/workspaces/{w}/time/entries/{id}MembersDelete an entry, with a
reasonin the body. Answers 204. - GET
/api/workspaces/{w}/time/entries/{id}/historyMembersEvery change to an entry, oldest first: who, when, before, after and why.
- GET
/api/workspaces/{w}/time/boardAdminsWho is on the clock right now.
- GET
/api/workspaces/{w}/time/exportAdminsTimesheets as CSV:
fromandtodates (YYYY-MM-DD),formatshiftsordaily, optionally oneuserId. Days are cut in each person's time zone. - GET
/api/workspaces/{w}/time/settingsAdminsThe punch clock settings and who it covers (
trackedUserIds). - PATCH
/api/workspaces/{w}/time/settingsAdminsTurn it on (
enabled), cover everyone (trackEveryone) or some people (trackedUserIds), and setweekStart,timezone, daily and weekly overtime, the longest shift andpaidBreaks.Example
Body
{ "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 above.
- GET
/api/workspaces/{w}/appsAdminsThe workspace's apps, by name. Tokens aren't included, only their last four characters (
tokenHint). - POST
/api/workspaces/{w}/appsAdminsMake an app from
manifest(parse YAML to JSON first). It gets a bot user, in no channels yet, a bot token and asigningSecret; the token is only in this answer. Slack scope and event names are mapped; ones Freesbe doesn't have are listed inunsupportedScopesandunsupportedEvents, and events whose scope is missing ineventsMissingScope. Withsettings.event_subscriptions.request_url, the URL must answer the challenge first, or this answers 400. Answers 201.Example
Body
{ "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
{ "app": { "id": "01JC2A…", "name": "Order desk", "description": "Posts new wholesale orders to #orders", "color": "#2D6A4F", "botUserId": "01JC2B…", "botName": "Order desk", "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}AdminsOne app.
- PATCH
/api/workspaces/{w}/apps/{id}AdminsReplace 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 withdisabled. A turned-off app's token answers 401 and it gets no events.Example
Body
{ "disabled": true } - POST
/api/workspaces/{w}/apps/{id}/tokenAdminsA new bot token, only in this answer. The old one stops working at once.
- DELETE
/api/workspaces/{w}/apps/{id}AdminsDelete 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}/secretAdminsThe signing secret the app's event deliveries are signed with.
Example
Answer
{ "signingSecret": "8f14e45fceea167a5a36dedd4bea2543" } - GET
/api/workspaces/{w}/apps/{id}/deliveriesAdminsThe 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/meApp tokenWhich app, bot user and workspace the token belongs to, and its scopes, like Slack's
auth.test. Needs no scope.Example
Answer
{ "appId": "01JC2A…", "appName": "Order desk", "botUserId": "01JC2B…", "workspaceId": "01JB8Q…", "workspaceName": "Northwind Traders", "scopes": ["channels:read", "channels:join", "chat:write"] } - GET
/api/workspaces/{w}/app-commandsMembersThe slash commands and message actions the workspace's apps add, for the message box and message menus. Turned-off apps are left out.
Example
Answer
{ "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}/commandsChannel membersRun a slash command in the channel (or the thread in
threadRootId): Freesbe sends it to the app that has it.replyis 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.Example
Body
{ "command": "/order", "text": "4512" }Answer
{ "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-actionsChannel membersSend the message to one of an app's message actions, as Slack's
message_action. Answers 204; the app answers later through itsresponse_url, if at all.Example
Body
{ "appId": "01JC2A…", "callbackId": "create_order" } - POST
/api/messages/{m}/app-buttonsChannel membersPress a button on an app's message: Freesbe sends the app Slack's
block_actions.clientTriggeris 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.Example
Body
{ "actionId": "pack_order", "blockId": "order_4471", "clientTrigger": "k3v9q2" } - POST
/api/channels/{c}/app-buttonsChannel membersPress a button on an app's answer only you see. Those aren't kept, so send the button's
textandvaluewith it. Answers 204.Example
Body
{ "appId": "01JC2A…", "ephemeralId": "01JD…", "actionId": "restock", "text": "Restock", "value": "assam" } - POST
/api/app-views/{id}/submitMembersSend an app's dialog.
valuesholds each input's answer by its block id: a string, or a list for checkboxes. The app hears it asview_submission;actionis what it answered:close,clear(close every dialog),errors(by block id), orupdateandpushwith the nextview.Example
Body
{ "values": { "qty": "24", "warehouse": "singapore" } }Answer
{ "action": "errors", "errors": { "qty": "Only 12 cases left" } } - POST
/api/app-views/{id}/buttonsMembersPress a button in an app's dialog, with what's filled in so far. Answers 204.
Example
Body
{ "actionId": "add_item", "values": { "qty": "24" } } - POST
/api/app-views/{id}/closeMembersClose an app's dialog, or every dialog of its stack with
cleared. An app that setnotify_on_closehearsview_closed. Answers 204.Example
Body
{ "cleared": true } - POST
/api/apps/viewsApp tokenOpen a dialog, like Slack's
views.open:triggerIdis thetrigger_idof a command, action or button press (it works for 30 seconds), andviewis a Block Kit modal. Needs no scope.Example
Body
{ "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
{ "view": { "id": "01JD…", "rootId": "01JD…", "parentId": null, "hash": "a1b2c3d4e5f6" } } - POST
/api/apps/views/pushApp tokenAdd a dialog on top of an open one (
views.push), with atrigger_idfrom inside it. Dialogs stack three deep.Example
Body
{ "triggerId": "eyJh…", "view": { "type": "modal", "title": { "type": "plain_text", "text": "Pick a tea" }, "blocks": [] } } - PUT
/api/apps/views/{id}App tokenChange an open dialog (
views.update). Withhash, 409 when it changed since.Example
Body
{ "view": { "type": "modal", "title": { "type": "plain_text", "text": "Order created" }, "blocks": [] }, "hash": "a1b2c3d4e5f6" } - GET
/api/workspaces/{w}/app-hooksAdminsEvery 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}AdminsStop an app's REST hook. The flow it belongs to stops hearing about the event.
- GET
/api/apps/hooksApp tokenThe 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.
Example
Answer
{ "hooks": [ { "id": "01JE…", "event": "message.created", "url": "https://hooks.zapier.com/hooks/standard/…", "channelId": "01JB8R…", "source": "zapier", "createdAt": 1767000000000 } ] } - POST
/api/apps/hooksApp tokenSend one event 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 haschannels:join. Each delivery has the event'stypeanddataas an outgoing webhook's do, plususer: who did it (id,name,isBot, andemailwhen the app hasusers:read). It's signed with the app's signing secret. Answer410and the hook is deleted. 100 hooks per app.Example
Body
{ "event": "message.created", "url": "https://hooks.zapier.com/hooks/standard/…", "channelId": "01JB8R…", "source": "zapier" }Answer
{ "hook": { "id": "01JE…", "event": "message.created", "channelId": "01JB8R…", … } } - DELETE
/api/apps/hooks/{id}App tokenStop a REST hook. Answers 204.
- POST
/hooks/commands/{token}No tokenAn 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": truechanges the message or answer the button was on, and"delete_original": trueremoves it. Answers{ "ok": true }.Example
Body
{ "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}/apps/{id}/sign-inAdminsThe app's Sign in with Freesbe settings, or
client: nullwhile it's off. The client id is the app's id; the secret shows only its last four characters.Example
Answer
{ "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 } } - PUT
/api/workspaces/{w}/apps/{id}/sign-inAdminsTurns it on or changes it. Return addresses must use https (http only for localhost);
publicapps may also use their own reverse-domain scheme and must use PKCE. The first time, the answer includes the clientsecret, shown once.DELETEturns it off.Example
Body
{ "redirectUris": ["https://dashboard.northwind.example/auth/callback"], "who": "members", "requireMfa": true, "preapproved": false, "public": false } - POST
/api/workspaces/{w}/apps/{id}/sign-in/secretAdminsA new client secret, in
secret; the old one stops working at once. - GET
/api/me/connected-appsSigned inApps 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.Example
Answer
{ "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.
- GET
/api/agent-connect/{id}Signed inThe connect page: the agent asking, the permissions it wants, and the person's workspaces with whether each one lets it in (
allowed,approvalwhen an admin has to approve it first,requestedwhile they do,off, orblocked) and which permissions it wouldn't get there (withheld). The id comes from the browser's redirect to/connect/{id}.Example
Answer
{ "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}Signed inAllows the agent in a workspace, or not. The answer's
redirectsends 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.Example
Body
{ "workspaceId": "01JC2W…", "allow": true, "note": "For drafting release notes" }Answer
{ "outcome": "connected", "redirect": "https://claude.ai/api/mcp/auth_callback?code=…&state=…" } - GET
/api/me/agentsSigned inThe agents you connected, most recently used first.
statusisactive,waitingfor an admin's approval, orapprovedonce they gave it (connect again from the agent).DELETE /api/me/agents/{grantId}disconnects one at once.Example
Answer
{ "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}/agentsAdminsThe 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.
Example
Answer
{ "settings": { "policy": "open", "allowPrivate": 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}/agentsAdminsChanges who can connect agents (
off,approvedoropen) and whether they may read private channels and direct messages. Answers likeGET.Example
Body
{ "policy": "approved", "allowPrivate": false } - PATCH
/api/workspaces/{w}/agents/{id}AdminsApproves, allows or blocks an agent (
status), or sets the most it may do here (scopeCeiling, a list of permissions, ornullfor any). Both apply to existing connections at once.DELETEremoves the agent and all its connections from the workspace.Example
Body
{ "status": "allowed", "scopeCeiling": ["channels:read", "channels:history", "chat:write"] } - GET
/api/workspaces/{w}/agents/activityAdminsWhat agents did here, newest first, 50 at a time: connections, approvals, blocks, removals and every tool call. Pass
nextback as?before=for older entries.Example
Answer
{ "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}/answerMembersAnswers a question an agent asked you (the
agent_questionblock on its message).valuefits the question'sanswer.type: text, a day ("2026-11-06"), an ETA ({ "date", "time" }, time optional), the choice or list of choices,trueorfalse, or a number. Only the person it's with can answer, once;409when it's no longer waiting.Example
Body
{ "value": { "date": "2026-11-06", "time": "15:00" }, "note": "Waiting on the carrier pickup" } - POST
/api/agent-questions/{id}/passMembersSays the question isn't yours to answer. It goes to
suggestUserIdif given, or the next person the agent named, or back to the agent as declined.Example
Body
{ "suggestUserId": "01JC2U…" } - POST
/api/agent-questions/{id}/laterMembersCan'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 and outgoing webhooks above for the formats.
- GET
/api/workspaces/{w}/webhooksAdminsEvery webhook you can see, newest first.
- POST
/api/workspaces/{w}/webhooksAdminsCreate an incoming webhook (
kind,name,channelId) or an outgoing one (kind,name,url,events, andchannelIdfor one channel). The incominghookUrlor outgoingsecretis only in this answer. Answers 201.Example
Body
{ "kind": "outgoing", "name": "Support bot", "url": "https://bot.northwind.example/freesbe", "events": ["message.created", "reaction.added"] }Answer
{ "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}AdminsChange
name,url,events,channelId, or turn it off and on withdisabled.Example
Body
{ "disabled": false } - DELETE
/api/workspaces/{w}/webhooks/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/webhooks/{id}/rotateAdminsA new URL (incoming) or signing secret (outgoing). The old one stops working at once.
- POST
/api/workspaces/{w}/webhooks/{id}/testAdminsSend a
webhook.testevent (outgoing). Answers 204. - GET
/api/workspaces/{w}/webhooks/{id}/deliveriesAdminsThe last 14 days of deliveries, newest first (outgoing): event, status, attempts, last HTTP code and error.
- POST
/hooks/custom/{id}/{token}No tokenPost to an incoming webhook.
{id}is the webhook's ID; the token is the secret. Older/hooks/{token}URLs keep working. Answersokas 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-alertsAdminsEvery payment alert you can see, newest first:
provider,channelId,kinds,minAmount, whether it'sready(its signing secret is saved) andlastEventAt. - POST
/api/workspaces/{w}/payment-alertsAdminsConnect an account:
provider(mercury,stripeorairwallex),channelId,kinds(money_in,money_out,balance,other) and an optionalminAmount. Mercury also takesapiToken, used once to register the webhook and not kept. Stripe and Airwallex answer with thehookUrlto paste into their dashboard, only in this answer. Answers 201.Example
Body
{ "provider": "stripe", "channelId": "01J…", "kinds": ["money_in", "other"], "minAmount": 100 } - PATCH
/api/workspaces/{w}/payment-alerts/{id}AdminsChange
channelId,kindsorminAmount, save the provider's signingsecret(Stripe and Airwallex), or turn it off and on withdisabled.Example
Body
{ "secret": "whsec_…" } - DELETE
/api/workspaces/{w}/payment-alerts/{id}AdminsDisconnect it. Answers 204.
- POST
/api/workspaces/{w}/payment-alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/finance/{provider}/{token}No tokenWhere the provider (
mercury,stripeorairwallex) delivers events, signed withMercury-Signature,Stripe-Signature, or Airwallex'sx-timestampandx-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.
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}/krakenAdminsEvery Kraken account you can see, newest first:
channelId,kinds,minUsd,keyHint(the key's last four characters),balanceRules,summaryHourandsummaryTimeZone,checkedAt,lastEventAt, anderrorwhen Kraken refused the key. - POST
/api/workspaces/{w}/krakenAdminsConnect an account:
apiKeyandprivateKey(a key with Query Funds and Query Ledger Entries),channelId,kinds(deposits,withdrawals,trades,other), and optionallyminUsd, up to 10balanceRules(asset,opbeloworabove,amount),summaryHour(0 to 23) andsummaryTimeZone. 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.Example
Body
{ "apiKey": "…", "privateKey": "…", "channelId": "01J…", "kinds": ["deposits", "trades"], "minUsd": 100, "balanceRules": [{ "asset": "BTC", "op": "below", "amount": 0.5 }] } - PATCH
/api/workspaces/{w}/kraken/{id}AdminsChange any of the above, replace the key (
apiKeyandprivateKeytogether), or turn it off and on withdisabled. Turned back on, it carries on from then.Example
Body
{ "summaryHour": 9, "summaryTimeZone": "America/New_York" } - DELETE
/api/workspaces/{w}/kraken/{id}AdminsDisconnect it and forget the key. Answers 204.
- POST
/api/workspaces/{w}/kraken/{id}/testAdminsPost 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-walletsAdminsEvery wallet you can see, newest first:
network,address,name,channelId,directions,minUsd,balanceRules,checkedAt,lastEventAt, anderrorwhen the network's data couldn't be read. Alsonetworks, the ones this server can watch, and the workspace's dailysummary(channelId,hour,timeZone) or null. - POST
/api/workspaces/{w}/crypto-walletsAdminsWatch a wallet:
network(bitcoin,ethereum,base,arbitrum,optimism,polygon,bnb,avalanche,solana,tron), its publicaddress, aname,channelId, and optionallydirections(in,out),minUsdand up to 5balanceRules(asset: the network's coin,USDCorUSDT;opbeloworabove;amount). Answers 400 when the address isn't one for the network or is already watched. Answers 201.Example
Body
{ "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}AdminsChange the
name,channelId,directions,minUsdorbalanceRules, or turn it off and on withdisabled. Turned back on, it carries on from then. The network and address stay as they are.Example
Body
{ "directions": ["in"], "minUsd": 100 } - DELETE
/api/workspaces/{w}/crypto-wallets/{id}AdminsStop watching the wallet. Answers 204.
- POST
/api/workspaces/{w}/crypto-wallets/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- PUT
/api/workspaces/{w}/crypto-wallets/summaryAdminsSet the daily card with every wallet's balances:
summarywithchannelId,hour(0 to 23) andtimeZone, or null to turn it off. Answers 204.Example
Body
{ "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. Admins see the alerts for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/hubspotAdminsWhether the server can connect HubSpot (
available), theconnection(portalId,hubDomain,botUserId,unfurls,brokenAtwhen HubSpot stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/hubspot/connectAdminsThe
urlof 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}/hubspotAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/hubspotAdminsDisconnect: 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/alertsAdminsAdd an alert:
channelId,kinds(deal_created,deal_stage,deal_won,deal_lost,contact_created) and an optionalminAmountfor deals. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 5000 } - PATCH
/api/workspaces/{w}/hubspot/alerts/{id}AdminsChange
channelId,kindsorminAmount, or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/hubspot/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/hubspot/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/crm/hubspotNo tokenWhere HubSpot delivers webhook events for every connected account, signed with
X-HubSpot-Signature-v3andX-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. Admins see the subscriptions for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/githubAdminsWhether the server can install GitHub (
available), theconnection(installationId,account,accountType,repositorySelection,botUserId,unfurls,brokenAtwhen the app was removed or suspended in GitHub) or null, and thesubscriptions. - POST
/api/workspaces/{w}/github/connectAdminsThe
urlof 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}/githubAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/githubAdminsDisconnect: 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/reposAdminsThe
reposthe installation can see (fullName,private), up to 300. - POST
/api/workspaces/{w}/github/subscriptionsAdminsSubscribe a channel to a repository:
channelId,repo(owner/repo), and optionalfeatures(pulls,reviews,issues,comments,commits,releases,deployments,workflows; pull requests, issues, releases and deployments when left out),labelsandbranch. A channel already subscribed to the repository gets the features added. Answers 201.Example
Body
{ "channelId": "01J…", "repo": "northwind-traders/storefront", "features": ["pulls", "releases"] } - PATCH
/api/workspaces/{w}/github/subscriptions/{id}AdminsChange
channelId,features,labelsorbranch, or turn it off and on withdisabled.Example
Body
{ "labels": ["bug"] } - DELETE
/api/workspaces/{w}/github/subscriptions/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/github/subscriptions/{id}/testAdminsPost a sample pull request card into its channel. Answers 204.
- POST
/hooks/dev-tools/githubNo tokenWhere GitHub delivers webhook events for every installation, signed with
X-Hub-Signature-256. Answers 200 to signed deliveries and posts them afterwards (eachX-GitHub-Deliveryonce), 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. Admins see the subscriptions for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/jiraAdminsWhether the server can connect Jira (
available), theconnection(siteUrl,siteName,jiraUserwho Freesbe acts as,botUserId,unfurls,brokenAtwhen Atlassian stopped accepting its access) or null, and thesubscriptions. - POST
/api/workspaces/{w}/jira/connectAdminsThe
urlof 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}/jiraAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/jiraAdminsDisconnect: 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/projectsAdminsThe
projectsthe connected account can browse (key,name), up to 300. - POST
/api/workspaces/{w}/jira/subscriptionsAdminsSubscribe a channel to a project:
channelId,project(its key), and optionalfeatures(created,status,assigned,comments; all but comments when left out) andlabels. A channel already subscribed to the project gets the features added. Answers 201.Example
Body
{ "channelId": "01J…", "project": "OPS", "features": ["created", "comments"] } - PATCH
/api/workspaces/{w}/jira/subscriptions/{id}AdminsChange
channelId,featuresorlabels, or turn it off and on withdisabled.Example
Body
{ "labels": ["customer"] } - DELETE
/api/workspaces/{w}/jira/subscriptions/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/jira/subscriptions/{id}/testAdminsPost a sample issue card into its channel. Answers 204.
- POST
/hooks/dev-tools/jira/{token}No tokenWhere 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-Identifieronce), 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. Admins see the subscriptions for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/gitlabAdminsWhether the server can connect GitLab.com (
available), theconnection(instanceUrl,selfManaged,gitlabUserwho Freesbe acts as,botUserId,unfurls,brokenAtwhen GitLab stopped accepting its access) or null, thesubscriptions, thecallbackUrla self-managed server's application needs as its redirect URI, and for aprivateserver thewebhook(url,secret) its admin adds to each project. - POST
/api/workspaces/{w}/gitlab/connectAdminsThe
urlof GitLab's page that asks to allow Freesbe. Leave the body empty for GitLab.com; for a self-managed server send its httpsinstanceUrland theclientIdandclientSecretof the application made on it. For a private server Freesbe can't reach, sendinstanceUrlandprivate: true: it connects straight away andurlis 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.Example
Body
{ "instanceUrl": "https://gitlab.northwind.example", "clientId": "…", "clientSecret": "…" } - PATCH
/api/workspaces/{w}/gitlabAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/gitlabAdminsDisconnect: 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/projectsAdminsThe
projectsthe connected account is a member of (path,name,private), most active first, up to 300. - POST
/api/workspaces/{w}/gitlab/subscriptionsAdminsSubscribe a channel to a project:
channelId,project(its path), and optionalfeatures(merge_requests,issues,comments,pushes,tags,releases,deployments,pipelines; merge requests, issues, releases and deployments when left out),labelsandbranch. Freesbe adds its webhook to the project;hookErrorsays why when GitLab refused (usually a missing Maintainer role). A channel already subscribed to the project gets the features added. Answers 201.Example
Body
{ "channelId": "01J…", "project": "northwind/storefront", "features": ["merge_requests", "pipelines"] } - PATCH
/api/workspaces/{w}/gitlab/subscriptions/{id}AdminsChange
channelId,features,labelsorbranch, or turn it off and on withdisabled. Turning it on tries the project's webhook again.Example
Body
{ "labels": ["bug"] } - DELETE
/api/workspaces/{w}/gitlab/subscriptions/{id}AdminsDelete it. The project's webhook goes when no channel follows the project any more. Answers 204.
- POST
/api/workspaces/{w}/gitlab/subscriptions/{id}/testAdminsPost a sample merge request card into its channel. Answers 204.
- POST
/hooks/dev-tools/gitlab/{token}No tokenWhere the projects' webhooks deliver, to the connection's own secret address, with its secret in
X-Gitlab-Token. Answers 200 and posts afterwards (eachX-Gitlab-Event-UUIDonce), 404 to an unknown address, and 401 to a wrong token.
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. 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}/salesforceAdminsWhether the server can connect Salesforce (
available), whether the workspace'splanincludes it (included), theconnection(orgId,orgName,instanceUrl,sandbox,botUserId,unfurls,brokenAtwhen Salesforce stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/salesforce/connectAdminsThe
urlof Salesforce's page that asks to allow Freesbe, at test.salesforce.com withsandbox: 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.Example
Body
{ "sandbox": false } - PATCH
/api/workspaces/{w}/salesforceAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/salesforceAdminsDisconnect: 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/alertsAdminsAdd an alert:
channelId,kinds(opportunity_created,opportunity_stage,opportunity_won,opportunity_lost,lead_created,case_created) and an optionalminAmountfor opportunities. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["opportunity_created", "opportunity_won"], "minAmount": 10000 } - PATCH
/api/workspaces/{w}/salesforce/alerts/{id}AdminsChange
channelId,kindsorminAmount, or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/salesforce/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/salesforce/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
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) 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/sourcesAdminsWhere customers write in from: each source's
id,kind(webchat,email,smsorwhatsapp),name,channelId,enabled,testMode, for a web chat itswebchatsettings with the publickey, thesnippetto paste on the site and apageUrlwith the chat on its own, for an email inbox itsemailsettings with theaddressto forward to, for SMS itssms.number, and for WhatsApp itswhatsappsettings: thenumber, whether it'sconnectedto a WhatsApp Business account, and that account'sverifiedName.availablelists the kinds this server can add now, andlivethe kinds that can leave test mode (smsonce texting customers is on,whatsapponce WhatsApp is set up on the server).whatsappIncludedsays whether the workspace's plan includes WhatsApp (Business and up). - POST
/api/workspaces/{w}/inbox/sourcesAdminsAdd a web chat, an email inbox, an SMS inbox or a WhatsApp inbox: a
kind, aname, thechannelIdnew conversations go to (not a DM), and optional settings. A web chat takeswebchat:title,greeting,color(#rrggbb),askEmail, andsites, the websites that may show it (any when empty). An email inbox takesemail:replyTo, the team's own address replies ask customers to answer, andfromName, the name replies come from. An SMS inbox takessms: thenumbercustomers text, with its country code; it starts in test mode. A WhatsApp inbox takeswhatsapp: the WhatsApp Businessnumber, with its country code; it starts in test mode too, and needs a plan that includes WhatsApp (403forbiddenotherwise). Answers 201.Example
Body
{ "kind": "webchat", "name": "Main website", "channelId": "01J…", "webchat": { "color": "#e8590c", "sites": ["www.northwind.example"] } } - PATCH
/api/workspaces/{w}/inbox/sources/{id}AdminsChange its
name,channelId,webchat,email,smsorwhatsappsettings, or turn it off and on withenabled. A web chat that's off disappears from the site; an email inbox that's off bounces mail. For SMS,testMode: falsetakes it live, texting customers for real (only whenlivelistssms, and only on one of the workspace's own phone numbers);testMode: truetakes it back. For WhatsApp,testMode: falseneeds the inbox connected first (connect WhatsApp), and a connected inbox keeps its number.Example
Body
{ "enabled": false } - POST
/api/workspaces/{w}/inbox/sources/{id}/testAdminsOnly for an inbox in test mode: a made-up customer's
text, from the number infrom(a made-up one when left out), with up to 10 of your uploadedfileIdsas photos or files they sent. It lands like a real one, and replies to it are kept withtest: trueinstead of being sent. Answers the conversation and its deliveries.Example
Body
{ "text": "Are you open on Saturday?" } - POST
/api/workspaces/{w}/inbox/sources/{id}/whatsapp/connectAdminsStarts connecting a WhatsApp inbox to the business's WhatsApp Business account: answers the
urlof 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 whenlivelistswhatsapp; the link works for 15 minutes, for the admin who started it. - GET
/api/workspaces/{w}/inbox/sources/{id}/whatsapp/templatesMembers, not guestsA WhatsApp inbox's message templates, from its WhatsApp Business account, approved ones first: each one's
id,name,language,category(UTILITY,MARKETINGorAUTHENTICATION),status(approved,pending,rejected,pausedordisabled),header,bodywith blanks{{1}},{{2}}…,footer, how manyparamsit takes, whether Freesbe can fill it (supported: not with a picture header, header blanks or a link button that changes) and Meta'srejectedReason. Until the inbox is connected,sampleis true and three made-up templates stand in.manageUrlopens the account's templates in WhatsApp Manager.Example
Answer
{ "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/templatesAdminsSends a new template to Meta for review: a lowercase
namewith underscores, alanguagecode likeen_US, acategory(UTILITYorMARKETING), thebodywith blanks{{1}},{{2}}… numbered in order, an example for each blank inexamples, and an optionalfooter. The inbox has to be connected. Answers 201 with the template,pendinguntil Meta approves it.Example
Body
{ "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/startMembers, not guestsWrite to a customer first, with an approved
template(name,languageand theparamsthat fill its blanks), to their WhatsApp numbertowith its country code, and an optionalname. 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 withtest: true.Example
Body
{ "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}AdminsDelete it, with its conversations. Their threads stay in the channel. Answers 204.
- GET
/api/workspaces/{w}/inboxMembers, not guestsThe
sourcesyou can see, a page ofconversations(newest change first) and thecountsof open ones: all, yours and nobody's. Pick a list with?view=open|mine|unassigned|closed, one source with?source=, and page with?before=<updatedAt>.Example
Answer
{ "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}Members, not guestsOne conversation, and the
deliveriesof the replies that went to the customer (messageId,status,error). Thread replies without one were notes. - PATCH
/api/inbox/conversations/{id}Members, not guestsClose or reopen it with
status, or give it to someone in its channel withassigneeId(null: nobody). The inbox bot says so in the thread and mentions the new owner.Example
Body
{ "assigneeId": "01JB8R…" } - POST
/api/inbox/conversations/{id}/suggestMembers, not guestsAn 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}/whatsapp/templateMembers, not guestsSends an approved WhatsApp template into the conversation (its
name,languageand theparamsthat 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.Example
Body
{ "name": "follow_up", "language": "en_US", "params": ["Dana", "your order NW-48231"] } - GET
/api/workspaces/{w}/inbox/repliesMembers, not guestsThe team's saved replies:
id,title,body. - POST
/api/workspaces/{w}/inbox/repliesMembers, not guestsSave a reply with a
titleand abody. Answers 201.Example
Body
{ "title": "Shipping times", "body": "We ship across the EU in 3 to 5 days." } - PATCH
/api/workspaces/{w}/inbox/replies/{id}Members, not guestsChange its
titleandbody. - DELETE
/api/workspaces/{w}/inbox/replies/{id}Members, not guestsDelete it: admins, and whoever saved it. Answers 204.
- GET
/api/chat/{key}No tokenThe web chat's
title,greeting,colorand whether it asks for an email (askEmail). 404 when it's off or gone. - POST
/api/chat/{key}/startNo tokenA visitor's first message (
text, withnameandemailwhen the chat asks): starts a conversation and returns the visitor's secret asvisitorToken, to send asAuthorization: Bearerfrom then on, and theirmessages. Answers 201. - GET
/api/chat/{key}/messagesNo tokenWith the visitor's token: their side of the latest conversation (
fromisvisitororteam; notes never show) after?after=<id>, and whether the teamclosedit. - POST
/api/chat/{key}/messagesNo tokenWith the visitor's token: another message (
text). After the team closed the conversation, it starts a new one.
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. Admins see the alerts for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/pipedriveAdminsWhether the server can connect Pipedrive (
available), theconnection(companyId,companyName,companyUrl,botUserId,receivingwhen Pipedrive sends changes to Freesbe,unfurls,lastEventAt,brokenAtwhen Pipedrive stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/pipedrive/connectAdminsThe
urlof 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}/pipedriveAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/pipedriveAdminsDisconnect: 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/alertsAdminsAdd an alert:
channelId,kinds(deal_created,deal_stage,deal_won,deal_lost,lead_created) and an optionalminAmountfor deals. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 } - PATCH
/api/workspaces/{w}/pipedrive/alerts/{id}AdminsChange
channelId,kindsorminAmount, or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/pipedrive/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/pipedrive/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/crm/pipedrive/{token}No tokenWhere 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. 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-crmAdminsWhether the server can connect Zoho (
available), theconnection(orgId,orgName,dataCenter,crmUrl,botUserId,unfurls,listeningwhile Zoho CRM sends changes,brokenAtwhen Zoho stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/zoho-crm/connectAdminsThe
urlof 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-crmAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/zoho-crmAdminsDisconnect: 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/alertsAdminsAdd an alert:
channelId,kinds(deal_created,deal_stage,deal_won,deal_lost,lead_created,contact_created) and an optionalminAmountfor deals. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 } - PATCH
/api/workspaces/{w}/zoho-crm/alerts/{id}AdminsChange
channelId,kindsorminAmount, or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/zoho-crm/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/zoho-crm/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/crm/zoho-crm/{token}No tokenWhere Zoho CRM delivers one organization's change notifications. The body's
tokenhas 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}/zendeskAdminsThe
connection(subdomain,auth(oauthortoken),email, whether it'sreceivingticket events,needsReconnectandlastEventAt), or null, itsalerts, and whetheroauthsign-in is available. - POST
/api/workspaces/{w}/zendesk/oauthAdminsStart signing in with Zendesk for
subdomain. Answers with theurlto send the browser to; Zendesk comes back to the app with?zendesk=connectedor?zendesk_error=.Example
Body
{ "subdomain": "northwind" } - POST
/api/workspaces/{w}/zendesk/tokenAdminsConnect with an agent's
emailand ZendeskapiTokeninstead. The token is checked with Zendesk and stored encrypted.Example
Body
{ "subdomain": "northwind", "email": "agent@northwind.example", "apiToken": "…" } - DELETE
/api/workspaces/{w}/zendeskAdminsDisconnect: removes Freesbe's webhook from Zendesk, the alerts and the Zendesk app. Answers 204.
- GET
/api/workspaces/{w}/zendesk/groupsAdminsThe Zendesk groups an alert can be limited to, as
groupswithidandname. - POST
/api/workspaces/{w}/zendesk/alertsAdminsAdd a ticket alert:
channelId,events(created,status,comment,priority,assignment) and an optionalgroupId. Answers 201.Example
Body
{ "channelId": "01J…", "events": ["created", "status"], "groupId": "7" } - PATCH
/api/workspaces/{w}/zendesk/alerts/{id}AdminsChange
channelId,eventsorgroupId, or turn it off and on withdisabled. - DELETE
/api/workspaces/{w}/zendesk/alerts/{id}AdminsDelete the alert. Answers 204.
- POST
/api/workspaces/{w}/zendesk/alerts/{id}/testAdminsPost a sample ticket into its channel. Answers 204.
- POST
/hooks/support/zendesk/{token}No tokenWhere Zendesk delivers ticket events, signed with
X-Zendesk-Webhook-SignatureandX-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. 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-deskAdminsWhether the server can connect Zoho (
available), theconnection(orgId,orgName,portalName,dataCenter,deskUrl,botUserId,unfurls,listeningonce Freesbe's webhook is in Zoho Desk,lastEventAt,brokenAtwhen Zoho stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/zoho-desk/connectAdminsThe
urlof 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-deskAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/zoho-deskAdminsDisconnect: 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/departmentsAdminsThe help desk's
departments, each with itsidandname, for alerts that take one. - POST
/api/workspaces/{w}/zoho-desk/alertsAdminsAdd an alert:
channelId,kinds(created,status,reply,priority,assignment) and an optionaldepartmentId. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["created", "reply"], "departmentId": "1234000000006907" } - PATCH
/api/workspaces/{w}/zoho-desk/alerts/{id}AdminsChange
channelId,kindsordepartmentId(null for every department), or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/zoho-desk/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/zoho-desk/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/support/zoho-desk/{token}No tokenWhere 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}AdminsWhether the server can connect Zoho (
available), theconnection(orgId,orgName,currency,dataCenter,appUrl,botUserId,unfurls,checkedAtwhen Freesbe last checked,brokenAtwhen Zoho stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/zoho-{product}/connectAdminsThe
urlof 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=connectedor?zoho-invoice=connected. Only the admin who started it can finish it, within 15 minutes. - PATCH
/api/workspaces/{w}/zoho-{product}AdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/zoho-{product}AdminsDisconnect: Freesbe's access is revoked in Zoho and checking stops. Alerts are kept for a reconnect. Answers 204.
- POST
/api/workspaces/{w}/zoho-{product}/alertsAdminsAdd an alert:
channelId,kinds(sent,payment,paid,overdue) and an optionalminAmount. Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["payment", "overdue"], "minAmount": 500 } - PATCH
/api/workspaces/{w}/zoho-{product}/alerts/{id}AdminsChange
channelId,kindsorminAmount(null for any amount), or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/zoho-{product}/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/zoho-{product}/alerts/{id}/testAdminsPost 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. Admins see the alerts for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/freshdeskAdminsThe
connection(subdomain, the agent'semailandagentName, the secrethookUrlFreshdesk's automation rules call,needsReconnectandlastEventAt), or null, and itsalerts. - POST
/api/workspaces/{w}/freshdeskAdminsConnect, or connect again, with an agent's Freshdesk
apiKey. The key is checked with Freshdesk and stored encrypted; connecting again keeps thehookUrl. Answers like GET.Example
Body
{ "subdomain": "northwind", "apiKey": "…" } - DELETE
/api/workspaces/{w}/freshdeskAdminsDisconnect: forgets the API key, the alerts and the Freshdesk app, and the
hookUrlstops working. Answers 204. - GET
/api/workspaces/{w}/freshdesk/groupsAdminsThe Freshdesk groups an alert can be limited to, as
groupswithidandname. - POST
/api/workspaces/{w}/freshdesk/alertsAdminsAdd a ticket alert:
channelId,events(created,status,reply,priority,assignment) and an optionalgroupId. Answers 201.Example
Body
{ "channelId": "01J…", "events": ["created", "status"], "groupId": "7" } - PATCH
/api/workspaces/{w}/freshdesk/alerts/{id}AdminsChange
channelId,eventsorgroupId, or turn it off and on withdisabled. - DELETE
/api/workspaces/{w}/freshdesk/alerts/{id}AdminsDelete the alert. Answers 204.
- POST
/api/workspaces/{w}/freshdesk/alerts/{id}/testAdminsPost a sample ticket into its channel. Answers 204.
- POST
/hooks/support/freshdesk/{token}No tokenWhere 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 and UptimeRobot. 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/) and [UptimeRobot](/docs/uptimerobot/) guides. Admins see the connections for public channels and the private channels they're in.
- GET
/api/workspaces/{w}/monitoringAdminsThe
connections:provider,name,channelId,minSeverity(info,warning,average,highorcritical),replyOnResolve,serverUrl,hasApiToken,openProblems,lastEventAt,board(the live problems board is pinned),reportSchedule(daily,weeklyor null),reportTimeZoneandnextReportAt. - POST
/api/workspaces/{w}/monitoringAdminsConnect a tool:
provider(zabbix,grafana,alertmanager,pagerduty,opsgenie,datadog,sentry,betterstack,newrelic,uptimekumaoruptimerobot),name,channelId, and optionallyminSeverity,replyOnResolve,serverUrl,apiToken,board(default true),reportScheduleandreportTimeZone(reports post at 9:00 there). Freesbe checks the token with the tool before saving it, encrypted. Answers 201 with thehookUrlto give the tool, shown only now.Example
Body
{ "provider": "zabbix", "name": "Production Zabbix", "channelId": "01J…", "minSeverity": "warning" } - PATCH
/api/workspaces/{w}/monitoring/{id}AdminsChange
name,channelId,minSeverity,replyOnResolve,serverUrl,apiToken(null removes it, and the buttons),board,reportScheduleorreportTimeZone, or turn it off and on withdisabled.Example
Body
{ "minSeverity": "high" } - DELETE
/api/workspaces/{w}/monitoring/{id}AdminsDisconnect it. Cards already posted stay. Answers 204.
- POST
/api/workspaces/{w}/monitoring/{id}/testAdminsPost a sample problem card into its channel. Answers 204.
- POST
/hooks/monitoring/zabbix/{token}No tokenWhere 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}No tokenWhere a Grafana webhook contact point sends: Alertmanager's webhook format (version 4) with Grafana's extra fields. Each alert in
alertsis one card, named by itsfingerprintandstartsAt; aresolvedalert turns its card green. Same answers as Zabbix's. - POST
/hooks/monitoring/alertmanager/{token}No tokenWhere Prometheus Alertmanager's
webhook_configssend, in its webhook format (version 4). Each alert is one card; theseveritylabel sets its severity (high when there's none). Same answers as Zabbix's. - POST
/hooks/monitoring/pagerduty/{token}No tokenWhere a PagerDuty generic webhook (V3) subscription sends incident events. Each incident is one card, named by its
id:incident.triggeredposts it, acknowledgements, reassignments, escalations, priority changes and notes update it, andincident.resolvedturns 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}No tokenWhere an Opsgenie Webhook integration sends alert actions. Each alert is one card, named by its
alertId:Createposts it,Acknowledge,UnAcknowledge,AddNote,AssignOwnershipandUpdatePriorityupdate it, andCloseturns it green. The priority (P1 to P5) sets its severity. Same answers as Zabbix's. - POST
/hooks/monitoring/datadog/{token}No tokenWhere 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,$LINKand more). Each alert cycle is one card:Triggered,WarnandNo Datapost or update it, andRecoveredturns it green. Same answers as Zabbix's. - POST
/hooks/monitoring/sentry/{token}No tokenWhere 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'sunresolvedafter it was resolved gets a new card. Metric alerts (critical,warning,resolved) are cards of their own. The issue'slevelsets its severity. Same answers as Zabbix's. - POST
/hooks/monitoring/betterstack/{token}No tokenWhere a Better Stack Uptime webhook sends incidents (
{ data: { id, type: "incident", attributes } }). Each incident is one card, named by its id:Startedposts it,Acknowledgedupdates it, andResolvedturns it green. Same answers as Zabbix's. - POST
/hooks/monitoring/newrelic/{token}No tokenWhere a New Relic workflow's webhook sends issues, with the payload shown when connecting (the workflow's
issueId,issuePageUrl,priority,state,entitiesData.names,accumulations.conditionNameand more). Each issue is one card:CREATEDandACTIVATEDpost it,ACKNOWLEDGEDupdates it, andCLOSEDturns it green. The priority sets its severity. Same answers as Zabbix's. - POST
/hooks/monitoring/uptimekuma/{token}No tokenWhere an Uptime Kuma Webhook notification sends (
{ heartbeat, monitor, msg }, as application/json). Each outage is one card per monitor:heartbeat.status0 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}No tokenWhere an UptimeRobot webhook alert contact sends, with the payload shown when connecting (
monitorID,name,url,alertType,details,duration,time).alertType1 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.
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}/feedsAdminsThe
feeds:url,siteUrl,name,iconUrl,channelId,keywords,lastCheckedAt,lastItemAt,nextCheckAt,error(while it keeps failing) andbrokenAt(paused after 3 days of failing), and the plan'slimit(null for none). - POST
/api/workspaces/{w}/feeds/previewAdminsFind and read a feed before adding it:
urlis the feed's address or a website's whose page links to its feed. Answers with the feed's ownurl,title,siteUrl,iconUrland its newest 3items, or 400 when there's no feed there.Example
Body
{ "url": "blog.northwind.example" } - POST
/api/workspaces/{w}/feedsAdminsAdd a feed:
url,channelId, and optionallyname(the feed's title),keywords(only items with one of them in the title or summary post) andpostLatest(default true: post the newest item now). Everything already in the feed counts as seen. Answers 201, or 409 at the plan's limit.Example
Body
{ "url": "https://blog.northwind.example/feed.xml", "channelId": "01J…", "keywords": ["tea"] } - PATCH
/api/workspaces/{w}/feeds/{id}AdminsChange
name,channelIdorkeywords, or turn it off and on withdisabled. Saving a paused feed checks it again soon.Example
Body
{ "keywords": ["tea", "coffee"] } - DELETE
/api/workspaces/{w}/feeds/{id}AdminsStop checking the feed. Cards already posted stay. Answers 204.
- POST
/api/workspaces/{w}/feeds/{id}/checkAdminsCheck 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-recruitAdminsWhether the server can connect Zoho (
available), theconnection(orgId,orgName,dataCenter,recruitUrl,botUserId,unfurls,listeningwhile Zoho Recruit sends changes,brokenAtwhen Zoho stopped accepting it) or null, and thealerts. - POST
/api/workspaces/{w}/zoho-recruit/connectAdminsThe
urlof 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-recruitAdminsTurn link previews on or off with
unfurls.Example
Body
{ "unfurls": false } - DELETE
/api/workspaces/{w}/zoho-recruitAdminsDisconnect: 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/alertsAdminsAdd an alert:
channelIdandkinds(candidate_created,candidate_status,candidate_hired,job_created,interview_scheduled). Answers 201.Example
Body
{ "channelId": "01J…", "kinds": ["candidate_hired", "interview_scheduled"] } - PATCH
/api/workspaces/{w}/zoho-recruit/alerts/{id}AdminsChange
channelIdorkinds, or turn it off and on withdisabled.Example
Body
{ "disabled": true } - DELETE
/api/workspaces/{w}/zoho-recruit/alerts/{id}AdminsDelete it. Answers 204.
- POST
/api/workspaces/{w}/zoho-recruit/alerts/{id}/testAdminsPost a sample alert into its channel. Answers 204.
- POST
/hooks/recruiting/zoho-recruit/{token}No tokenWhere Zoho Recruit delivers one organization's change notifications. The body's
tokenhas 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}/workflowsMembersThe workflows you manage, plus the button workflows that are on, by name.
canCreatesays whether you can make one. People who can't edit a workflow don't get its steps.Example
Answer
{ "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}/workflowsMembers, not guestsMake a workflow:
name, optionaldescriptionandemoji, atrigger, up to 30steps, andstatusonoroff. Only admins can when the workspace'sworkflowBuildersisadmins. 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(withconditions,thenandelse). Answers 201.Example
Body
{ "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
{ "workflow": { "id": "01JBK4…", "name": "IT help request", "status": "on", "canEdit": true, … } } - GET
/api/workflows/{id}MembersOne workflow you can see.
Example
Answer
{ "workflow": { "id": "01JBK4…", "name": "IT help request", "trigger": { "type": "manual", … }, "steps": [ … ], … } } - PATCH
/api/workflows/{id}Creator or adminsChange any of the fields above. Turning it
onneeds it free of problems; a schedule's next run is worked out again.Example
Body
{ "status": "off" } - DELETE
/api/workflows/{id}Creator or adminsDelete it and stop its runs that are still going. Answers 204.
- POST
/api/workflows/{id}/runMembers, not guestsRun a workflow that starts from a button. Pass
channelIdto start it from a channel. Its maker can try it while it's off. A form for you comes back asrequest, ready to fill in. Up to 200 runs an hour each.Example
Body
{ "channelId": "01JB9S…" }Answer
{ "run": { "id": "01JBK7…", "status": "waiting", … }, "request": { "id": "01JBK8…", "title": "Ask IT for help", … } } - GET
/api/workflows/{id}/runsCreator or adminsThe latest runs, newest first, each with its status and a log of what it did.
Example
Answer
{ "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-urlCreator or adminsA 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}/cancelCreator or adminsStop a run that is still going. Answers 204.
- GET
/api/workspaces/{w}/workflow-requestsMembersForms, approvals and questions that workflows sent you and that are still open.
kindisform,approvalorask. Questions posted in a channel for anyone to answer aren't listed.Example
Answer
{ "requests": [ { "id": "01JBK7…", "workflowName": "Monthly check-in", "title": "How was your month?", "fields": [ … ], "status": "open", "createdAt": 1761238800000 } ] } - GET
/api/workflow-requests/{id}Its ownerOne request sent to you, open or not, or a question in a channel you're in.
- POST
/api/workflow-requests/{id}Its ownerAnswer it, and the workflow carries on at once. A form takes
answersby question id; an approval takesdecision(approvedorrejected) and an optionalcomment; a question takes one of itschoicesaschoice. Answers 204, or 409 when someone already answered.Example
Body
{ "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}Its ownerClose 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}No tokenStart 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 }, orok: falsewith anerror.Example
Body
{ "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}/phoneMembersYour SIP account, if it's set up. Add
?reveal=1for the password. Guests getprovider: "off".Example
Answer
{ "provider": "sipio", "account": { "username": "dana.whitfield", "domain": "northwind.sip.example", "createdAt": 1761200000000, "rotatedAt": null } } - POST
/api/workspaces/{w}/phoneMembers, not guestsSet up your SIP account. The answer includes the password.
- POST
/api/workspaces/{w}/phone/rotateMembers, not guestsA new SIP password. The old one stops working.
- POST
/api/workspaces/{w}/phone/webphoneMembers, not guestsA short-lived login for calling from Freesbe itself, on a phone system from Freesbe: a SIP over WebSocket
server, theuri,usernameandpassword,iceServersfor the call's audio, and when it runs out (expiresAt). One at a time per person: a new one ends the last. 409 when the workspace has no phone system from Freesbe or it's turned off.inApponGET /phonesays whether to use it.Example
Answer
{ "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 } - GET
/api/workspaces/{w}/phone/callsMembers, not guestsYour calls on the phone system from Freesbe from the last 30 days, newest first, 50 at a time (
?page=2for older ones whilemoreis true).directionis from your side;userIdis the colleague on the other end, andnumberis null for them.secondsis time talking, 0 when unanswered. 409 without a phone system from Freesbe.Example
Answer
{ "calls": [{ "id": "c2", "direction": "in", "number": "+16465550111", "name": "Acme Corp", "userId": null, "startedAt": 1761228800000, "answered": false, "seconds": 0 }], "more": false } - POST
/api/workspaces/{w}/phone/callsMembers, not guestsphone:callClick-to-call: ring someone's Freesbe phone, and once they pick up, dial
tofrom the workspace's number. People start calls for themselves; an app with thephone:callscope names whose phone rings withuserIdoremail(for example a CRM's Call button, from its server).leadIdandvars(up to 20 short values) come back in thephone.call_endedevent, with how it went and how long it lasted. 409 withcall_unreachablewhen the person isn't signed in to Freesbe's phone anywhere (the app on a computer or phone),call_busywhen they're on a call,call_duplicatefor the same call twice within 5 seconds, andconflictwithout a phone system from Freesbe or while it's off.Example
Body
{ "to": "+14155550142", "email": "dana@northwind.example", "leadId": "lead-2291", "vars": { "deal": "Acme renewal" } }Answer
{ "id": "c2c_01JC9A2B3C4D5E6F7G8H9J0K1M", "userId": "01JB8Z6Y0QH3W8T2M5N7P9R1S3" } - GET
/api/workspaces/{w}/sip-connectionAdminsWhether the workspace is connected to sip.io, and how.
- PUT
/api/workspaces/{w}/sip-connectionAdminsConnect sip.io:
apiBase,accountId,apiKey, andlinkByEmailto put each new device on the sip.io user with the same email (off by default). Freesbe checks the key first.Example
Body
{ "apiBase": "https://northwind.sip.example/api", "accountId": "4411", "apiKey": "…", "linkByEmail": false } - DELETE
/api/workspaces/{w}/sip-connectionAdminsDisconnect sip.io. Answers 204.
- GET
/api/workspaces/{w}/phone-systemAdminsThe phone system from Freesbe.
availablesays whether this server offers one,includedwhether the workspace has the Phone system add-on, andenabledwhether it's on.statusisactive,outbound_offwhile a payment is a week overdue (incoming calls still ring), oroff, withoffReason(adminorunpaid) andreleaseAt, when the kept numbers are released.Example
Answer
{ "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-systemAdminsTurn the phone system on, for a
country(a two-letter code; the United States or Canada for now). Answers likeGET.Example
Body
{ "country": "US" } - DELETE
/api/workspaces/{w}/phone-systemAdminsTurn the phone system off. Calls, texts and phone logins stop, and the numbers are kept until
releaseAt. Answers likeGET. - POST
/api/workspaces/{w}/phone-system/turn-on-againAdminsTurn 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/identityAdminsWhere the workspace is with identity verification, which buying numbers needs once:
required, andstatus:none,waiting(thelinkis ready for a phone, untillinkExpiresAt),started,checking,review,approved,rejectedorexpired.POSTstarts a check, or answers the one that's open.Example
Answer
{ "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=415AdminsNumbers for sale.
typeislocal(the default) ortollfree;areaCodeis optional. Each offer has its monthly and setup price incurrency, andneedsDocumentswhen the carrier needs documents before it works.includedis how many local numbers the workspace can still get at no extra cost (one comes with each person on the Phone system add-on).Example
Answer
{ "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/numbersAdminsThe workspace's numbers: each one's
status(active,pending,documents_needed,releasingorother), place, type, who it rings (ringsUserIds, everyone it rings at once;ringsUserIdis set when that's one person), whether it can send and receivetexts, and whether it goes to voicemail after hours (afterHoursVoicemail).Example
Answer
{ "numbers": [ { "id": "01JC5N…", "e164": "+14155550134", "status": "active", "city": "San Francisco", "region": "CA", "numberType": "local", "ringsUserId": "01JB8Q…", "ringsUserIds": ["01JB8Q…"], "texts": true, "afterHoursVoicemail": false } ] } - POST
/api/workspaces/{w}/phone-system/numbersAdminsBuy a number: send back an
offerfrom the search, and optionallyringsUserId, the person its calls ring. Needs identity verification first.statusisactive,pendingordocuments_needed. Answers 201.Example
Body
{ "offer": "…", "ringsUserId": "01JB8Q…" }Answer
{ "number": { "id": "01JC5N…", "e164": "+14155550134", "status": "active", … }, "status": "active" } - PATCH
/api/workspaces/{w}/phone-system/numbers/{id}AdminsChange who a number rings:
ringsUserIds, 1 to 20 people, who all ring at once and the first to answer takes the call (ringsUserIdstill works for one person). The first time a number rings someone, Freesbe makes their phone account.afterHoursVoicemail: truesends its calls outside the business hours to the voicemail of the first person it rings (set the hours first).menuIdanswers it with a phone menu instead, andqueueIdwith a call queue (both turn voicemail after hours off). Send what changes. Answers the number, with itsmenuIdandqueueId.Example
Body
{ "ringsUserIds": ["01JB8R…", "01JB8S…"], "afterHoursVoicemail": true } - GET
/api/workspaces/{w}/phone-system/menusAdminsThe phone menus (IVR): each one's
greeting(typedtextread in avoice, or an uploaded recording withtextnull), theoptionscallers press,noChoice(where the call goes when nobody presses a key, after the greeting plays twice),waitSeconds, and thenumberIdsit answers. An action ispeople(userIds, all ringing at once),voicemail(a person'suserId),menu(menuId),queue(queueId) orhangup.Example
Answer
{ "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/menusAdminsMake a phone menu: a
name, agreeting(textand avoicefromGET …/voices, arecordingIdfrom uploading one, or null for none), up to 12optionswith akey(0 to 9, * or #) and anaction, anoChoiceaction, andwaitSeconds(3 to 30). Answers 201 with the menu. A workspace can have 30.Example
Body
{ "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}AdminsChange a phone menu. Send it whole, like
POST. Answers the menu. - DELETE
/api/workspaces/{w}/phone-system/menus/{id}AdminsDelete 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/queuesAdminsThe call queues: each one's
memberIds(who takes its calls, in order),ring(longestIdle,all,turns,inOrder,fewestCallsorrandom),greeting(like a menu's),holdMusicId(null for the standard music),announcePositionandannounceEverySeconds,ringSeconds,wrapUpSeconds,maxWaitMinutesandmaxCallers(null for no limit), where callers goafterWaitand whennoOneAvailableis signed in (actions, like a menu's), and thenumberIdsit answers.Example
Answer
{ "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/queuesAdminsMake a call queue, with every field above but
idandnumberIds: 1 to 100memberIds,announceEverySeconds30 to 600,ringSeconds5 to 120,wrapUpSeconds0 to 600,maxWaitMinutes1 to 120 andmaxCallers1 to 500 (or null).holdMusicIdis arecordingIdfrom 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.Example
Body
{ "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}AdminsChange a call queue. Send it whole, like
POST. Answers the queue. - DELETE
/api/workspaces/{w}/phone-system/queues/{id}AdminsDelete 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-reasonsAdminsThe reasons people in call queues pick for a break, each with a
name, acolor("#RRGGBB", or null for gray) and alimitMinutestime limit (null for none).Example
Answer
{ "reasons": [{ "id": "pr_2a…", "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 }] } - POST
/api/workspaces/{w}/phone-system/break-reasonsAdminsAdd a break reason: a
name(40 characters at most), acolor, and an optionallimitMinutes(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.Example
Body
{ "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 } - PATCH
/api/workspaces/{w}/phone-system/break-reasons/{id}AdminsRename a break reason or change its color or time limit. Send the
nameandcolor; leaving outlimitMinutestakes the limit off. Answers the reason. - DELETE
/api/workspaces/{w}/phone-system/break-reasons/{id}AdminsDelete a break reason. Answers 204.
- GET
/api/workspaces/{w}/phone-system/supervisorsAdminsWho besides admins can open the live Call center board and nudge people in call queues.
Example
Answer
{ "userIds": ["01J…"] } - PUT
/api/workspaces/{w}/phone-system/supervisorsAdminsReplace the supervisors: up to 50 people in the workspace, not guests. Needs the Call center add-on. Answers like
GET.Example
Body
{ "userIds": ["01J…"] } - GET
/api/workspaces/{w}/phone-system/call-center-report?from=2026-10-01&to=2026-10-31&tz=America/New_York&queues=AdminsCall 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.Example
Answer
{ "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/voicesAdminsThe voices a typed greeting can be read in, in US and Canadian English, US Spanish and Canadian French.
Example
Answer
{ "voices": [{ "id": "1|en-US|Neural2-F", "language": "en-US", "name": "Neural2-F", "gender": "female" }] } - GET
/api/workspaces/{w}/phone-system/greetingsAdminsHear a typed greeting before saving it:
?text=read in?voice=. Answers the audio (WAV). - POST
/api/workspaces/{w}/phone-system/greetingsAdminsUpload a recorded greeting as a WAV file in the multipart field
audio(10 MB at most). Answers 201 with therecordingIdto save in a menu, or as a queue's greeting or hold music.Example
Answer
{ "recordingId": "media_4b…", "seconds": 8 } - GET
/api/workspaces/{w}/phone-system/greetings/{id}/audioAdminsPlay a menu's greeting. Answers the audio.
- GET
/api/workspaces/{w}/phone-system/hoursAdminsThe workspace's business hours, or
nulluntil they're set: atimezone, open windows byday(0 = Sunday) withopenandclosetimes, andclosedDates.Example
Answer
{ "hours": { "timezone": "America/New_York", "days": [{ "day": 1, "open": "09:00", "close": "17:00" }], "closedDates": ["2026-12-25"] } } - PUT
/api/workspaces/{w}/phone-system/hoursAdminsSet the business hours, replacing them all.
closecan be24:00for midnight. Answers the hours.Example
Body
{ "timezone": "America/New_York", "days": [{ "day": 1, "open": "09:00", "close": "17:00" }], "closedDates": ["2026-12-25"] } - GET
/api/workspaces/{w}/phone-system/emergencyAdminsEmergency addresses (E911): the workspace's
locationswith how manypeoplework from each, thedefaultLocationIdused for everyone who hasn't picked one, andalertChannelId, the channel told when someone dials 911 (null: each admin gets a message from Phone). A location'sstatusissaveduntil it's registered with the carrier (registrationfalse means that isn't turned on yet), thenpending,confirmed, orneeds_fixingwith aproblem.Example
Answer
{ "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/emergencyAdminsChange the
defaultLocationId(one of the workspace's locations) oralertChannelId(a channel, or null for the admins). Answers likeGET.Example
Body
{ "alertChannelId": "01JC…" } - POST
/api/workspaces/{w}/phone-system/emergency/locationsAdminsAdd an emergency address in the United States or Canada: a
nameand the streetaddresswith its number, two-letterregionand ZIP or postal code. The first one becomes the default. Answers 201 with the location.Example
Body
{ "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}AdminsChange a location's name or address, like
POST. It's checked with the carrier again. - DELETE
/api/workspaces/{w}/phone-system/emergency/locations/{id}AdminsRemove a location. 409 for the default, or while people work from it. Answers 204.
- GET
/api/workspaces/{w}/phone/queue-statusMembers, not guestsYour place in the call queues: the
queuesyou're in (empty when none, or without the Call center add-on),status(taking,breakorout), thebreakReasonIdof a break,sincewhen,phoneOfflinewhen you take calls but your Freesbe phone isn't open anywhere, and thebreakReasonsyou can pick.Example
Answer
{ "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-statusMembers, not guestsTake your queues' calls (
taking), take abreak(with abreakReasonIdor null), or sign out (out). Calls to your own number ring either way. 409 when you're in no queue. Answers your status.Example
Body
{ "status": "break", "breakReasonId": "pr_2a…" } - GET
/api/workspaces/{w}/phone/call-centerMembers, not guestsThe live Call center board, for admins and supervisors (403 for anyone else, or without the Call center add-on): the
queueswith their people,breakReasons, andlive: 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).agentIdsmaps the ids inliveto people.socketis a one-time, one-minutewss://URL that sends a freshliveas asnapshotframe whenever something changes; null when live updates aren't available.Example
Answer
{ "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/socketMembers, not guestsA new one-time
socketURL for the Call center board, to reconnect. Admins and supervisors only.Example
Answer
{ "socket": { "url": "wss://…", "expiresAt": 1767000060000 } } - POST
/api/workspaces/{w}/phone/call-center/nudgeMembers, not guestsNudge someone in a call queue with a direct message (
message, 500 characters at most),fromyou (me) or from the Call center bot (bot). Admins and supervisors only. Answers 204.Example
Body
{ "userId": "01J…", "message": "Callers are waiting. Can you come back from your break?", "from": "bot" } - GET
/api/workspaces/{w}/phone/directoryMembers, not guestsThe colleagues with a phone, by name, with their
extension(null when they have none) and FreesbeuserId(null for a phone system user who isn't a member), and the callqueues(empty without the Call center add-on): who the phone can call and pass calls to.Example
Answer
{ "people": [{ "userId": "01J…", "name": "James Okafor", "extension": "102" }], "queues": [{ "id": "q_4d…", "name": "Support" }] } - POST
/api/workspaces/{w}/phone/transferMembers, not guestsPass your current call on.
startwithmodenow(the caller holds until the target answers, then goes to them; nobody answering leaves the caller with you) ortalk(you talk to the target first while the caller holds), to atarget:{ kind: "person", userId },{ kind: "queue", id }(alwaysnow) or{ kind: "number", number }(a colleague's extension goes to them). Talking first answers atransferId; thencompletehands the caller over and ends your side,mergemakes it a 3-way call, andcancelgoes 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.Example
Body
{ "action": "start", "mode": "talk", "target": { "kind": "person", "userId": "01J…" } }Answer
{ "transferId": "call_7f…" } - GET
/api/workspaces/{w}/phone/voicemailsMembers, not guestsYour own voicemails, newest first: who called (
number, andnamefor a colleague), when (at), how long (seconds), whether it'sunread, and theunreadcount.Example
Answer
{ "voicemails": [{ "id": "vmsg_3c…", "number": "+14155550187", "name": null, "at": 1767000000000, "seconds": 42, "unread": true }], "unread": 1 } - PATCH
/api/workspaces/{w}/phone/voicemails/{id}Members, not guestsMark one of your voicemails heard (
unread: false) or not. Answers 204.Example
Body
{ "unread": false } - DELETE
/api/workspaces/{w}/phone/voicemails/{id}Members, not guestsDelete one of your voicemails. The copy in your DM from Phone stays. Answers 204.
- GET
/api/workspaces/{w}/phone/voicemails/{id}/audioMembers, not guestsThe recording, as WAV. Playing it marks it heard.
- GET
/api/workspaces/{w}/phone/emergencyMembers, not guestsWhere your 911 calls say you are: the workspace's
locationsand your own, the one you picked (locationId, null for the workspace'sdefaultLocationId). - PUT
/api/workspaces/{w}/phone/emergencyMembers, not guestsSay where you work from:
locationId(one of the workspace's or your own, null for the default), orownto add a place of yours, like Home, and pick it (up to 5). Answers likeGET.Example
Body
{ "own": { "name": "Home", "address": { "line1": "12 Oak Ave", "city": "Oakland", "region": "CA", "postalCode": "94610", "country": "US" } } } - DELETE
/api/workspaces/{w}/phone/emergency/locations/{id}Members, not guestsRemove 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}AdminsRelease 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}/importsAdminsImports, newest first, with their status, phase and counts.
- POST
/api/workspaces/{w}/importsAdminsStart an upload: the zip's
fileNameandsize. The answer'spartSizeis how big each part is.Example
Body
{ "fileName": "Northwind Slack export.zip", "size": 734003200 } - GET
/api/workspaces/{w}/imports/{id}AdminsOne import and its progress.
- PUT
/api/workspaces/{w}/imports/{id}/parts/{part}AdminsUpload part 1, 2, 3… of the zip as raw bytes. Every part but the last is exactly
partSize. AnswerspartNumberandetag. - POST
/api/workspaces/{w}/imports/{id}/startAdminsThe upload is complete: send every part's
partNumberandetag, and the import starts.Example
Body
{ "parts": [ { "partNumber": 1, "etag": "…" }, { "partNumber": 2, "etag": "…" } ] } - POST
/api/workspaces/{w}/imports/{id}/cancelAdminsStop an upload or a running import.
- POST
/api/workspaces/{w}/claimsAdminsSign-in links for imported people who haven't signed in: everyone, or
userIds.email: truealso emails each person theirs.Example
Body
{ "email": true } - GET
/api/claims/{token}No tokenWhat 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-emailNo tokenEmail a sign-in link if the address belongs to an imported account. Answers 204 either way.
- POST
/api/auth/claimNo tokenTake over an imported account: the link's
token, apassword, and anemailwhen Slack had none. Answers like login. - POST
/api/claims/{token}/acceptSigned inAdd the imported account's history to the account you're signed in to.
Realtime
- WS
/api/workspaces/{w}/rtMembersThe workspace WebSocket: every change as it happens. See Realtime events above.
Operator
- GET
/api/operator/usageFreesbe staffUsage per workspace for the people who run the server:
?days=(0 for all time) and&workspace=for its people. - GET
/api/operator/call-qualityFreesbe staffHow calls sounded, from the apps' reports:
?days=(0 for all time) and&workspace=. By day, app and version, connection route, system and workspace, with the latest poor calls and what people said was wrong. - GET
/api/operator/spendFreesbe staffThis month's running costs against the cap, broken down by calls, email and storage, with each workspace's video and storage.
- POST
/api/operator/spendFreesbe staffPause or resume video calls everywhere (
paused). - PATCH
/api/operator/workspaces/{w}/planFreesbe staffSet a workspace's
plan:free,business,corporateorenterprise. Billing isn't built yet. Answers 204. - PATCH
/api/operator/workspaces/{w}/limitsFreesbe staffSet a workspace's own monthly limits:
videoGb,storageGb,aiRequestsandspendUsd(dollars of usage before free allowances; past it video, recordings, transcripts and AI stop there). null uses the default (the plan's allowance for AI, no spending limit), 0 means no limit; leavingaiRequestsorspendUsdout keeps it. Answers 204.Example
Body
{ "videoGb": 200, "storageGb": null, "aiRequests": 5000 }
Not in the API yet
The Freesbe apps use this API for almost everything, so a script can do it too. These are the exceptions, and what to do instead for now.
| What | Today | Instead |
|---|---|---|
| Bot tokens from other workspaces' apps | Apps belong to one workspace: an admin makes them there, and their bot token only works there. | Make the app again in each workspace from the same manifest. Public apps that any workspace installs come later. |
| A list of waiting guests | Each request shows up as a guest_request message in its channel, which holds the requestId. | Read it from the channel's messages or the message.created event, then approve or decline. |
| Webhooks for people, calls and meetings | Outgoing webhooks send messages, reactions, new channels and channel members. New workspace members, huddles, statuses and the punch clock are only on the WebSocket. | Use the realtime WebSocket. |
| Looking someone up by email | There's only the full member list. | Fetch the list once and keep a map of emails to user ids. |
| Search filters | Search takes text and one channel. | Filter the results by userId and createdAt yourself. |
| Passkeys, Google and Microsoft sign-in, and the calls themselves | They need a browser prompt, a redirect or a live audio and video connection. | Use the Freesbe apps. These are marked App only in the reference. |
| Leaving a workspace, deleting an account or a workspace, handing over ownership | These aren't in the app either. | Ask a workspace admin to deactivate the account. |