Freesbe Freesbe

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 ofX-Freesbe-Locale (or Accept-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,ro or ms. Error messages are in English.
Endpoints
594, 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.

LabelWho
No tokenAnyone can call it, without a token.
Signed inAny valid token. It isn't tied to a workspace, so workspace sign-in rules don't apply.
MembersAnyone in the workspace, guests included. Guests only see their own channels and the people in them.
Members, not guestsOwners, admins and members. Guests get 403 forbidden.
Channel membersPeople who can see the channel: anyone in the workspace for a public channel, members for a private channel or DM.
Creator or adminsThe person who created the channel, or a workspace owner or admin.
Organizer or adminsThe person who scheduled the meeting, or a workspace owner or admin.
Its ownerOnly the person it belongs to, such as the maker of a booking link.
AdminsWorkspace owners and admins.
App onlyNeeds 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 tokenOnly an app's bot token (see Apps). People's tokens get 403 forbidden.
Freesbe staffOnly 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:write

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

ScopeLets the app
team:readSee the workspace's name and settings
users:readSee the people in the workspace
users:writeSet the app's own status
channels:readSee channels and who is in them
channels:joinJoin and leave public channels
channels:manageCreate channels, rename them, archive them and add people
channels:historyRead messages in channels the app is in
chat:writeSend messages as the app, schedule them, and edit or delete them
im:writeStart direct messages with people
reactions:writeAdd and remove emoji reactions
pins:writePin and unpin messages
bookmarks:writeAdd, change and remove the links kept at the top of a channel
files:readOpen files shared in channels the app is in
files:writeUpload files and attach them to its messages
search:readSearch messages in channels the app is in
todos:readSee the app's own to-dos
todos:writeMake, change and delete the app's own to-dos
meetings:readSee the workspace's meetings
meetings:writeSchedule meetings and change the ones the app made
recordings:readOpen huddle and meeting recordings the app can see
phone:callStart phone calls: ring someone's phone, then dial the number
app_mentions:readHear 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_added

When 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" } }
StatusCodeMeaning
400bad_requestThe body isn't JSON, or a parameter is malformed.
400invalidA field failed validation. message says which.
401unauthorizedNo token, or it expired or was signed out.
403forbiddenYou're signed in but not allowed, for example not an admin.
403sso_requiredThe workspace requires Google or Microsoft sign-in, and this token signed in another way.
403mfa_requiredThe workspace requires multi-factor authentication, and this token hasn't verified a second step (lately). Verify with POST /api/me/mfa/totp.
403missing_scopeAn app's bot token called an endpoint its scopes don't cover. message names the scope to add to the app's manifest.
403not_allowed_token_typeAn app's bot token called an endpoint apps can't use, such as sign-in or your account.
403file_blockedThe 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.
403unclaimedThe email belongs to an account imported from Slack that nobody has taken over yet. message says what happens next.
404not_foundIt doesn't exist, or you can't see it. Private channels you're not in answer 404 too.
410not_foundA meeting link whose meeting was canceled or has ended.
409conflictIt clashes with something, such as a channel name in use.
413too_largeThe body or file is too big, or the workspace is out of storage.
429rate_limitedSlow down and try again in a minute.
502upstreamA service Freesbe depends on failed.
503unavailableThat feature is turned off on this server.
500internalSomething 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 } }
EventHolds
message.createdmessage, and thread counters for a reply
message.updatedmessage
message.deletedchannelId, messageId, threadRootId, thread
reaction.added / reaction.removedchannelId, messageId, emoji, userId
pins.changed / bookmarks.changedchannelId: fetch its pins or bookmarks again
channel.created / channel.updatedchannel
channel.prefsYour own muted, notify and sound changed, on another device
person.soundYou picked a sound for messages from userId, on another device
member.joined / member.leftchannel, userId
workspace.member_added / workspace.member_updatedmember
presence.changeduserId, state (active, away, offline), inHuddle
status.changedstatus
typingchannelId, userId, threadRootId. Not replayed.
read.updatedYou read channelId up to lastReadId on another device
saved.changedYour Later list changed, on another device
reminder.dueA reminder went off: the saved item, with its message
huddle.started / huddle.updatedhuddle
huddle.endedhuddleId, channelId
huddle.ringA DM call for you: huddleId, channelId, fromUserId, expiresAt
huddle.declinedhuddleId, channelId, userId
time.changedSomeone's punch clock (sent to them and to admins)
time.settings_changedAn admin changed the punch clock settings
devices.changedWhich of your open apps alerts for messages. Not replayed.
keywords.changedkeywords: your keyword alerts. Only to you.
emoji.changednothing: fetch the workspace's custom emoji again
sidebar.sectionssections: all your sidebar sections, in order. Only to you.
channel.sectionchannelId, 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

FieldWhat Freesbe does with it
textThe message, in mrkdwn. Up to 40,000 characters.
blocksBlock Kit isn't drawn, but when text is empty the text of section and context blocks is used.
attachmentsLegacy attachments, up to 20: color, pretext, author_name, author_link, title, title_link, text, fields, image_url, thumb_url, footer, ts, fallback.
usernameThe sender name shown on the message. Defaults to the webhook's name.
icon_emojiThe sender icon, as :shortcode:.
icon_urlThe 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.

StatusBodyMeaning
200okThe message was posted.
400invalid_payloadThe body isn't a JSON object, or a form without a JSON payload.
400no_textNo text, block text or attachments to show.
404no_serviceThe URL is wrong, rotated, deleted or turned off.
404channel_not_foundThe channel the webhook posts to is gone.
410channel_is_archivedThe channel is archived.
413payload_too_largeThe body is over 1 MB.
429rate_limitedMore 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.

Eventdata holdsApp scope
message.createdmessage, channelchannels:history
message.updatedmessage, channelchannels:history
message.deletedchannelId, messageId, threadRootId, channelchannels:history
file.sharedmessage (with its files), channelfiles:read
app.mentionedmessage, channel. Apps only: a message that mentions the app's bot.app_mentions:read
reaction.addedchannelId, messageId, emoji, userId, channelchannels:history
reaction.removedchannelId, messageId, emoji, userId, channelchannels:history
channel.createdchannel, userId (who made it)channels:read
channel.renamedchannel, userId, previousNamechannels:read
channel.updatedchannel, userId. The topic changed.channels:read
channel.archivedchannel, userIdchannels:read
channel.unarchivedchannel, userIdchannels:read
member.joinedchannel, userIdchannels:read
member.leftchannel, userIdchannels:read
user.joinedmember: someone joined the workspaceusers:read
user.updatedmember: their name, title, picture or role changedusers:read
user.deactivatedmemberusers:read
user.status_changedstatususers:read
huddle.startedhuddleId, channel, userId (who started it), startedAtchannels:read
huddle.joinedhuddleId, channel, userId, startedAtchannels:read
huddle.lefthuddleId, channel, userId, startedAtchannels:read
huddle.endedhuddleId, channel, startedAt, endedAt, participantIdschannels:read
meeting.scheduledmeeting: id, code, title, startsAt, durationMin, timeZone, organizerId, inviteeUserIdsmeetings:read
meeting.updatedmeetingmeetings:read
meeting.canceledmeetingmeetings:read
meeting.guest_waitingmeeting, guestName: someone knocked in the waiting roommeetings:read
booking.createdbooking: meetingId, bookingLinkId, userId, name, email, note, startsAt, durationMinWebhooks only
recording.readyrecording: id, channelId, meetingId, startedAt, durationMs; and channelrecordings:read
transcript.readyrecording, channelrecordings:read
time.clocked_inuserId, atWebhooks only
time.clocked_outuserId, atWebhooks only
phone.call_endedcall: 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.testtext. 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 token

    The server's name and version, the sign-up policy (first, invite or open), 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 token

    Answers { "ok": true } while the API is up.

  • POST/api/auth/signupNo token

    Create an account. Needs an inviteToken unless sign-up is open; the invite also joins its workspace. acceptTerms: true records that the person agreed to the current Terms of Service and Privacy Policy; without it the apps ask them to agree before they go on. Answers 201 like login. 10 sign-ups a minute from one IP address.

    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 token

    Sign in with an email and password. Send wantToken: true to get a token in the answer. 10 tries a minute per address and per email.

    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 in

    End the session the token belongs to. Answers 204.

  • GET/api/meSigned in

    The 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 in

    Change your name (up to 80 characters) or timezone (an IANA zone), set your workingHours (seven days, Sunday first, each a list of [start, end] minutes after midnight in your time zone; null goes back to Monday to Friday 9 to 5), or turn off the email about guests waiting in your meetings' lobbies (lobbyEmail). Answers like GET /api/me.

    Example

    Body

    { "name": "Dana Whitfield", "timezone": "America/New_York" }
  • PUT/api/me/passwordSigned in

    Change your password, or set one on an account made with Google or Microsoft (leave out currentPassword). Signs out every other session. Answers 204.

    Example

    Body

    { "currentPassword": "…", "newPassword": "…" }
  • PUT/api/me/avatarSigned in

    Set your picture: the raw PNG, JPEG, GIF or WebP bytes with their Content-Type, up to 2 MB. Answers like GET /api/me.

    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 in

    Remove your picture, so your initials show. Answers like GET /api/me.

  • GET/api/me/avatar/{provider}Signed in

    The photo Google or Microsoft (google or microsoft) shared the last time you signed in with it, as image bytes, kept even when you use another picture. Answers 404 when there's none. To use it, upload it with PUT /api/me/avatar.

  • POST/api/me/photo-handoffsSigned in

    A one-time link for taking your picture with your phone, shown as a QR code. url opens a page in the phone's browser that sends one photo back. It works once, for 10 minutes. Nothing changes until you upload the picture with PUT /api/me/avatar.

    Example

    Answer

    { "code": "kq3V…", "url": "https://app.freesbe.net/snap#kq3V…", "expiresAt": 1761462600000 }
  • GET/api/me/photo-handoffs/{code}Signed in

    Whether the phone sent the photo: waiting, ready, or gone (expired, cancelled or already picked up).

    Example

    Answer

    { "state": "ready" }
  • POST/api/me/photo-handoffs/{code}/photoSigned in

    The phone's photo as raw image bytes, once. The link is used up.

  • DELETE/api/me/photo-handoffs/{code}Signed in

    Cancel the link, and drop a photo the phone already sent. Answers 204.

  • GET/api/photo-handoffs/{code}No token

    For the phone's page: the first name of the person whose picture it takes. 404 once the link expired or was used.

    Example

    Answer

    { "name": "Dana", "expiresAt": 1761462600000 }
  • PUT/api/photo-handoffs/{code}No token

    For the phone's page: the photo, as raw JPEG, PNG or WebP bytes with their Content-Type, up to 8 MB. One per link. Answers 204.

  • GET/api/avatars/{userId}Signed in

    Someone's picture, if you share a workspace with them. avatarUrl on people points here.

  • GET/api/me/sign-inSigned in

    How you can sign in: whether you have a password, your linked Google and Microsoft accounts, your passkeys, and whether an authenticator app is set up.

    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 in

    Unlink your google or microsoft account. Answers 204.

  • POST/api/me/claimsSigned in

    When importedHistory is true, emails you a link for each Slack account imported under your address, to add its history to your account. Answers 204.

Email sign-in and your address

These need a server that can send email (email: true in /api/meta), and answer 503 unavailable otherwise.

  • POST/api/auth/email-codeNo token

    Email a one-time sign-in code. Answers 204 whether or not the address has an account, so it doesn't reveal who does. 10 codes a day per address.

    Example

    Body

    { "email": "dana@northwind.example" }
  • POST/api/auth/email-code/verifyNo token

    Sign in with the emailed code. Answers like login.

    Example

    Body

    { "email": "dana@northwind.example", "code": "482913", "wantToken": true }
  • POST/api/auth/verify-emailNo token

    Verify an address from the link in a verification email (its token). Answers { "email": "…" }.

  • POST/api/me/email/verificationSigned in

    Email yourself a new verification code and link. Answers 204.

  • POST/api/me/email/verifySigned in

    Verify your address with the emailed code. Answers like GET /api/me.

    Example

    Body

    { "code": "482913" }
  • PUT/api/me/emailSigned in

    Start changing your address: emails a code to the new one. Needs currentPassword when the account has a password. Answers 204.

    Example

    Body

    { "email": "dana.whitfield@northwind.example", "currentPassword": "…" }
  • POST/api/me/email/confirmSigned in

    Finish the change with the code sent to the new address. Answers like GET /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 only

    WebAuthn options for navigator.credentials.get().

  • POST/api/auth/passkey/loginApp only

    Sign in with the passkey's assertion (response). Answers like login.

  • GET/api/me/passkeysSigned in

    Your passkeys: id, name, backedUp, createdAt, lastUsedAt.

  • POST/api/me/passkeys/optionsApp only

    WebAuthn options for navigator.credentials.create(). Refused with a token: add passkeys from the app.

  • POST/api/me/passkeysApp only

    Save a new passkey from the browser's response, with an optional name.

  • PATCH/api/me/passkeys/{id}Signed in

    Rename a passkey.

    Example

    Body

    { "name": "Work laptop" }
  • DELETE/api/me/passkeys/{id}Signed in

    Remove a passkey. Answers 204.

  • GET/api/auth/oauth/{provider}/startApp only

    Open in the browser to sign in with google or microsoft. ?next=/path returns there, ?invite=<token> signs up through an invite and ?link=1 links the account instead.

  • GET/api/auth/oauth/{provider}/callbackApp only

    Where Google and Microsoft send people back.

  • POST/api/auth/handoffApp only

    The desktop app's sign-in through the system browser: a one-time code for a PKCE challenge.

  • POST/api/auth/handoff/redeemApp only

    The desktop app trades the code and its verifier for a session.

Sessions and multi-factor authentication

In a workspace that requires multi-factor authentication, its endpoints answer 403 mfa_required until the token verifies a second step. A script verifies with an authenticator app: set one up once with /api/me/totp, then send a code to /api/me/mfa/totp after each sign-in, and again when the workspace asks.

  • GET/api/me/sessionsSigned in

    Everywhere you're signed in: device, app, sign-in method, address and rough location, when it started and was last used. current marks this one.

    Example

    Answer

    { "sessions": [
      { "id": "…", "current": true, "method": "password", "createdAt": …, "lastActiveAt": …,
        "ip": "203.0.113.24", "location": "London, United Kingdom", … }
    ] }
  • DELETE/api/me/sessionsSigned in

    Sign out every other session, keeping this one. Answers 204.

  • DELETE/api/me/sessions/{id}Signed in

    Sign out one session. Answers 204.

  • POST/api/me/termsSigned in

    Agree to the current Terms of Service and Privacy Policy. Send the version you were shown; an older one answers 409. Signed-in browser sessions only. GET /api/me has termsVersion, the version you last agreed to, or null. Answers like GET /api/me.

    Example

    Body

    { "version": "2026-10-28" }
  • GET/api/me/security-activitySigned in

    Your security activity, newest first, 50 at a time: each sign-in, second step, sign-out and change to how you sign in (event: sign_in, sign_in.failed, mfa.verified, mfa.failed, sign_out, session.ended, sessions.ended, password.set, password.changed, email.changed, authenticator.added, authenticator.removed, passkey.added, passkey.removed, sso.linked, sso.unlinked), with what happened (detail: the sign-in method, a passkey's name, the provider), the browser or app, the IP address and where it was. Narrow it with ?event=, ?from= and ?to= (times in milliseconds) and ?limit= (up to 5000). When more is true, pass the last event's id as ?before= for the next page. Events are kept for a year.

    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 in

    Whether your account is being deleted (pending, with the purge date), what stops you deleting it (blockers: workspaces you own, owner with other people in them or alone), and the workspaces you'd leave.

    Example

    Answer

    { "pending": null, "blockers": [], "workspaces": [{ "id": "01JB…", "name": "Northwind Traders" }] }
  • POST/api/me/deletionSigned in

    Deletes your account in 30 days: you leave every workspace and every session, passkey and app or agent grant ends now. Signed-in browser sessions only, with a second factor verified in the last 15 minutes (mfa_required). Answers 409 while you own a workspace. Until the purge, signing in works but every other endpoint answers 403 account_deleting.

    Example

    Body

    { "reason": "Moving to another team" }

    Answer

    { "purgeAt": 1795322400000 }
  • DELETE/api/me/deletionSigned in

    Keeps your account, if it's still waiting to be deleted. Workspaces' admins reactivate you. Answers 204.

  • GET/api/me/mfaSigned in

    When this session last verified a second step, and which ways it can: phone (a phone set up to approve sign-ins), passkey, totp, email, password. setup means none yet: setting up a passkey or authenticator app counts.

    Example

    Answer

    { "verifiedAt": null, "phone": false, "passkey": false, "totp": true, "email": true, "password": false, "setup": false }
  • POST/api/me/mfa/totpSigned in

    Verify with a code from your authenticator app. Each code works once. Answers like GET /api/me.

    Example

    Body

    { "code": "123456" }
  • POST/api/me/mfa/emailSigned in

    Email a code to verify with. Not after signing in with an emailed code. Answers 204.

  • POST/api/me/mfa/email/verifySigned in

    Verify with the emailed code. Answers like GET /api/me.

  • POST/api/me/mfa/passwordSigned in

    Verify with your password, only after signing in with an emailed code. Answers like GET /api/me.

  • POST/api/me/mfa/passkey/optionsApp only

    WebAuthn options to verify with one of your passkeys.

  • POST/api/me/mfa/passkeyApp only

    Verify with the passkey's assertion.

  • POST/api/me/mfa/phoneSigned in

    Ask your phones to approve this session's second step. Show the number: the phone asks for it among three. Lasts two minutes; at most two can wait at once, and none for 15 minutes after the phone reports one that wasn't you.

    Example

    Answer

    { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "number": 47, "expiresAt": 1792771320000, "phones": 1 }
  • POST/api/me/mfa/phone/{id}Signed in

    Whether the phone answered: pending, denied, expired, or approved with me (like GET /api/me), which verifies this session once. DELETE stops waiting.

    Example

    Answer

    { "status": "pending" }
  • POST/api/auth/phoneNo token

    Sign in with no password: ask that person's phones to approve. Show the number and keep the secret for the checks. Answers the same whether or not the address has an account or a phone, so it doesn't reveal who does. DELETE with { id, secret } stops waiting.

    Example

    Body

    { "email": "dana@northwind.example" }

    Answer

    { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "number": 63, "expiresAt": 1792771320000 }
  • POST/api/auth/phone/checkNo token

    Whether the phone answered: pending or denied, or approved with auth (like login), which signs in once. Counts as the second step. Stop asking at expiresAt.

    Example

    Body

    { "id": "01JC8Z6Y0QH3W8T2M5N7P9R1S3", "secret": "…", "wantToken": true }

    Answer

    { "status": "pending" }
  • GET/api/me/approver-devicesSigned in

    Your phones that approve sign-ins. DELETE /api/me/approver-devices/{id} removes one (from another device, it needs a second step verified in the last hour, like other sign-in methods). Phones are set up in the mobile app.

    Example

    Answer

    { "devices": [{ "id": "01JC8Z…", "name": "iPhone", "platform": "ios", "createdAt": 1792540320000, "lastUsedAt": 1792760520000, "current": false }] }
  • POST/api/me/mfa/launchApp only

    The app calls this each time it opens. For workspaces that ask each time Freesbe opens, it forgets this session's verification.

  • POST/api/me/totpSigned in

    Start setting up an authenticator app: a new Base32 secret and an otpauth:// uri for a QR code.

    Example

    Answer

    { "secret": "JBSWY3DPEHPK3PXP…", "uri": "otpauth://totp/Freesbe:deploy-bot%40northwind.example?secret=…" }
  • PUT/api/me/totpSigned in

    Turn the authenticator app on with a first code from it. This counts as verifying. Answers like GET /api/me.

    Example

    Body

    { "code": "123456" }
  • DELETE/api/me/totpSigned in

    Remove the authenticator app. Answers 204.

Workspaces and settings

  • GET/api/workspacesSigned in

    The workspaces you belong to, with your role in each.

  • POST/api/workspacesSigned in

    Create a workspace and become its owner, when canCreateWorkspaces is true. region is where its data is stored: eu or us (guaranteed to stay in the region), or apac or oc (Asia-Pacific or Oceania, placed close to the team without that guarantee; see Data residency in the guides). It can't be changed afterwards; without it, the region closest to where the request comes from is used. The workspace's region is null for workspaces made before regions. Answers 201.

    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:read

    One 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}Admins

    Change the name, require single sign-on (requireSso: google, microsoft, any or null), require multi-factor authentication (requireMfa, and mfaReverifyHours: null, 0, 12, 24 or 168), allow huddle and meeting recordings, give them transcripts and AI notes, transcribe every huddle (autoTranscribe), share meetings' notes outside the workspace by a private link (guestTranscripts, with guestTranscriptEmails to email meeting guests theirs automatically, guestTranscriptDays: the longest a link works, 1, 3, 7, 14 or 30, and guestTranscriptVideo to let links play the recording; turning these on asks people with an approving phone again), and delete recordings after recordingRetentionDays (30, 90, 365 or null), choose who can start and stop recordings (recordingControl: everyone, starter, the default, or hosts), and turn AI summaries and translations on or off (ai). lobbyRemindMin (0, 1, 2, 3, 5 or 10) and lobbyEmailMin (0, 5, 10, 15 or 30) set when a meeting's hosts get a louder alert and an email about a guest waiting in its lobby; 0 is off. lobbyMusic picks what guests hear while they wait in a meeting's lobby: lofi (the default), drift, sunny, classic, bossa or off. workflowBuilders is members or admins: who can make workflows. editWindowMin (null for any time, 0 for never, 5, 60, 1440 or 10080) is how many minutes people can edit a message after sending it, membersCanDelete lets members and guests delete their own messages, and adminsCanDelete lets owners and admins delete anyone's message in a channel. readReceipts is where people see who read their messages: off (the default), small (DMs and channels of up to 20 people) or all (bigger channels show a count, not names); readReceiptsOptOut lets each person turn their own off. fileRules sets which files members and guests can share: each has a preset (any, safe or only), kinds for only, and allow and block extension lists (lists and only need Corporate or Enterprise; see File types in the guides). Your own session must meet a new sign-in rule first, so you can't lock yourself out.

    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-receiptsMembers

    Turn your own read receipts on or off with show. Off works both ways: others don't see how far you read, and you don't see how far they did. Refused with 403 when admins turned readReceiptsOptOut off. Answers with the workspace, whose showReadReceipts is your choice.

    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-deletionsAdmins

    The last 100 messages owners and admins deleted for someone else, newest first: the channel, whose message it was (authorId, or authorName for an app), who deleted it and when, whether it was in a thread and how many files it had. What it said isn't kept.

    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/sessionsAdmins

    The remote control log: the last 100 requests to control someone's shared screen, newest first. kind is huddle or meeting (a support session, where sharerName is the name the customer gave). startedAt is null when the request was declined or never answered, and endReason is declined, expired, stopped, left, share_stopped or turned_off.

    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}/exportsAdmins

    The workspace's last 20 data exports, newest first: what each covers (scope public or all, includeFiles, from, to), its status (queued, running, ready, failed or expired), progress, and its zips (parts) once ready. canExportAll says whether you can include private channels and DMs, and allNeeds why not (owner or plan).

    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}/exportsAdmins

    Start an export in Slack's export format. scope all (private channels and DMs too) is for owners on plans that include it, and emails every owner. One export runs at a time (409) and a workspace can start one a day (429). from and to limit it to messages in that time.

    Example

    Body

    { "scope": "public", "includeFiles": true, "from": null }

    Answer

    { "export": { "id": "01JC5E…", "status": "queued", … } }
  • POST/api/workspaces/{w}/exports/{id}/parts/{n}/linkAdmins

    Only for the admin who started the export: a download link for zip n of a ready export, working for 15 minutes without signing in. Needs a second factor verified in the last 15 minutes: mfa_required otherwise. Each link is recorded in the audit log.

    Example

    Answer

    { "url": "https://app.freesbe.net/api/export-downloads/kQ3…", "expiresAt": 1792730700000 }
  • GET/api/workspaces/{w}/auditAdmins

    The audit log, newest first, 50 at a time: what owners and admins changed (action: settings.changed, member.role, member.deactivated, member.reactivated, invite.created, invite.emailed, invite.revoked, webhook.created, webhook.deleted, app.created, app.deleted), who did it (actorId), to whom (targetId), what changed (detail) and the IP address. When more is true, pass the last entry's id as ?before= for the next page. Entries are kept for a year.

    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-activityAdmins

    Members' security activity, newest first, 50 at a time: their sign-ins, second steps, sign-outs and changes to how they sign in, only from when they joined the workspace. The same events and filters as GET /api/me/security-activity, plus ?user= for one member.

    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}/logoAdmins

    Set the logo: raw PNG, JPEG, GIF or WebP bytes with their Content-Type, up to 2 MB.

  • DELETE/api/workspaces/{w}/logoAdmins

    Remove the logo, so the workspace's initials show.

  • GET/api/workspace-logos/{w}/{version}No token

    The logo image. logoUrl points here; the version can't be guessed, so only people who saw the URL can fetch it.

  • GET/api/workspaces/{w}/bootMembers

    Everything the app needs to start in one call: you, the workspace, members, your channels and DMs with unread counts, statuses, presence, live huddles, the event cursor seq for the WebSocket, and which features are on.

  • GET/api/workspaces/{w}/cache-keyMembers

    The key the app encrypts its local cache of the workspace with. It changes when you lose access.

  • GET/api/workspaces/{w}/allowanceAdmins

    This month's video and storage use against the workspace's limits, and why cameras and screen sharing are paused, if they are (paused, cap, allowance or budget, 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}/billingAdmins

    The 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, canceled or comped), yearly or monthly interval, seats (active members, not guests) and seatsBilled, what the workspace gets (limits and features, its plan's with any changes Freesbe made for it), the plans on sale (plans, from the pricing catalog), when the trial or period ends, and up to 24 invoices, newest first. Amounts are in cents. enforced says whether the plan's limits apply yet.

    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}Admins

    Adds an add-on to the workspace's subscription, changes how many (for add-ons bought by the unit, like call center agents), or removes it with quantity 0. Add-ons charged per person or counted by Freesbe (phone numbers) take 1 or 0. 409 without a paid plan, when it needs another add-on first, or when the card is declined. Returns the Billing page's data.

    Example

    Body

    { "quantity": 3 }

    Answer

    { "plan": "business", "addOns": [...], ... }
  • POST/api/workspaces/{w}/billing/checkoutAdmins

    Where to send an admin to pay for plan (a plan in the catalog that's bought by card, like business) yearly or monthly (interval: year or month): Stripe Checkout, or, when the workspace already pays, Stripe's page to confirm the switch. returnPath is the app page to come back to. 503 until payments are set up on the server.

    Example

    Body

    { "plan": "business", "interval": "year", "returnPath": "/northwind/c/general" }

    Answer

    { "url": "https://checkout.stripe.com/c/pay/cs_…" }
  • POST/api/workspaces/{w}/billing/portalAdmins

    A 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 token

    The published pricing catalog: every plan with its prices in cents per person (month, year), limits (null is no limit) and features, plus add-ons and metered prices. Cached for 5 minutes.

    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 token

    What [freesbe.net/status](/status/) shows: the overall status and each component's (operational, degraded, outage or maintenance), 90 days of daily uptime (null for components that aren't checked automatically) and the worst incident impact per 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:read

    Everyone 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}Members

    Anyone can set their own title. Admins change anyone's title, make someone an admin or member (a guest made a member joins the whole workspace), and set deactivated. 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 owner

    Hands the workspace to another admin or member who has signed in: they become the owner and you become an admin. Needs a second factor verified in the last 15 minutes (mfa_required). Answers 204.

    Example

    Body

    { "userId": "01JB8S…" }
  • GET/api/workspaces/{w}/closureAdmins

    Whether the workspace is being closed (closure: stage closing until closesAt, then closed and read-only until purgeAt), when the paid period ends, and the add-ons and phone system closing would end.

    Example

    Answer

    { "closure": null, "periodEnd": 1795322400000, "addOns": ["Phone system"], "phoneSystem": true }
  • POST/api/workspaces/{w}/closureIts owner

    Closes the workspace: the plan stops renewing and it goes read-only when the paid period ends (at once on Free), then everything in it is deleted 30 days later. name must be the workspace's name. Signed-in browser sessions only, with a second factor verified in the last 15 minutes (mfa_required). While closed, changes answer 403 workspace_closed; reading, exports, the billing portal and marking read still work. A workspace Freesbe's team holds answers the same way with 403 workspace_on_hold (waiting for documents) or workspace_suspended; under review only invites, phone numbers and customer inbox changes answer 403 workspace_under_review (see the account status guide).

    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 owner

    Reopens a closing or closed workspace until its deletion starts. In the paid period the plan renews again; once closed it's back on Free. Needs a second factor verified in the last 15 minutes. Answers 204.

  • GET/api/workspaces/{w}/profilesMembersusers:read

    Profile fields and everyone's values, by user id: department, location, manager (a user id), phone, pronouns and the workspace's own fields. Dates are YYYY-MM-DD. Guests get only fields not hidden from guests, for the people they share channels with.

    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:read

    The workspace's profile fields, in the order profiles show them. preset is pronouns, department, location, manager, phone or null for a field admins added; kind is text, link, date, person, options or phone.

  • PUT/api/workspaces/{w}/profile-fieldsAdmins

    Saves every field, in order: keep a field by its id, add one without an id, and leave one out to delete it with its values. Set label, options (for options fields), locked (only admins fill it in) and hiddenFromGuests. Adding your own fields and locking fields need Business, Corporate or Enterprise.

    Example

    Body

    { "fields": [ { "id": "01JC2…", "kind": "text", "locked": true }, { "kind": "options", "label": "Region", "options": ["EMEA", "Americas"] } ] }
  • PUT/api/workspaces/{w}/members/{userId}/profileMembers

    Sets someone's values by field id; null or an empty string clears one. Anyone can set their own fields that aren't locked; admins set anyone's. Answers with all of that person's values.

    Example

    Body

    { "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } }

    Answer

    { "values": { "01JC2…": "Engineering", "01JC3…": "01JB8S…" } }
  • POST/api/workspaces/{w}/profiles/bulkAdmins

    Sets 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}/invitesAdmins

    Open invite links and emailed invites, guest invites included.

  • POST/api/workspaces/{w}/invitesAdmins

    Create an invite link to share: role (member or admin), expiresInDays (default 7, at most 30), maxUses (null for unlimited). The token and url are only in this answer.

    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/emailAdmins

    Email up to 50 people their own single-use invite, with an optional role. Says who was sent one, who's already in, and who couldn't be emailed or was held back by the email limits.

    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}Admins

    Revoke an invite. Answers 204.

  • GET/api/invites/{token}No token

    What an invite link shows: the workspace, who sent it, whether it still works, and for guest invites the channels and whether someone has to let the guest in.

    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 in

    Join the workspace with an invite. A guest invite that needs approval answers 202 { "workspace": null, "pending": true } until someone in the channel lets you in.

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 members

    An invite link that makes people guests in this channel. approval (default true) means someone in the channel lets each guest in. Also expiresInDays and maxUses. Not for DMs, and not for guests.

    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 members

    Email 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 in

    Where your request to join through a guest invite stands: none, pending, approved (with the workspace) or declined.

  • POST/api/guest-requests/{id}/approveChannel members

    Let a waiting guest in. The id is the requestId of the guest_request system message posted in the channel. Answers 204, or 409 when someone already answered.

  • POST/api/guest-requests/{id}/declineChannel members

    Turn a waiting guest away. Answers 204.

Channels

Channel names are lowercase letters, numbers, hyphens and underscores, up to 80 characters; Freesbe tidies what you send (Q3 Launch! becomes q3-launch). Private channels you're not in answer 404, as if they didn't exist.

  • GET/api/workspaces/{w}/channelsMemberschannels:read

    Every 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:manage

    Create a channel: name, kind (public or private), an optional topic and the memberIds to add besides you. Answers 201, or 409 when the name is taken.

    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:read

    One 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:manage

    Change the topic (channel members, up to 250 characters) or the name (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:join

    Join a public channel.

  • POST/api/channels/{c}/leaveChannel memberschannels:join

    Leave a channel. Not DMs, and not the general channel. Answers 204.

  • POST/api/channels/{c}/archiveCreator or adminschannels:manage

    Archive a channel: it becomes read-only. Not the general channel.

  • POST/api/channels/{c}/unarchiveCreator or adminschannels:manage

    Reopen an archived channel.

  • GET/api/channels/{c}/membersChannel memberschannels:read

    The user ids of everyone in the channel.

    Example

    Answer

    { "userIds": ["01JB8Q…", "01JB8R…"] }
  • POST/api/channels/{c}/membersChannel memberschannels:manage

    Add 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:manage

    Remove someone from the channel. Answers 204.

  • PUT/api/channels/{c}/prefsChannel members

    Your own settings for the channel: muted, notify (default, all, mentions or none) and the notification sound (null for your default).

    Example

    Body

    { "muted": false, "notify": "mentions", "sound": "marimba" }
  • PUT/api/channels/{c}/sectionChannel members

    Put the conversation in one of your sidebar sections, or back under Channels or Direct messages with null. It comes back as sectionId on the channel. Answers 204. Apps can't use this.

    Example

    Body

    { "sectionId": "01JC2…" }
  • POST/api/channels/{c}/readChannel members

    Mark the channel read up to lastReadId, the newest message you've seen. It only moves forward, and your other devices clear the same unread state. Answers 204.

    Example

    Body

    { "lastReadId": "01JBC4…" }
  • POST/api/channels/{c}/unreadChannel members

    Mark the channel unread from messageId on (a message in the channel, not a thread reply): the read cursor moves back to just before it. Answers with the channel and its new unreadCount and mentionCount, and your other devices get a read.updated event with counts. Read receipts don't move back. Apps can't use this.

    Example

    Body

    { "messageId": "01JBC4…" }

    Answer

    { "channel": { "id": "01JBC2…", "lastReadId": "01JBC3…", "unread": true, "unreadCount": 3, "mentionCount": 1, … } }
  • GET/api/channels/{c}/receiptsChannel members

    Read receipts: how far the others in the conversation have read. mode is names, count (a big channel: show how many, not who) or off (the workspace doesn't show receipts here, you turned yours off, or you aren't a member), with an empty readers list. A message was seen by everyone whose lastReadId is the same or newer. People who turned their receipts off aren't listed. Changes arrive as receipt.updated events.

    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:write

    Find or open the DM with exactly these people (userIds, up to 8 besides you). An empty list opens your own notes. Guests can only message people they share a channel with.

    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:history

    A 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. hasMore says 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:write

    Post a message, or a thread reply with threadRootId. Attach up to 10 uploaded fileIds. Sending the same clientMsgId again returns the first message instead of posting twice. In a customer inbox conversation's thread the reply goes to the customer, unless note is true. You have to be in the channel. Apps can also send a card as Slack blocks or attachments, with body as its notification text. 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:history

    One 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:write

    Edit your own message's body, within the workspace's editWindowMin. Sets editedAt.

    Example

    Body

    { "body": "Release *2.4.1* is out :tada:" }
  • DELETE/api/messages/{m}Channel memberschat:write

    Delete your own message (unless the workspace turned off membersCanDelete; owners and admins always can), or, as an owner or admin, anyone's message in a channel you can see (not in DMs, and not when adminsCanDelete is off). A thread's first message stays as a placeholder while it has replies, and a message an admin deleted stays as a placeholder with deletedByAdmin: true; admin deletions are logged. Answers 204.

  • GET/api/messages/{m}/repliesChannel memberschannels:history

    A 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:write

    Forward a message you can read to a conversation you're in (channelId), with your own comment above it. The copy carries the original as an attachment with forwarded (messageId, channelId, threadRootId, userId, channelName, createdAt, fileNames); files stay in the original conversation. Forwarding a forward with no comment passes the original on.

    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 members

    Notify anyway: alert the other person in a 1:1 DM about your own message even though they paused notifications. Works for a day after sending and once a message; asking again does nothing. Answers 204. Not open to apps.

Reactions

  • PUT/api/messages/{m}/reactions/{emoji}Channel membersreactions:write

    React with one emoji: the character itself, URL-encoded (%F0%9F%8E%89 for 🎉). Doing it twice is harmless. Answers 204, or 409 when the message has all the different reactions it can take.

    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:write

    Remove your reaction. Answers 204.

Keyword alerts

Words and phrases that alert you like a mention: a message in one of your channels with one of them notifies you, counts toward the mention badge and shows in Activity. They match whole words, ignoring case. Up to 50 per workspace, 40 characters each. Changes reach your other devices as a keywords.changed event. Apps can't use these.

  • GET/api/workspaces/{w}/keywordsMembers

    Your keywords.

    Example

    Answer

    { "keywords": ["Northwind", "price list"] }
  • PUT/api/workspaces/{w}/keywordsMembers

    Replace them. Blanks and repeats are dropped. Answers the saved list.

    Example

    Body

    { "keywords": ["Northwind", "price list", "launch"] }

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:read

    Every custom emoji, by name. The url loads 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}Members

    Add one: raw PNG, JPEG, GIF or WebP bytes with their Content-Type, up to 256 KB. Guests can't. Answers 201 with { "emoji": { … } }, or 409 when the name is taken. Apps can't add emoji.

    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}Members

    Remove one you added; owners and admins can remove any. Reactions that used it stay and show its name. Answers 204.

  • GET/api/custom-emoji/{w}/{id}No token

    The picture. Its id can't be guessed, and the picture behind it never changes.

Sidebar sections

Your own groups of channels and DMs in the sidebar. Only you see them; changes reach your other devices as sidebar.sections (the whole list) and channel.section events. Up to 50, with names of up to 40 characters. Apps can't use these.

  • GET/api/workspaces/{w}/sidebar-sectionsMembers

    Your sections, in sidebar order.

    Example

    Answer

    { "sections": [ { "id": "01JC2…", "name": "Customers", "emoji": "🤝" } ] }
  • POST/api/workspaces/{w}/sidebar-sectionsMembers

    Add a section at the end. Answers 201 with { "section": { … } }.

    Example

    Body

    { "name": "Customers", "emoji": "🤝" }
  • PATCH/api/workspaces/{w}/sidebar-sections/{id}Members

    Rename it, or change its emoji (null for none).

    Example

    Body

    { "name": "Clients" }
  • PUT/api/workspaces/{w}/sidebar-sections/{id}/positionMembers

    Move it, counted from 0. Answers the whole list.

    Example

    Body

    { "position": 0 }
  • DELETE/api/workspaces/{w}/sidebar-sections/{id}Members

    Delete it. Its conversations go back under Channels and Direct messages. Answers 204.

Notification sounds

The sounds are freesbe, ding, pop, knock, bell, bubble, chord, marimba and none (notify without a sound). Set one per conversation with the channel's prefs, or per person here; your other devices follow.

  • GET/api/workspaces/{w}/soundsMembers

    The sounds you picked for messages from particular people, by user id.

    Example

    Answer

    { "sounds": { "01JB8R…": "bell" } }
  • PUT/api/workspaces/{w}/sounds/{userId}Members

    Pick 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:write

    Upload the raw bytes with their Content-Type and a Content-Length, up to 50 MB. Add &width=&height= for images when you know them. Answers 201, 403 file_blocked when the workspace's file type rules turn the file away (checked by name and by contents), or 413 when the workspace is out of storage.

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

    Answer

    { "file": { "id": "01JBC9…", "name": "q3-report.pdf", "mime": "application/pdf",
      "size": 482113, "url": "/api/files/01JBC9…" } }
  • GET/api/files/{id}Channel membersfiles:read

    Download a file from a channel you can see. Images, audio, video and plain text open inline; add ?download=1 to always download. Supports Range.

  • GET/api/channels/{c}/filesChannel membersfiles:read

    The files shared in a channel or DM you can see, threads included, newest first: what its Files tab shows. Filter with kind (images, media for video and audio, documents, other) and q (part of the name). Up to limit (default 50, at most 100) at a time; pass next as before for older ones. Files of deleted messages aren't listed. Apps need files:read.

    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:write

    Set your status in a workspace. Fields you leave out stay as they are: text (up to 100 characters), emoji, expiresAt (when the text and emoji clear), dndUntil (Do Not Disturb until then; null turns it off) and manualAway.

    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:read

    Search the messages you can see for 2 to 200 characters, newest first, optionally in one channel with &channelId=. q can hold filters: from:@name (or from:me), in:#channel, after:yyyy-mm-dd, before:yyyy-mm-dd, on:yyyy-mm-dd and has:file; after and before leave out the day itself. Answers the matching results, the channels they're in, and the filters as understood, with any names that matched nobody in unknown.

    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}/askMembers

    Ask Freesbe: an answer to a question (2 to 200 characters, filters allowed) written by AI from the messages you can see, optionally only in channelId, in language when the question doesn't make it clear. Each of the sentences lists the sources it comes from, numbered from 1 in the order of sources. status is unknown when the messages found don't answer it. search is the question as a search with the filters it understood typed out. Counts as one AI request; 403 when admins turned AI off or the month's requests are used up. Not for guests.

    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=Members

    Mentions of you (including @channel and @here), huddles in your DMs that you missed (system messages of type huddle_missed) and replies in threads you started or replied in, newest first, 50 at a time, with the channels they're in. Page back with before, 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=Members

    Your own messages and replies, newest first, 50 at a time, in conversations you're still in, with the channels they're in. Page back with before, the oldest message id you have. The app's Drafts & sent page shows these under Sent.

    Example

    Answer

    { "items": [ { "id": "01JBD2…", "channelId": "01JB8S…", "body": "Shipped the Acme Corp order", … } ],
      "channels": [ { "id": "01JB8S…", "name": "sales", … } ] }
  • GET/api/workspaces/{w}/calls?before=Members

    Your calls: huddles in your DMs that have ended, newest first, 50 at a time. direction is out when you called; answered says whether you joined (incoming) or someone else did (outgoing). Page back with before, the oldest call's startedAt. Activity → Calls in the app lists these with your phone system calls. Not for apps.

    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:read

    Your 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:write

    Add a to-do: text (the one-line title) up to 500 characters, optional notes up to 10,000 characters in message formatting (mrkdwn, line breaks kept), optional dueAt, and optional messageId to make it from a message you can see. With a messageId, text can be empty and the message's text is used; a long or multi-line message also goes whole into notes. Up to 500 open to-dos each.

    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/aiMembers

    AI help for a to-do in the editor, counted like other AI requests (not for guests, and only while the workspace has AI and allowance left). action is write (notes from the title), plan (numbered steps, plus a dueDate when the to-do says when it's needed; pass today as yyyy-mm-dd) or summarize (what the message a to-do was made from and its thread say; needs todoId). Send title and the current notes. Nothing is saved: put the answer in the to-do with PATCH.

    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:write

    Change text, notes or dueAt (null clears either), or tick it off with done: true (false reopens it).

    Example

    Body

    { "done": true }

    Answer

    { "todo": { "id": "01JBF3…", "doneAt": 1761242400000, … } }
  • DELETE/api/workspaces/{w}/todos/{id}Its ownertodos:write

    Delete one of your to-dos. Answers 204.

Scheduled messages

Messages you write now to go out later. Only their author can see or change them until they're sent. sendAt is milliseconds, at least a minute ahead and at most 120 days. At its time the message is posted as if you'd just sent it, with the usual notifications and outgoing webhooks. One that can't be sent (you left the channel, it was archived) stays listed with status: "failed" and the reason in error.

  • GET/api/workspaces/{w}/scheduled-messagesMemberschat:write

    Your 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:write

    Schedule a message in a channel, DM or thread (threadRootId) you can post in. body is written like a message's; fileIds are uploads that aren't attached yet. Up to 100 each.

    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:write

    Change the body or move sendAt. A failed one given a new sendAt is scheduled again.

    Example

    Body

    { "sendAt": 1761526800000 }

    Answer

    { "scheduled": { "id": "01JBG1…", "sendAt": 1761526800000, … } }
  • POST/api/workspaces/{w}/scheduled-messages/{id}/sendIts ownerchat:write

    Send 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:write

    Delete it so it's never sent. Answers 204.

Pins and bookmarks

Everyone who can write in a channel or DM can pin its messages (up to 100) and keep links at its top as bookmarks (up to 50). Everyone who can read it sees them. Changes reach the conversation's members as pins.changed and bookmarks.changed events with its channelId. A pin goes when its message is deleted.

  • GET/api/channels/{c}/pinsChannel memberschannels:history

    The 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:write

    Pin a message in its conversation. Pinning one that's pinned does nothing. Answers 204.

  • DELETE/api/messages/{m}/pinChannel memberspins:write

    Unpin it. Answers 204.

  • GET/api/channels/{c}/bookmarksChannel memberschannels:history

    The 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:write

    Add a link at the end. url must be a web address (https:// is added when it's missing); without a title the link's address names it.

    Example

    Body

    { "title": "Price list", "url": "https://northwind.example/prices" }

    Answer

    { "bookmark": { "id": "01JBB4…", … } }
  • PATCH/api/channels/{c}/bookmarks/{id}Channel membersbookmarks:write

    Rename 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:write

    Remove it. Answers 204.

Save for later and reminders

Your own Later list: messages you saved to come back to, each with an optional reminder (remindAt, in milliseconds). Only you see it. A reminder goes off once, as a reminder.due event to your apps and a push notification to your phone when you aren't using Freesbe; any change reaches your other devices as saved.changed. Up to 500 in progress per workspace. Apps can't use these.

  • GET/api/workspaces/{w}/saved?status=openMembers

    In progress (open): reminders that went off first, then upcoming ones soonest first, then the rest newest first. done lists the newest 100 completed. message is null for one that was deleted or is in a conversation you left.

    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 members

    Save 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 owner

    Change the reminder (remindAt, null to take it off) or mark it done. A new time sets the reminder off again.

    Example

    Body

    { "done": true }

    Answer

    { "item": { "messageId": "01JBA2…", "doneAt": 1761300000000, … } }
  • DELETE/api/messages/{m}/saveIts owner

    Take it out of Later. Answers 204, also when it wasn't there.

Drafts

What you've typed in a conversation and not sent, kept for all your devices: one per channel, DM or thread (threadRootId). Only you can see your drafts. Each change reaches your other devices as a draft.changed event. A workspace keeps up to 200 per person; the oldest make room. Apps can't use these.

  • GET/api/workspaces/{w}/draftsMembers

    Your drafts, newest first.

    Example

    Answer

    { "drafts": [ { "channelId": "01JB8S…", "threadRootId": null,
      "body": "Can you check the Northwind order before", "updatedAt": 1761238800000 } ] }
  • PUT/api/workspaces/{w}/draftsMembers

    Save the draft for a conversation you can see, replacing the one there. body is written like a message's. An empty body deletes 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 owner

    Forget a draft, for example once it's sent. Leave out threadRootId for the conversation's own. Answers 204, also when there was none.

Canvases

Each channel and DM can have one canvas, a shared document. Its content travels only over the canvas WebSocket as Yjs updates: binary frames whose first byte is 0 for a document update or 1 for a presence (awareness) update from y-protocols. On connect the server sends {"t":"ready","canEdit":true} and then the whole document as one update; send it what it's missing, then every local change. Members of the conversation can write; anyone who can read it can watch.

  • GET/api/channels/{c}/canvasMembers

    The conversation's canvas, or null before 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/wsMembers

    Opens the canvas WebSocket (send Upgrade: websocket). A member who can post starts the canvas by connecting; others get 404 until it exists. Changes from people who can only read are refused with {"t":"error","code":"read_only"}. A canvas holds up to 4 MB, and one frame up to 512 KB.

Huddles and recordings

Huddles are live calls in a channel; in a DM they ring the other people. They need the realtime WebSocket (for the connId) and Cloudflare Realtime for the audio and video, so in practice the Freesbe apps run them. Scripts can follow them through the WebSocket events. Answers 503 when huddles aren't set up on the server. When the workspace has used its video allowance, videoPaused is set: cameras and screen sharing stop, and voice keeps working.

  • POST/api/channels/{c}/huddle/joinChannel members

    Join the channel's huddle, starting one if needed. connId is from the WebSocket's hello, so a dropped connection leaves the call. Up to 20 people.

    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/leaveMembers

    Leave the huddle. Works even if you were just removed from the channel. Answers 204.

  • POST/api/workspaces/{w}/call-qualityMembers

    How a huddle sounded for you, sent by the apps as you leave: the huddleId and a report measured from the connection (packet loss, made-up audio, jitter, round trip, an estimated MOS). Nothing about what was said. Answers 204.

    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 token

    Thumbs up or down after a huddle or meeting: rating 1 or -1 and, for -1, issues (echo, noise, choppy, quiet, delay). The report's random id is the permission, for an hour after it was sent. Answers 204.

    Example

    Body

    { "rating": -1, "issues": ["choppy"] }
  • POST/api/channels/{c}/huddle/declineChannel members

    Turn down a DM call that's ringing for you. Answers 204.

  • PUT/api/huddles/{id}/stateChannel members

    Your state in the call: muted, and the published track (microphone), video and screen, or null when they stop. After reconnecting, send the new connId.

    Example

    Body

    { "muted": true }
  • ANY/api/partytracks/{huddleId}/*App only

    The media proxy to Cloudflare Realtime. Only the huddle's participants get through.

  • POST/api/huddles/{id}/recordingApp only

    Start recording the huddle, in video/webm, audio/webm, video/mp4 or audio/mp4. Everyone in the call is told. 403 when an admin turned recordings off, or when the workspace's recordingControl is hosts and you aren't the huddle's host (whoever started it), a co-host or an admin. Guests can't. A transcriptOnly start in a workspace with autoTranscribe skips that rule. With an audioMime, audio in the answer says whether to also upload an audio-only track for the transcript. With transcriptOnly, nothing is recorded: mime is an audio type, the chunks are the transcript's audio, and only the transcript and notes are kept (the notes are posted in the channel). That needs transcripts on, not recordings; 403 when they're off or the plan's transcripts for the month are used, 503 when the server can't transcribe.

    Example

    Body

    { "mime": "video/webm", "audioMime": "audio/webm;codecs=opus" }

    Answer

    { "recording": { "id": "01JBF4…", … }, "audio": true }
  • PUT/api/recordings/{id}/chunks/{seq}App only

    The recorder's app uploads chunk 0, 1, 2… as the body, up to 32 MB each.

  • PUT/api/recordings/{id}/audio/{seq}App only

    The audio-only track for the transcript, chunk by chunk like the video, when the start answered audio: true.

  • POST/api/recordings/{id}/stopChannel members

    Stops the recording; the recorder's app then finishes it. Who can depends on the workspace's recordingControl: everyone (anyone from the workspace in the call), starter (the default: the recorder, the call's hosts and co-hosts, and owners and admins) or hosts (the same, for starting too). A meeting's hosts are its organizer and invited members; a huddle's is whoever started it. The recorder can always stop their own. 403 otherwise. Answers 204.

  • PUT/api/huddles/{id}/cohostsChannel members

    A host of the huddle (whoever started it, a co-host, or an owner or admin) makes someone in it a co-host, or stops them being one. Co-hosts start and stop recordings like a host, and name more co-hosts, until the huddle ends. Guests can't be co-hosts. The huddle's cohosts lists them.

    Example

    Body

    { "userId": "01JBF2…", "cohost": true }

    Answer

    { "huddle": { "id": "01JBF4…", "cohosts": ["01JBF2…"], … } }
  • PUT/api/huddles/{id}/drawingChannel members

    A host of the huddle (whoever started it, a co-host, or an owner or admin) turns drawing on shared screens off for everyone, or back on. Drawing is on when a huddle starts. While it's off, the huddle's drawingOffBy is the host who turned it off and lines people draw aren't passed on. Each participant's drawSlot is their pen colour for the whole huddle. The lines themselves go over the realtime socket (draw frames out, huddle.draw events in) and are never stored.

    Example

    Body

    { "on": false }

    Answer

    { "huddle": { "id": "01JBF4…", "drawingOffBy": "01JBF2…", … } }
  • POST/api/recordings/{id}/finishApp only

    The recorder uploaded every chunk (durationMs). speakers is who spoke when, to name the voices in the transcript: [{ id, name, member, spans: [[startMs, endMs], …] }]. The recording is saved shortly after: a huddle's is posted in the channel as a file, a meeting's is kept with the meeting. Then the transcript and notes are written.

  • GET/api/workspaces/{w}/recordingsMembersrecordings:read

    Recordings of huddles and meetings you can see, newest first, up to 100. A meeting's recording has the meeting's attendees. state is saving from when a recording stops until its file is ready, then saved, or failed when nothing usable arrived (file is null until it's saved). transcript is queued, working, ready, failed, skipped (with a skipped reason: not_set_up, off, allowance, cap or empty) or null for recordings made before transcripts.

    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:read

    A recording with its notes (summary, decisions and action items with their owner) and its transcript as segments: who spoke, their user id when they're a member, start and end in ms, and the text.

    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}/transcribeMembers

    Try a transcript again after it failed, or after the workspace had used its plan's transcripts. Answers the recording.

  • GET/api/recordings/{id}/guest-linksMembers

    A meeting recording's links to its notes and transcript for people outside the workspace: the ones its guests were emailed, and the ones made with Share (createdBy; email is null for a link for anyone who has it). Who, until when, when and how often it was opened (opens), whether it plays the recording, and whether the email went out. canRevoke is true for the meeting's organizer, whoever recorded it and admins, and only they get url, the link itself, for links made with Share.

    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}Admins

    Revoke one outside link right away (the meeting's organizer and whoever recorded it can too). Without /{link}, revokes every outside link for the recording. Answers the list.

  • GET/api/recordings/{id}/sharingMembers

    Who the recording is shared with in the workspace (shares: a userId or a channelId, who shared it and when), its outside links when you can manage them, and what you can do: canShare (members, not guests), outsideBlock (null when you can make outside links, otherwise off, not_allowed, no_transcript, huddle or guest), maxDays and videoAllowed.

    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 guests

    Share a recording you can see with up to 20 people (userIds, posted in your DM with each) and channels or group DMs (channelIds, which you must be able to post in), with an optional comment. Each message carries an attachment with the recording's title, a link to its page, the start of its summary, and recording: its id, kind, startedAt, durationMs and whether it has a video. The people it's shared with, and everyone in a shared channel, can then see the recording. Answers the sharing, 201.

    Example

    Body

    { "userIds": ["01JBC4…"], "channelIds": ["01JBC9…"], "comment": "Notes from the Acme call" }
  • DELETE/api/recordings/{id}/shares/{share}Members

    Take a share back: whoever made it, the meeting's organizer, whoever recorded it and admins. The message stays. Answers the sharing.

  • POST/api/recordings/{id}/linksMembers

    Make a link to a meeting's notes and transcript for people outside the workspace: the meeting's organizer, whoever recorded it and admins, when the workspace has outside sharing on (guestTranscripts) and the transcript is ready. days is 1, 3, 7, 14 or 30, up to the workspace's guestTranscriptDays; video plays the recording too, when guestTranscriptVideo allows it. With an email (not a member of the workspace), the link is emailed there. Answers the link, with its url, and the sharing, 201.

    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 guests

    Whether 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 members

    Summarize a thread (threadRootId) or the channel over a span: unread (since you last read, the default), day or week, up to the newest 400 messages. language is an app language code; it defaults to yours. Each point links to its message (messageId, and threadRootId for a reply). Messages repeated by the same author (alerts, bots) are read once. Summaries are kept: asking again with nothing new answers at once and isn't counted as a request, and new messages are read into the kept summary. Send Accept: text/event-stream to get it as server-sent events while it's written: {"type":"stage","stage":"reading","count":240,"folded":180}, then {"type":"text","text":"…"} pieces, then {"type":"done","summary":{…},"cached":false} (or {"type":"error","message":"…"}). Guests get 403.

    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 members

    Translate a message into language (an app language code; yours by default). same is true when it already is in that language. Guests get 403.

    Example

    Body

    { "language": "de" }

    Answer

    { "text": "Neue Bestellung von Contoso …", "same": false }
  • POST/api/workspaces/{w}/ai/rewriteMembers, not guests

    Writing help: a better version of a draft (text, up to 4,000 characters) in a style: fix (spelling and grammar), clearer, shorter, friendlier or formal. It only rewrites the draft, in the draft's language, keeping mentions, links and code. Nothing is sent.

    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 guests

    How 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 guests

    Improve with AI for a profile photo: send image as base64 JPEG (up to 1,024 px on its longest side and 1 MB). The model checks it's a photo of a person with their face showing and answers where the face is (fractions of the picture) and gentle light and colour fixes, which the app applies itself. Anything else answers ok: false. 3 a day per person and 20 a day per workspace on Free (200 on paid plans); past that, 429. Counts as an AI request.

    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:read

    Upcoming meetings, and the last week's, that you organize, are invited to or joined. attendees lists who joined the call, in the order they came in: { userId, name }, with userId null for a guest. While a call can be going, live lists who's in it and who's waiting in the lobby. Once everyone has left a call that took place, live.callEndedAt says when, and live.rejoinUntil how long the link still lets them back in. lastCallAt is when its last call ended: the link works until 30 days after, and calls lists its calls that are over, oldest first (startedAt, endedAt, attendees). Last week's means scheduled or called in the last 7 days. chatMessages counts the messages in its chat. email says whether invites are emailed.

    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:write

    Schedule one: title, startsAt, durationMin (15, 30, 45, 60, 90, 120, 180 or 240), and optionally description, timeZone, members by userIds (those also in optionalUserIds are invited as optional) and anyone else by emails (up to 50 people), and waitingRoom (default true). Invites are emailed unless notify is false; unsent lists addresses that couldn't be. Answers 201.

    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 guests

    Start a support session: a one-hour meeting starting now, with a waiting room, whose link and code you give a customer. Members in it can ask to control the screen the customer shares from the desktop app. Needs remote control turned on for the workspace, or answers 403. Answers 201 with the meeting, like scheduling one.

    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:write

    Change a meeting. Send the whole meeting, as when scheduling it. Everyone still invited gets an updated invite, new people an invite, removed people a cancellation.

  • DELETE/api/meetings/{id}Organizer or adminsmeetings:write

    Cancel it and email the cancellation. Anyone in the call is disconnected. Answers 204.

  • POST/api/meetings/{id}/endOrganizer or adminsmeetings:write

    End the call for everyone; the link stops working. Answers 204.

  • POST/api/meetings/{id}/admitMembers, not guests

    Let someone in from the lobby (admit: true) or turn them away. peerId is from live.waiting. The organizer, invited members and admins can. Answers 204.

    Example

    Body

    { "peerId": "01JBE9…", "admit": true }
  • POST/api/meetings/{id}/new-linkOrganizer or adminsmeetings:write

    Replace 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 token

    What a meeting link shows before joining: title, time, organizer, workspace, whether it's early, open, closed or canceled, and whether guests wait in a lobby.

  • POST/api/meet/{code}/joinApp only

    Knock or join, as a member or a guest with a name (or the personal invite token from their email). Answers a token for the call's socket and media, and whether you're admitted or waiting.

  • WS/api/meet/{code}/ws?t=App only

    The meeting call's socket.

  • ANY/api/meet/{code}/media/{token}/*App only

    The meeting's media proxy to Cloudflare Realtime.

  • POST/api/meet/{code}/qualityApp only

    How the call sounded for you, sent as you leave, with the token from joining: the same report as a huddle's. Answers 204.

  • GET/api/meet/{code}/chat/{token}App only

    The call's chat so far, oldest first, for anyone let into the call (members and guests, with the token from joining). New messages also arrive on the socket as { t: "chat", message }. In the call, open a message's file at /api/meet/{code}/chat/{token}/files/{id}.

    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 only

    Send a message (body, up to 4,000 characters) to everyone in the call. Answers 201 with message.

    Example

    Body

    { "body": "The price list is at https://northwind.example/wholesale" }
  • POST/api/meet/{code}/chat/{token}/files?name=App only

    Send a file to the call's chat: the file is the body, with Content-Type and Content-Length, and width/height for pictures, text for a caption. Guests from outside follow the workspace's file rules for guests (403 file_blocked). Answers 201 with message.

  • GET/api/meetings/{id}/chatMembers

    The chat kept with a meeting, after the call: for its organizer, invited members, members who were in the call, people who can see its recording, and owners and admins. Files open at their url. 404 for anyone else.

  • POST/api/meetings/code/{code}/recordingApp only

    Start recording the meeting you're in, by its code. Workspace members can; guests can't. Everyone in the call sees who is recording. Upload and finish it like a huddle recording; it's kept with the meeting and listed under /api/workspaces/{w}/recordings. 409 when it's already being recorded.

    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 in

    Your connected calendar, if any, and the providers this server can connect to: google, microsoft and zoho. needsReconnect means the provider stopped accepting Freesbe's access.

    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 in

    Turn the automatic "In a meeting" status on or off, and change the Calendar app: app.workspaceId turns it on (messages go to your DM with yourself there) or off with null; agendaMinute is minutes after midnight in your time zone; reminderMinutes is 1, 5, 10, 15, 30 or null. Setting your status by hand takes it back from the calendar.

    Example

    Body

    { "autoStatus": false, "app": { "agendaMinute": 450, "reminderMinutes": 5 } }
  • DELETE/api/me/calendarSigned in

    Disconnect the calendar and clear a status it set. Answers 204.

  • GET/api/auth/oauth/{provider}/start?calendar=1App only

    Open in the browser to connect a calendar. Comes back to ?next= with ?calendar=connected or ?calendar_error=. Add &events=1&workspace={w} to also let the Calendar app read events and answer invitations, and turn it on in that workspace; without that permission it comes back with ?calendar_error=events.

  • GET/api/integrations/zoho/calendar/startApp only

    Open in the browser to connect Zoho Calendar, in any Zoho data center. Takes the same next, events=1 and workspace={w} as the Google and Microsoft one and comes back the same way, through Zoho's shared callback. Zoho Calendar events have canAnswer: false: Freesbe can't answer them, so rsvp answers 400.

  • GET/api/me/calendar/agenda?day=YYYY-MM-DDSigned in

    Calendar app: your events on a day (today when left out) in your time zone, without declined or canceled ones. response is organizer, pending, accepted, tentative or declined; joinUrl is a video call link when the event has one.

    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 in

    Calendar app: send today's agenda to your DM with yourself now. Answers 204.

  • POST/api/me/calendar/events/{id}/rsvpSigned in

    Calendar app: answer an invitation in your calendar; the organizer hears about it. With messageId, the Calendar message it came from shows the answer.

    Example

    Body

    { "answer": "accepted", "messageId": "01JBF7…" }

    Answer

    { "event": { "id": "7k2…", "response": "accepted", … } }
  • GET/api/workspaces/{w}/calendar-appAdmins

    Whether people can use the Calendar app in the workspace, and how many get its messages there; whether they can schedule Freesbe meetings from Outlook and Google Calendar (addins), how many connected an add-in, the Outlook add-in's manifest address for the Microsoft 365 admin center, and the Google Calendar add-on's Apps Script files.

    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-appAdmins

    Turn the Calendar app (enabled) or the calendar add-ins for Outlook and Google Calendar (addins) on or off for the workspace; send either or both. Calendar app off stops everyone's Calendar messages there; add-ins off stop everyone's add-ins from scheduling meetings in it.

    Example

    Body

    { "enabled": false, "addins": true }
  • GET/api/me/calendar-addinsSigned in

    The calendar add-ins (Outlook, Google Calendar) you connected, one per workspace and calendar, with how many meetings each scheduled, and the add-in's manifest address.

    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 only

    What the add-in's sign-in window calls: connects the Outlook add-in (client: outlook) or the Google Calendar add-on (google) to a workspace. Outlook's gets its token (fsb-cal-…), which can only schedule your meetings there; Google's gets a one-time code, good for ten minutes, that the add-on trades for its token with POST /addins/google/token (body: code). Connecting again replaces the old token. Answers 403 when the workspace's admins turned the add-ins off, and to guests.

    Example

    Body

    { "client": "outlook", "workspaceId": "01JB8Q…" }
  • DELETE/api/me/calendar-addins/{id}Signed in

    Disconnect an add-in; its token stops working at once. Meetings it scheduled stay. Answers 204.

  • POST/api/calendar-addins/meetingsApp only

    The add-in's own call, with its token as Authorization: Bearer fsb-cal-…: the Freesbe meeting for the Outlook or Google Calendar event it's in. The same event (by eventId, or the code of the link already in the invitation) keeps its meeting, brought up to date; otherwise a new one. Freesbe sends no emails for it, since the calendar sends the invitation. Returns the meeting, the link to put in the invitation, and the text for it in the calendar's locale.

    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 only

    With 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 guests

    What it takes to find a time for up to 20 userIds between from and to, at most 8 days apart. busy comes from each person's connected calendar (calendar is connected, none or error); meetings are their Freesbe meetings in any workspace, leaving out exceptMeetingId when you're moving one. workingHours (Sunday first, minutes after midnight) are in their timezone, and dndUntil is when their paused notifications resume.

    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-linksMembers

    Your booking links in the workspace, the baseUrl they start with (the workspace's customDomain when it has one), whether your calendar is checked, and whether bookings are emailed.

  • POST/api/workspaces/{w}/booking-linksMembers, not guests

    Make a link: slug (3 to 40 lowercase letters, digits and dashes), title, durations, weekly hours (Sunday first, as minutes after midnight) and timeZone, and optionally description, minNoticeMin, maxDaysAhead, bufferMin, showPhoto and enabled. 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 owner

    Change one of your links. Send the whole link, as when making it; enabled: false pauses it.

  • DELETE/api/booking-links/{id}Its owner

    Delete one of your links. Meetings already booked stay. Answers 204.

  • GET/api/booking-slugs/{slug}Signed in

    Whether a link name is free.

    Example

    Answer

    { "slug": "dana-intro", "available": true }
  • GET/api/book/{slug}No token

    What 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 token

    The free start times for one length, between from and to (at most 42 days apart).

    Example

    Answer

    { "durationMin": 30, "slots": [1761300000000, 1761301800000, 1761303600000] }
  • POST/api/book/{slug}No token

    Book a time: durationMin, startsAt, name, email, and optionally a note and the booker's timeZone. Answers the booker's own meeting link, whether an invite was emailed, and manageUrl, 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 token

    The booker's own booking, by the secret t from their manageUrl (the part after the dot in manage=<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 token

    The free times the booking can move to, at its own length. Its current time counts as free.

  • POST/api/book/manage/{meetingId}/rescheduleNo token

    Move the booking to a free time: token (the secret) and startsAt, and optionally the booker's timeZone. Both sides get an updated invite. Answers like booking does.

    Example

    Body

    { "token": "…", "startsAt": 1761309000000, "timeZone": "America/Chicago" }
  • POST/api/book/manage/{meetingId}/cancelNo token

    Cancel the booking: token, and optionally a reason for the host. Both sides get a cancellation. Answers 204.

    Example

    Body

    { "token": "…", "reason": "Our budget review moved to November" }
  • GET/api/book/{slug}/photoNo token

    The 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-domainAdmins

    The workspace's domain, if any, with its status (pending, active or failed), what it's still waiting for, and the DNS records to add. Also the workspace's plan and whether it included a custom domain.

    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-domainAdmins

    Connect a subdomain you own (hostname; a bare domain like northwind.example can't be used). 403 when the plan doesn't include it, 409 when the workspace already has one or another workspace uses it.

    Example

    Body

    { "hostname": "meetme.northwind.example" }
  • POST/api/workspaces/{w}/custom-domain/checkAdmins

    Check again whether the DNS record and certificate are ready.

  • DELETE/api/workspaces/{w}/custom-domainAdmins

    Disconnect the domain. Links go back to the Freesbe address. Answers 204.

Punch clock

When an admin turns the punch clock on, the people it covers clock in and out. Times are milliseconds; every change after the fact needs a reason and is kept in the entry's history.

  • GET/api/workspaces/{w}/time/clockMembers

    Your clock: the settings, whether the clock covers you, and whether you're working, on a break or off.

    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/clockMembers

    action: in, break, resume or out, with an optional note and your current timezone.

    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/entriesMembers

    Entries between from and to (milliseconds, at most 93 days): your own, or for admins everyone's, or one person's with userId.

  • POST/api/workspaces/{w}/time/entriesMembers

    Add time after the fact: kind (work or break), startedAt, endedAt, note and a reason. Admins can add it for someone with userId.

    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}Members

    Change 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}Members

    Delete an entry, with a reason in the body. Answers 204.

  • GET/api/workspaces/{w}/time/entries/{id}/historyMembers

    Every change to an entry, oldest first: who, when, before, after and why.

  • GET/api/workspaces/{w}/time/boardAdmins

    Who is on the clock right now.

  • GET/api/workspaces/{w}/time/exportAdmins

    Timesheets as CSV: from and to dates (YYYY-MM-DD), format shifts or daily, optionally one userId. Days are cut in each person's time zone.

  • GET/api/workspaces/{w}/time/settingsAdmins

    The punch clock settings and who it covers (trackedUserIds).

  • PATCH/api/workspaces/{w}/time/settingsAdmins

    Turn it on (enabled), cover everyone (trackEveryone) or some people (trackedUserIds), and set weekStart, timezone, daily and weekly overtime, the longest shift and paidBreaks.

    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}/appsAdmins

    The workspace's apps, by name. Tokens aren't included, only their last four characters (tokenHint).

  • POST/api/workspaces/{w}/appsAdmins

    Make an app from manifest (parse YAML to JSON first). It gets a bot user, in no channels yet, a bot token and a signingSecret; the token is only in this answer. Slack scope and event names are mapped; ones Freesbe doesn't have are listed in unsupportedScopes and unsupportedEvents, and events whose scope is missing in eventsMissingScope. With settings.event_subscriptions.request_url, the URL must answer the challenge 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}Admins

    One app.

  • PATCH/api/workspaces/{w}/apps/{id}Admins

    Replace the manifest (the name, colour, scopes, events and bot name follow it; a new request URL must answer the challenge), or turn the app off and on with disabled. A turned-off app's token answers 401 and it gets no events.

    Example

    Body

    { "disabled": true }
  • POST/api/workspaces/{w}/apps/{id}/tokenAdmins

    A new bot token, only in this answer. The old one stops working at once.

  • DELETE/api/workspaces/{w}/apps/{id}Admins

    Delete the app and its token. Its bot stays as a deactivated member, so its messages keep its name. Answers 204.

  • GET/api/workspaces/{w}/apps/{id}/secretAdmins

    The signing secret the app's event deliveries are signed with.

    Example

    Answer

    { "signingSecret": "8f14e45fceea167a5a36dedd4bea2543" }
  • GET/api/workspaces/{w}/apps/{id}/deliveriesAdmins

    The last 50 event deliveries to the app's request URL, newest first, kept for 14 days. Same shape as a webhook's deliveries.

  • GET/api/apps/meApp token

    Which 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-commandsMembers

    The 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 members

    Run a slash command in the channel (or the thread in threadRootId): Freesbe sends it to the app that has it. reply is the app's answer when only you see it, or null. 404 when no app has the command; 502 when the app can't be reached, answers with an error or takes longer than 3 seconds.

    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 members

    Send the message to one of an app's message actions, as Slack's message_action. Answers 204; the app answers later through its response_url, if at all.

    Example

    Body

    { "appId": "01JC2A…", "callbackId": "create_order" }
  • POST/api/messages/{m}/app-buttonsChannel members

    Press a button on an app's message: Freesbe sends the app Slack's block_actions. clientTrigger is any random string; a dialog the app opens in answer comes over the socket with it. Answers 204; 404 when the button or app is gone.

    Example

    Body

    { "actionId": "pack_order", "blockId": "order_4471", "clientTrigger": "k3v9q2" }
  • POST/api/channels/{c}/app-buttonsChannel members

    Press a button on an app's answer only you see. Those aren't kept, so send the button's text and value with it. Answers 204.

    Example

    Body

    { "appId": "01JC2A…", "ephemeralId": "01JD…", "actionId": "restock", "text": "Restock",
      "value": "assam" }
  • POST/api/app-views/{id}/submitMembers

    Send an app's dialog. values holds each input's answer by its block id: a string, or a list for checkboxes. The app hears it as view_submission; action is what it answered: close, clear (close every dialog), errors (by block id), or update and push with the next view.

    Example

    Body

    { "values": { "qty": "24", "warehouse": "singapore" } }

    Answer

    { "action": "errors", "errors": { "qty": "Only 12 cases left" } }
  • POST/api/app-views/{id}/buttonsMembers

    Press 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}/closeMembers

    Close an app's dialog, or every dialog of its stack with cleared. An app that set notify_on_close hears view_closed. Answers 204.

    Example

    Body

    { "cleared": true }
  • POST/api/apps/viewsApp token

    Open a dialog, like Slack's views.open: triggerId is the trigger_id of a command, action or button press (it works for 30 seconds), and view is a Block Kit modal. Needs no scope.

    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 token

    Add a dialog on top of an open one (views.push), with a trigger_id from 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 token

    Change an open dialog (views.update). With hash, 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-hooksAdmins

    Every app's REST hooks, newest first, with the app's name and the channel's: what **Workspace settings → Integrations & apps → Integrations → Automation** lists.

  • DELETE/api/workspaces/{w}/app-hooks/{id}Admins

    Stop an app's REST hook. The flow it belongs to stops hearing about the event.

  • GET/api/apps/hooksApp token

    The app's REST hooks: the events it asked to be sent to a URL, as Zapier, Make, n8n and Zoho Flow do. Needs no scope.

    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 token

    Send 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 has channels:join. Each delivery has the event's type and data as an outgoing webhook's do, plus user: who did it (id, name, isBot, and email when the app has users:read). It's signed with the app's signing secret. Answer 410 and the hook is deleted. 100 hooks per app.

    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 token

    Stop a REST hook. Answers 204.

  • POST/hooks/commands/{token}No token

    An app's response_url: answer a slash command, message action or button press later, up to 30 minutes after. The body is a message like an incoming webhook's (text, blocks, attachments), with "response_type": "in_channel" to post it in the channel; otherwise only the person sees it. After a button press, "replace_original": true changes the message or answer the button was on, and "delete_original": true removes it. Answers { "ok": true }.

    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-inAdmins

    The app's Sign in with Freesbe settings, or client: null while it's off. The client id is the app's id; the secret shows only its last four characters.

    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-inAdmins

    Turns it on or changes it. Return addresses must use https (http only for localhost); public apps may also use their own reverse-domain scheme and must use PKCE. The first time, the answer includes the client secret, shown once. DELETE turns 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/secretAdmins

    A new client secret, in secret; the old one stops working at once.

  • GET/api/me/connected-appsSigned in

    Apps you've allowed to sign you in, most recently used first. DELETE /api/me/connected-apps/{appId} takes it back; the app asks again next time.

    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 in

    The connect page: the agent asking, the permissions it wants, and the person's workspaces with whether each one lets it in (allowed, approval when an admin has to approve it first, requested while they do, off, or blocked) and which permissions it wouldn't get there (withheld). The id comes from the browser's redirect to /connect/{id}.

    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 in

    Allows the agent in a workspace, or not. The answer's redirect sends the browser back to the agent with a one-time code (outcome: "connected"), or with an error once admins have been asked to approve it ("requested") or when the person said no ("denied"). Guests can't connect agents.

    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 in

    The agents you connected, most recently used first. status is active, waiting for an admin's approval, or approved once they gave it (connect again from the agent). DELETE /api/me/agents/{grantId} disconnects one at once.

    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}/agentsAdmins

    The workspace's agent settings, every agent anyone connected (with how many people, who is waiting for approval and their notes, and the permission ceiling), and the MCP server address to give people.

    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}/agentsAdmins

    Changes who can connect agents (off, approved or open) and whether they may read private channels and direct messages. Answers like GET.

    Example

    Body

    { "policy": "approved", "allowPrivate": false }
  • PATCH/api/workspaces/{w}/agents/{id}Admins

    Approves, allows or blocks an agent (status), or sets the most it may do here (scopeCeiling, a list of permissions, or null for any). Both apply to existing connections at once. DELETE removes the agent and all its connections from the workspace.

    Example

    Body

    { "status": "allowed", "scopeCeiling": ["channels:read", "channels:history", "chat:write"] }
  • GET/api/workspaces/{w}/agents/activityAdmins

    What agents did here, newest first, 50 at a time: connections, approvals, blocks, removals and every tool call. Pass next back as ?before= for older entries.

    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}/answerMembers

    Answers a question an agent asked you (the agent_question block on its message). value fits the question's answer.type: text, a day ("2026-11-06"), an ETA ({ "date", "time" }, time optional), the choice or list of choices, true or false, or a number. Only the person it's with can answer, once; 409 when it's no longer waiting.

    Example

    Body

    { "value": { "date": "2026-11-06", "time": "15:00" }, "note": "Waiting on the carrier pickup" }
  • POST/api/agent-questions/{id}/passMembers

    Says the question isn't yours to answer. It goes to suggestUserId if given, or the next person the agent named, or back to the agent as declined.

    Example

    Body

    { "suggestUserId": "01JC2U…" }
  • POST/api/agent-questions/{id}/laterMembers

    Can't say yet: you're asked again at 9:00 on your next working day.

Webhooks

Admins see the webhooks for public channels and the private channels they're in. See incoming and outgoing webhooks above for the formats.

  • GET/api/workspaces/{w}/webhooksAdmins

    Every webhook you can see, newest first.

  • POST/api/workspaces/{w}/webhooksAdmins

    Create an incoming webhook (kind, name, channelId) or an outgoing one (kind, name, url, events, and channelId for one channel). The incoming hookUrl or outgoing secret is only in this answer. Answers 201.

    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}Admins

    Change name, url, events, channelId, or turn it off and on with disabled.

    Example

    Body

    { "disabled": false }
  • DELETE/api/workspaces/{w}/webhooks/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/webhooks/{id}/rotateAdmins

    A new URL (incoming) or signing secret (outgoing). The old one stops working at once.

  • POST/api/workspaces/{w}/webhooks/{id}/testAdmins

    Send a webhook.test event (outgoing). Answers 204.

  • GET/api/workspaces/{w}/webhooks/{id}/deliveriesAdmins

    The last 14 days of deliveries, newest first (outgoing): event, status, attempts, last HTTP code and error.

  • POST/hooks/custom/{id}/{token}No token

    Post to an incoming webhook. {id} is the webhook's ID; the token is the secret. Older /hooks/{token} URLs keep working. Answers ok as plain text.

Payment alerts

Mercury, Stripe and Airwallex activity posted into a channel. See the [payment alerts guide](/docs/payment-alerts/). Admins see the alerts for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/payment-alertsAdmins

    Every payment alert you can see, newest first: provider, channelId, kinds, minAmount, whether it's ready (its signing secret is saved) and lastEventAt.

  • POST/api/workspaces/{w}/payment-alertsAdmins

    Connect an account: provider (mercury, stripe or airwallex), channelId, kinds (money_in, money_out, balance, other) and an optional minAmount. Mercury also takes apiToken, used once to register the webhook and not kept. Stripe and Airwallex answer with the hookUrl to paste into their dashboard, only in this answer. Answers 201.

    Example

    Body

    { "provider": "stripe", "channelId": "01J…", "kinds": ["money_in", "other"], "minAmount": 100 }
  • PATCH/api/workspaces/{w}/payment-alerts/{id}Admins

    Change channelId, kinds or minAmount, save the provider's signing secret (Stripe and Airwallex), or turn it off and on with disabled.

    Example

    Body

    { "secret": "whsec_…" }
  • DELETE/api/workspaces/{w}/payment-alerts/{id}Admins

    Disconnect it. Answers 204.

  • POST/api/workspaces/{w}/payment-alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/finance/{provider}/{token}No token

    Where the provider (mercury, stripe or airwallex) delivers events, signed with Mercury-Signature, Stripe-Signature, or Airwallex's x-timestamp and x-signature. Answers 200 to signed events, posted or filtered out, 401 to a bad or old signature and 404 when the alert is off or gone.

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}/krakenAdmins

    Every Kraken account you can see, newest first: channelId, kinds, minUsd, keyHint (the key's last four characters), balanceRules, summaryHour and summaryTimeZone, checkedAt, lastEventAt, and error when Kraken refused the key.

  • POST/api/workspaces/{w}/krakenAdmins

    Connect an account: apiKey and privateKey (a key with Query Funds and Query Ledger Entries), channelId, kinds (deposits, withdrawals, trades, other), and optionally minUsd, up to 10 balanceRules (asset, op below or above, amount), summaryHour (0 to 23) and summaryTimeZone. Freesbe checks the key reads the account first and answers 400 when Kraken refuses it, or when the key is already connected. The private key is never sent back. Answers 201.

    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}Admins

    Change any of the above, replace the key (apiKey and privateKey together), or turn it off and on with disabled. Turned back on, it carries on from then.

    Example

    Body

    { "summaryHour": 9, "summaryTimeZone": "America/New_York" }
  • DELETE/api/workspaces/{w}/kraken/{id}Admins

    Disconnect it and forget the key. Answers 204.

  • POST/api/workspaces/{w}/kraken/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

Crypto wallets

Money in and out of public addresses on Bitcoin, Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Solana and Tron, posted into a channel. See the [Crypto wallets guide](/docs/crypto-wallets/). Admins see the wallets that post to public channels and the private channels they're in.

  • GET/api/workspaces/{w}/crypto-walletsAdmins

    Every wallet you can see, newest first: network, address, name, channelId, directions, minUsd, balanceRules, checkedAt, lastEventAt, and error when the network's data couldn't be read. Also networks, the ones this server can watch, and the workspace's daily summary (channelId, hour, timeZone) or null.

  • POST/api/workspaces/{w}/crypto-walletsAdmins

    Watch a wallet: network (bitcoin, ethereum, base, arbitrum, optimism, polygon, bnb, avalanche, solana, tron), its public address, a name, channelId, and optionally directions (in, out), minUsd and up to 5 balanceRules (asset: the network's coin, USDC or USDT; op below or above; amount). Answers 400 when the address isn't one for the network or is already watched. Answers 201.

    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}Admins

    Change the name, channelId, directions, minUsd or balanceRules, or turn it off and on with disabled. Turned back on, it carries on from then. The network and address stay as they are.

    Example

    Body

    { "directions": ["in"], "minUsd": 100 }
  • DELETE/api/workspaces/{w}/crypto-wallets/{id}Admins

    Stop watching the wallet. Answers 204.

  • POST/api/workspaces/{w}/crypto-wallets/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • PUT/api/workspaces/{w}/crypto-wallets/summaryAdmins

    Set the daily card with every wallet's balances: summary with channelId, hour (0 to 23) and timeZone, or null to turn it off. Answers 204.

    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}/hubspotAdmins

    Whether the server can connect HubSpot (available), the connection (portalId, hubDomain, botUserId, unfurls, brokenAt when HubSpot stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/hubspot/connectAdmins

    The url of HubSpot's page that asks to allow Freesbe. Send the browser there; HubSpot sends it back to Freesbe, which finishes connecting and opens the app with ?hubspot=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/hubspotAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/hubspotAdmins

    Disconnect: Freesbe forgets HubSpot's access and the app's command and action go away. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/hubspot/alertsAdmins

    Add an alert: channelId, kinds (deal_created, deal_stage, deal_won, deal_lost, contact_created) and an optional minAmount for deals. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 5000 }
  • PATCH/api/workspaces/{w}/hubspot/alerts/{id}Admins

    Change channelId, kinds or minAmount, or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/hubspot/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/hubspot/alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/crm/hubspotNo token

    Where HubSpot delivers webhook events for every connected account, signed with X-HubSpot-Signature-v3 and X-HubSpot-Request-Timestamp. Answers 200 to signed events and posts them afterwards, and 401 to a bad or old signature.

GitHub

The GitHub app: Freesbe's GitHub App installed on an organization, the repositories channels subscribe to, and link previews. See the [GitHub guide](/docs/github/). /github and **Create GitHub issue** run like any app's slash command and message action. Admins see the subscriptions for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/githubAdmins

    Whether the server can install GitHub (available), the connection (installationId, account, accountType, repositorySelection, botUserId, unfurls, brokenAt when the app was removed or suspended in GitHub) or null, and the subscriptions.

  • POST/api/workspaces/{w}/github/connectAdmins

    The url of GitHub's page that installs the app. Send the browser there; GitHub sends it back to Freesbe, which checks the installation is one this person can reach and opens the app with ?github=connected. Only the admin who started it can finish it, within 30 minutes.

  • PATCH/api/workspaces/{w}/githubAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/githubAdmins

    Disconnect: Freesbe forgets the installation and its subscriptions, and the app's command and action go away. The app stays installed in GitHub until someone removes it there. Answers 204.

  • GET/api/workspaces/{w}/github/reposAdmins

    The repos the installation can see (fullName, private), up to 300.

  • POST/api/workspaces/{w}/github/subscriptionsAdmins

    Subscribe a channel to a repository: channelId, repo (owner/repo), and optional features (pulls, reviews, issues, comments, commits, releases, deployments, workflows; pull requests, issues, releases and deployments when left out), labels and branch. A channel already subscribed to the repository gets the features added. Answers 201.

    Example

    Body

    { "channelId": "01J…", "repo": "northwind-traders/storefront", "features": ["pulls", "releases"] }
  • PATCH/api/workspaces/{w}/github/subscriptions/{id}Admins

    Change channelId, features, labels or branch, or turn it off and on with disabled.

    Example

    Body

    { "labels": ["bug"] }
  • DELETE/api/workspaces/{w}/github/subscriptions/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/github/subscriptions/{id}/testAdmins

    Post a sample pull request card into its channel. Answers 204.

  • POST/hooks/dev-tools/githubNo token

    Where GitHub delivers webhook events for every installation, signed with X-Hub-Signature-256. Answers 200 to signed deliveries and posts them afterwards (each X-GitHub-Delivery once), and 401 to a bad signature.

Jira

The Jira app: a connected Jira Cloud site, the projects channels subscribe to, and link previews. See the [Jira guide](/docs/jira/). /jira and **Create Jira issue** run like any app's slash command and message action. Admins see the subscriptions for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/jiraAdmins

    Whether the server can connect Jira (available), the connection (siteUrl, siteName, jiraUser who Freesbe acts as, botUserId, unfurls, brokenAt when Atlassian stopped accepting its access) or null, and the subscriptions.

  • POST/api/workspaces/{w}/jira/connectAdmins

    The url of Atlassian's page that asks to allow Freesbe. Send the browser there; Atlassian sends it back to Freesbe, which finishes connecting and opens the app with ?jira=connected. Only the admin who started it can finish it, within 30 minutes.

  • PATCH/api/workspaces/{w}/jiraAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/jiraAdmins

    Disconnect: Freesbe removes its webhook from the site and forgets the connection and its subscriptions, and the app's command and action go away. Answers 204.

  • GET/api/workspaces/{w}/jira/projectsAdmins

    The projects the connected account can browse (key, name), up to 300.

  • POST/api/workspaces/{w}/jira/subscriptionsAdmins

    Subscribe a channel to a project: channelId, project (its key), and optional features (created, status, assigned, comments; all but comments when left out) and labels. A channel already subscribed to the project gets the features added. Answers 201.

    Example

    Body

    { "channelId": "01J…", "project": "OPS", "features": ["created", "comments"] }
  • PATCH/api/workspaces/{w}/jira/subscriptions/{id}Admins

    Change channelId, features or labels, or turn it off and on with disabled.

    Example

    Body

    { "labels": ["customer"] }
  • DELETE/api/workspaces/{w}/jira/subscriptions/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/jira/subscriptions/{id}/testAdmins

    Post a sample issue card into its channel. Answers 204.

  • POST/hooks/dev-tools/jira/{token}No token

    Where the site's webhook delivers issue and comment events, to the connection's own secret address, which Freesbe registers with Jira itself. Answers 200 and posts afterwards (each X-Atlassian-Webhook-Identifier once), 404 to an unknown address, and 401 to a bad signature.

GitLab

The GitLab app: a connected GitLab.com account or self-managed server, the projects channels subscribe to, and link previews. See the [GitLab guide](/docs/gitlab/). /gitlab and **Create GitLab issue** run like any app's slash command and message action. Admins see the subscriptions for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/gitlabAdmins

    Whether the server can connect GitLab.com (available), the connection (instanceUrl, selfManaged, gitlabUser who Freesbe acts as, botUserId, unfurls, brokenAt when GitLab stopped accepting its access) or null, the subscriptions, the callbackUrl a self-managed server's application needs as its redirect URI, and for a private server the webhook (url, secret) its admin adds to each project.

  • POST/api/workspaces/{w}/gitlab/connectAdmins

    The url of GitLab's page that asks to allow Freesbe. Leave the body empty for GitLab.com; for a self-managed server send its https instanceUrl and the clientId and clientSecret of the application made on it. For a private server Freesbe can't reach, send instanceUrl and private: true: it connects straight away and url is null. Otherwise send the browser there; GitLab sends it back to Freesbe, which finishes connecting and opens the app with ?gitlab=connected. Only the admin who started it can finish it, within 30 minutes.

    Example

    Body

    { "instanceUrl": "https://gitlab.northwind.example", "clientId": "…", "clientSecret": "…" }
  • PATCH/api/workspaces/{w}/gitlabAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/gitlabAdmins

    Disconnect: Freesbe takes its webhooks off the projects and forgets the connection and its subscriptions, and the app's command and action go away. Answers 204.

  • GET/api/workspaces/{w}/gitlab/projectsAdmins

    The projects the connected account is a member of (path, name, private), most active first, up to 300.

  • POST/api/workspaces/{w}/gitlab/subscriptionsAdmins

    Subscribe a channel to a project: channelId, project (its path), and optional features (merge_requests, issues, comments, pushes, tags, releases, deployments, pipelines; merge requests, issues, releases and deployments when left out), labels and branch. Freesbe adds its webhook to the project; hookError says why when GitLab refused (usually a missing Maintainer role). A channel already subscribed to the project gets the features added. Answers 201.

    Example

    Body

    { "channelId": "01J…", "project": "northwind/storefront", "features": ["merge_requests", "pipelines"] }
  • PATCH/api/workspaces/{w}/gitlab/subscriptions/{id}Admins

    Change channelId, features, labels or branch, or turn it off and on with disabled. Turning it on tries the project's webhook again.

    Example

    Body

    { "labels": ["bug"] }
  • DELETE/api/workspaces/{w}/gitlab/subscriptions/{id}Admins

    Delete it. The project's webhook goes when no channel follows the project any more. Answers 204.

  • POST/api/workspaces/{w}/gitlab/subscriptions/{id}/testAdmins

    Post a sample merge request card into its channel. Answers 204.

  • POST/hooks/dev-tools/gitlab/{token}No token

    Where the projects' webhooks deliver, to the connection's own secret address, with its secret in X-Gitlab-Token. Answers 200 and posts afterwards (each X-Gitlab-Event-UUID once), 404 to an unknown address, and 401 to a wrong token.

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}/salesforceAdmins

    Whether the server can connect Salesforce (available), whether the workspace's plan includes it (included), the connection (orgId, orgName, instanceUrl, sandbox, botUserId, unfurls, brokenAt when Salesforce stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/salesforce/connectAdmins

    The url of Salesforce's page that asks to allow Freesbe, at test.salesforce.com with sandbox: true. Send the browser there; Salesforce sends it back to Freesbe, which finishes connecting and opens the app with ?salesforce=connected. Only the admin who started it can finish it, within 15 minutes. 403 when the plan doesn't include Salesforce.

    Example

    Body

    { "sandbox": false }
  • PATCH/api/workspaces/{w}/salesforceAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/salesforceAdmins

    Disconnect: Freesbe forgets Salesforce's access and the app's command and action go away. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/salesforce/alertsAdmins

    Add an alert: channelId, kinds (opportunity_created, opportunity_stage, opportunity_won, opportunity_lost, lead_created, case_created) and an optional minAmount for opportunities. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["opportunity_created", "opportunity_won"], "minAmount": 10000 }
  • PATCH/api/workspaces/{w}/salesforce/alerts/{id}Admins

    Change channelId, kinds or minAmount, or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/salesforce/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/salesforce/alerts/{id}/testAdmins

    Post 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/sourcesAdmins

    Where customers write in from: each source's id, kind (webchat, email, sms or whatsapp), name, channelId, enabled, testMode, for a web chat its webchat settings with the public key, the snippet to paste on the site and a pageUrl with the chat on its own, for an email inbox its email settings with the address to forward to, for SMS its sms.number, and for WhatsApp its whatsapp settings: the number, whether it's connected to a WhatsApp Business account, and that account's verifiedName. available lists the kinds this server can add now, and live the kinds that can leave test mode (sms once texting customers is on, whatsapp once WhatsApp is set up on the server). whatsappIncluded says whether the workspace's plan includes WhatsApp (Business and up).

  • POST/api/workspaces/{w}/inbox/sourcesAdmins

    Add a web chat, an email inbox, an SMS inbox or a WhatsApp inbox: a kind, a name, the channelId new conversations go to (not a DM), and optional settings. A web chat takes webchat: title, greeting, color (#rrggbb), askEmail, and sites, the websites that may show it (any when empty). An email inbox takes email: replyTo, the team's own address replies ask customers to answer, and fromName, the name replies come from. An SMS inbox takes sms: the number customers text, with its country code; it starts in test mode. A WhatsApp inbox takes whatsapp: the WhatsApp Business number, with its country code; it starts in test mode too, and needs a plan that includes WhatsApp (403 forbidden otherwise). 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}Admins

    Change its name, channelId, webchat, email, sms or whatsapp settings, or turn it off and on with enabled. A web chat that's off disappears from the site; an email inbox that's off bounces mail. For SMS, testMode: false takes it live, texting customers for real (only when live lists sms, and only on one of the workspace's own phone numbers); testMode: true takes it back. For WhatsApp, testMode: false needs the inbox connected first (connect WhatsApp), and a connected inbox keeps its number.

    Example

    Body

    { "enabled": false }
  • POST/api/workspaces/{w}/inbox/sources/{id}/testAdmins

    Only for an inbox in test mode: a made-up customer's text, from the number in from (a made-up one when left out), with up to 10 of your uploaded fileIds as photos or files they sent. It lands like a real one, and replies to it are kept with test: true instead of being sent. Answers the conversation and its deliveries.

    Example

    Body

    { "text": "Are you open on Saturday?" }
  • POST/api/workspaces/{w}/inbox/sources/{id}/whatsapp/connectAdmins

    Starts connecting a WhatsApp inbox to the business's WhatsApp Business account: answers the url of Meta's sign-up to send the admin to. Meta sends them back to Freesbe, which finds the inbox's number in the account they picked and takes the inbox live. Only when live lists whatsapp; the link works for 15 minutes, for the admin who started it.

  • GET/api/workspaces/{w}/inbox/sources/{id}/whatsapp/templatesMembers, not guests

    A WhatsApp inbox's message templates, from its WhatsApp Business account, approved ones first: each one's id, name, language, category (UTILITY, MARKETING or AUTHENTICATION), status (approved, pending, rejected, paused or disabled), header, body with blanks {{1}}, {{2}}…, footer, how many params it takes, whether Freesbe can fill it (supported: not with a picture header, header blanks or a link button that changes) and Meta's rejectedReason. Until the inbox is connected, sample is true and three made-up templates stand in. manageUrl opens the account's templates in WhatsApp Manager.

    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/templatesAdmins

    Sends a new template to Meta for review: a lowercase name with underscores, a language code like en_US, a category (UTILITY or MARKETING), the body with blanks {{1}}, {{2}}… numbered in order, an example for each blank in examples, and an optional footer. The inbox has to be connected. Answers 201 with the template, pending until Meta approves it.

    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 guests

    Write to a customer first, with an approved template (name, language and the params that fill its blanks), to their WhatsApp number to with its country code, and an optional name. It goes into their open conversation from this inbox, or starts a thread in the inbox's channel, and shows there as your message. You have to be able to post in that channel. Answers 201 with the conversation and its deliveries; in test mode it's kept with test: true.

    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}Admins

    Delete it, with its conversations. Their threads stay in the channel. Answers 204.

  • GET/api/workspaces/{w}/inboxMembers, not guests

    The sources you can see, a page of conversations (newest change first) and the counts of open ones: all, yours and nobody's. Pick a list with ?view=open|mine|unassigned|closed, one source with ?source=, 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 guests

    One conversation, and the deliveries of the replies that went to the customer (messageId, status, error). Thread replies without one were notes.

  • PATCH/api/inbox/conversations/{id}Members, not guests

    Close or reopen it with status, or give it to someone in its channel with assigneeId (null: nobody). The inbox bot says so in the thread and mentions the new owner.

    Example

    Body

    { "assigneeId": "01JB8R…" }
  • POST/api/inbox/conversations/{id}/suggestMembers, not guests

    An answer to the customer the AI drafts from the conversation, its notes and the saved replies, as text. Nothing is sent. Counts toward the workspace's AI allowance.

  • POST/api/inbox/conversations/{id}/whatsapp/templateMembers, not guests

    Sends an approved WhatsApp template into the conversation (its name, language and the params that fill its blanks): how to reach a customer more than 24 hours after their last message, when WhatsApp doesn't take a free-form reply. It shows in the thread as your message. Answers the conversation and its deliveries.

    Example

    Body

    { "name": "follow_up", "language": "en_US", "params": ["Dana", "your order NW-48231"] }
  • GET/api/workspaces/{w}/inbox/repliesMembers, not guests

    The team's saved replies: id, title, body.

  • POST/api/workspaces/{w}/inbox/repliesMembers, not guests

    Save a reply with a title and a body. 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 guests

    Change its title and body.

  • DELETE/api/workspaces/{w}/inbox/replies/{id}Members, not guests

    Delete it: admins, and whoever saved it. Answers 204.

  • GET/api/chat/{key}No token

    The web chat's title, greeting, color and whether it asks for an email (askEmail). 404 when it's off or gone.

  • POST/api/chat/{key}/startNo token

    A visitor's first message (text, with name and email when the chat asks): starts a conversation and returns the visitor's secret as visitorToken, to send as Authorization: Bearer from then on, and their messages. Answers 201.

  • GET/api/chat/{key}/messagesNo token

    With the visitor's token: their side of the latest conversation (from is visitor or team; notes never show) after ?after=<id>, and whether the team closed it.

  • POST/api/chat/{key}/messagesNo token

    With 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}/pipedriveAdmins

    Whether the server can connect Pipedrive (available), the connection (companyId, companyName, companyUrl, botUserId, receiving when Pipedrive sends changes to Freesbe, unfurls, lastEventAt, brokenAt when Pipedrive stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/pipedrive/connectAdmins

    The url of Pipedrive's page that asks to allow Freesbe. Send the browser there; Pipedrive sends it back to Freesbe, which finishes connecting and opens the app with ?pipedrive=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/pipedriveAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/pipedriveAdmins

    Disconnect: Freesbe removes its webhooks from Pipedrive, forgets Pipedrive's access, and the app's command and action go away. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/pipedrive/alertsAdmins

    Add an alert: channelId, kinds (deal_created, deal_stage, deal_won, deal_lost, lead_created) and an optional minAmount for deals. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 }
  • PATCH/api/workspaces/{w}/pipedrive/alerts/{id}Admins

    Change channelId, kinds or minAmount, or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/pipedrive/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/pipedrive/alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/crm/pipedrive/{token}No token

    Where the webhooks Freesbe adds in Pipedrive deliver deal and lead changes, signed in with HTTP Basic auth and a password only Freesbe knows. Answers 200 to signed-in deliveries, 401 to a wrong password and 404 after a disconnect.

Zoho CRM

The Zoho CRM app: a connected Zoho CRM organization, in any Zoho data center, its deal, lead and contact alerts and link previews. See the [Zoho CRM guide](/docs/zoho-crm/). /zoho and **Log to Zoho CRM** run like any app's slash command and message action. 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-crmAdmins

    Whether the server can connect Zoho (available), the connection (orgId, orgName, dataCenter, crmUrl, botUserId, unfurls, listening while Zoho CRM sends changes, brokenAt when Zoho stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/zoho-crm/connectAdmins

    The url of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with ?zoho-crm=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/zoho-crmAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/zoho-crmAdmins

    Disconnect: Zoho CRM stops sending changes, Freesbe's access is revoked in Zoho, and the app's command and action go away. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/zoho-crm/alertsAdmins

    Add an alert: channelId, kinds (deal_created, deal_stage, deal_won, deal_lost, lead_created, contact_created) and an optional minAmount for deals. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["deal_created", "deal_won"], "minAmount": 10000 }
  • PATCH/api/workspaces/{w}/zoho-crm/alerts/{id}Admins

    Change channelId, kinds or minAmount, or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/zoho-crm/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/zoho-crm/alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/crm/zoho-crm/{token}No token

    Where Zoho CRM delivers one organization's change notifications. The body's token has to be the one Freesbe gave Zoho with the channel. Answers 200 and posts afterwards, 401 to a wrong token, and 404 after a disconnect.

Zendesk

A workspace's Zendesk connection and its ticket alerts. See the [Zendesk guide](/docs/zendesk/). Admins see the alerts for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/zendeskAdmins

    The connection (subdomain, auth (oauth or token), email, whether it's receiving ticket events, needsReconnect and lastEventAt), or null, its alerts, and whether oauth sign-in is available.

  • POST/api/workspaces/{w}/zendesk/oauthAdmins

    Start signing in with Zendesk for subdomain. Answers with the url to send the browser to; Zendesk comes back to the app with ?zendesk=connected or ?zendesk_error=.

    Example

    Body

    { "subdomain": "northwind" }
  • POST/api/workspaces/{w}/zendesk/tokenAdmins

    Connect with an agent's email and Zendesk apiToken instead. The token is checked with Zendesk and stored encrypted.

    Example

    Body

    { "subdomain": "northwind", "email": "agent@northwind.example", "apiToken": "…" }
  • DELETE/api/workspaces/{w}/zendeskAdmins

    Disconnect: removes Freesbe's webhook from Zendesk, the alerts and the Zendesk app. Answers 204.

  • GET/api/workspaces/{w}/zendesk/groupsAdmins

    The Zendesk groups an alert can be limited to, as groups with id and name.

  • POST/api/workspaces/{w}/zendesk/alertsAdmins

    Add a ticket alert: channelId, events (created, status, comment, priority, assignment) and an optional groupId. Answers 201.

    Example

    Body

    { "channelId": "01J…", "events": ["created", "status"], "groupId": "7" }
  • PATCH/api/workspaces/{w}/zendesk/alerts/{id}Admins

    Change channelId, events or groupId, or turn it off and on with disabled.

  • DELETE/api/workspaces/{w}/zendesk/alerts/{id}Admins

    Delete the alert. Answers 204.

  • POST/api/workspaces/{w}/zendesk/alerts/{id}/testAdmins

    Post a sample ticket into its channel. Answers 204.

  • POST/hooks/support/zendesk/{token}No token

    Where Zendesk delivers ticket events, signed with X-Zendesk-Webhook-Signature and X-Zendesk-Webhook-Signature-Timestamp. Freesbe adds this webhook in Zendesk when you connect. Answers 200 to signed events and 401 to a bad or old signature.

Zoho Desk

The Zoho Desk app: a connected Zoho Desk help desk, in any Zoho data center, its ticket alerts and link previews. See the [Zoho Desk guide](/docs/zoho-desk/). **Create Zoho Desk ticket** runs like any app's message action. 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-deskAdmins

    Whether the server can connect Zoho (available), the connection (orgId, orgName, portalName, dataCenter, deskUrl, botUserId, unfurls, listening once Freesbe's webhook is in Zoho Desk, lastEventAt, brokenAt when Zoho stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/zoho-desk/connectAdmins

    The url of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with ?zoho-desk=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/zoho-deskAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/zoho-deskAdmins

    Disconnect: Freesbe removes its webhook from Zoho Desk, its access is revoked in Zoho, and the app's action goes away. Alerts are kept for a reconnect. Answers 204.

  • GET/api/workspaces/{w}/zoho-desk/departmentsAdmins

    The help desk's departments, each with its id and name, for alerts that take one.

  • POST/api/workspaces/{w}/zoho-desk/alertsAdmins

    Add an alert: channelId, kinds (created, status, reply, priority, assignment) and an optional departmentId. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["created", "reply"], "departmentId": "1234000000006907" }
  • PATCH/api/workspaces/{w}/zoho-desk/alerts/{id}Admins

    Change channelId, kinds or departmentId (null for every department), or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/zoho-desk/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/zoho-desk/alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/support/zoho-desk/{token}No token

    Where Freesbe's webhook in Zoho Desk delivers one help desk's new tickets, ticket changes and customer replies. The token is random and only Zoho Desk has it, and every event has to be for the connected help desk. Answers 200 and posts afterwards, 403 to another help desk's events, and 404 after a disconnect.

Zoho Books and Zoho Invoice

The Zoho Books and Zoho Invoice apps: a connected organization in either, in any Zoho data center, its invoice and payment alerts and link previews. See the [Zoho Books and Zoho Invoice guide](/docs/zoho-books/). The two have the same routes, under zoho-books and zoho-invoice; {product} below is one of them. Neither lets an app add a webhook, so Freesbe checks the organization every ten minutes while an alert is on. Admins see the alerts for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/zoho-{product}Admins

    Whether the server can connect Zoho (available), the connection (orgId, orgName, currency, dataCenter, appUrl, botUserId, unfurls, checkedAt when Freesbe last checked, brokenAt when Zoho stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/zoho-{product}/connectAdmins

    The url of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with ?zoho-books=connected or ?zoho-invoice=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/zoho-{product}Admins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/zoho-{product}Admins

    Disconnect: Freesbe's access is revoked in Zoho and checking stops. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/zoho-{product}/alertsAdmins

    Add an alert: channelId, kinds (sent, payment, paid, overdue) and an optional minAmount. Answers 201.

    Example

    Body

    { "channelId": "01J…", "kinds": ["payment", "overdue"], "minAmount": 500 }
  • PATCH/api/workspaces/{w}/zoho-{product}/alerts/{id}Admins

    Change channelId, kinds or minAmount (null for any amount), or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/zoho-{product}/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/zoho-{product}/alerts/{id}/testAdmins

    Post a sample alert, of the first kind it takes, into its channel. Answers 204.

Freshdesk

A workspace's Freshdesk connection and its ticket alerts. See the [Freshdesk guide](/docs/freshdesk/). /freshdesk, **Create Freshdesk ticket** and **Add to Freshdesk ticket** run like any app's slash command and message actions. Admins see the alerts for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/freshdeskAdmins

    The connection (subdomain, the agent's email and agentName, the secret hookUrl Freshdesk's automation rules call, needsReconnect and lastEventAt), or null, and its alerts.

  • POST/api/workspaces/{w}/freshdeskAdmins

    Connect, or connect again, with an agent's Freshdesk apiKey. The key is checked with Freshdesk and stored encrypted; connecting again keeps the hookUrl. Answers like GET.

    Example

    Body

    { "subdomain": "northwind", "apiKey": "…" }
  • DELETE/api/workspaces/{w}/freshdeskAdmins

    Disconnect: forgets the API key, the alerts and the Freshdesk app, and the hookUrl stops working. Answers 204.

  • GET/api/workspaces/{w}/freshdesk/groupsAdmins

    The Freshdesk groups an alert can be limited to, as groups with id and name.

  • POST/api/workspaces/{w}/freshdesk/alertsAdmins

    Add a ticket alert: channelId, events (created, status, reply, priority, assignment) and an optional groupId. Answers 201.

    Example

    Body

    { "channelId": "01J…", "events": ["created", "status"], "groupId": "7" }
  • PATCH/api/workspaces/{w}/freshdesk/alerts/{id}Admins

    Change channelId, events or groupId, or turn it off and on with disabled.

  • DELETE/api/workspaces/{w}/freshdesk/alerts/{id}Admins

    Delete the alert. Answers 204.

  • POST/api/workspaces/{w}/freshdesk/alerts/{id}/testAdmins

    Post a sample ticket into its channel. Answers 204.

  • POST/hooks/support/freshdesk/{token}No token

    Where Freshdesk's automation rules say a ticket was made ({"freesbe_event":"created","ticket_id":"…"}) or changed ("updated"). Freesbe reads the ticket from Freshdesk and posts what changed since it last saw it. Answers 200, 400 without a ticket number and 404 after a disconnect.

Monitoring

Monitoring tools posting problems into a channel: Zabbix, Grafana, Prometheus Alertmanager, PagerDuty, Opsgenie, Datadog, Sentry, Better Stack, New Relic, Uptime Kuma 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}/monitoringAdmins

    The connections: provider, name, channelId, minSeverity (info, warning, average, high or critical), replyOnResolve, serverUrl, hasApiToken, openProblems, lastEventAt, board (the live problems board is pinned), reportSchedule (daily, weekly or null), reportTimeZone and nextReportAt.

  • POST/api/workspaces/{w}/monitoringAdmins

    Connect a tool: provider (zabbix, grafana, alertmanager, pagerduty, opsgenie, datadog, sentry, betterstack, newrelic, uptimekuma or uptimerobot), name, channelId, and optionally minSeverity, replyOnResolve, serverUrl, apiToken, board (default true), reportSchedule and reportTimeZone (reports post at 9:00 there). Freesbe checks the token with the tool before saving it, encrypted. Answers 201 with the hookUrl to give the tool, shown only now.

    Example

    Body

    { "provider": "zabbix", "name": "Production Zabbix", "channelId": "01J…", "minSeverity": "warning" }
  • PATCH/api/workspaces/{w}/monitoring/{id}Admins

    Change name, channelId, minSeverity, replyOnResolve, serverUrl, apiToken (null removes it, and the buttons), board, reportSchedule or reportTimeZone, or turn it off and on with disabled.

    Example

    Body

    { "minSeverity": "high" }
  • DELETE/api/workspaces/{w}/monitoring/{id}Admins

    Disconnect it. Cards already posted stay. Answers 204.

  • POST/api/workspaces/{w}/monitoring/{id}/testAdmins

    Post a sample problem card into its channel. Answers 204.

  • POST/hooks/monitoring/zabbix/{token}No token

    Where Freesbe's Zabbix media type sends each problem, update and recovery, as JSON with Zabbix's macros filled in (event_id, event_value, event_update_status, event_nseverity, event_name, host_name…). Answers 200 whether or not it posted, 404 to an unknown or switched-off connection, and 410 when its channel is gone.

  • POST/hooks/monitoring/grafana/{token}No token

    Where a Grafana webhook contact point sends: Alertmanager's webhook format (version 4) with Grafana's extra fields. Each alert in alerts is one card, named by its fingerprint and startsAt; a resolved alert turns its card green. Same answers as Zabbix's.

  • POST/hooks/monitoring/alertmanager/{token}No token

    Where Prometheus Alertmanager's webhook_configs send, in its webhook format (version 4). Each alert is one card; the severity label sets its severity (high when there's none). Same answers as Zabbix's.

  • POST/hooks/monitoring/pagerduty/{token}No token

    Where a PagerDuty generic webhook (V3) subscription sends incident events. Each incident is one card, named by its id: incident.triggered posts it, acknowledgements, reassignments, escalations, priority changes and notes update it, and incident.resolved turns it green. The priority (P1 to P5) sets its severity, or the urgency when there's none. Same answers as Zabbix's.

  • POST/hooks/monitoring/opsgenie/{token}No token

    Where an Opsgenie Webhook integration sends alert actions. Each alert is one card, named by its alertId: Create posts it, Acknowledge, UnAcknowledge, AddNote, AssignOwnership and UpdatePriority update it, and Close turns it green. The priority (P1 to P5) sets its severity. Same answers as Zabbix's.

  • POST/hooks/monitoring/datadog/{token}No token

    Where a Datadog webhook sends monitor notifications, with the payload shown when connecting (Datadog's $ALERT_CYCLE_KEY, $ALERT_ID, $ALERT_TRANSITION, $ALERT_PRIORITY, $EVENT_TITLE, $HOSTNAME, $ALERT_SCOPE, $TAGS, $LINK and more). Each alert cycle is one card: Triggered, Warn and No Data post or update it, and Recovered turns it green. Same answers as Zabbix's.

  • POST/hooks/monitoring/sentry/{token}No token

    Where a Sentry internal integration sends its webhooks. Issue webhooks (created, resolved, unresolved, archived, assigned) and issue alerts (triggered) are one card per issue, named by its id; an issue that's unresolved after it was resolved gets a new card. Metric alerts (critical, warning, resolved) are cards of their own. The issue's level sets its severity. Same answers as Zabbix's.

  • POST/hooks/monitoring/betterstack/{token}No token

    Where a Better Stack Uptime webhook sends incidents ({ data: { id, type: "incident", attributes } }). Each incident is one card, named by its id: Started posts it, Acknowledged updates it, and Resolved turns it green. Same answers as Zabbix's.

  • POST/hooks/monitoring/newrelic/{token}No token

    Where a New Relic workflow's webhook sends issues, with the payload shown when connecting (the workflow's issueId, issuePageUrl, priority, state, entitiesData.names, accumulations.conditionName and more). Each issue is one card: CREATED and ACTIVATED post it, ACKNOWLEDGED updates it, and CLOSED turns it green. The priority sets its severity. Same answers as Zabbix's.

  • POST/hooks/monitoring/uptimekuma/{token}No token

    Where an Uptime Kuma Webhook notification sends ({ heartbeat, monitor, msg }, as application/json). Each outage is one card per monitor: heartbeat.status 0 posts it, and 1 turns it green. Going down again after being back up is a new card. Kuma's test message posts nothing. Same answers as Zabbix's.

  • POST/hooks/monitoring/uptimerobot/{token}No token

    Where an UptimeRobot webhook alert contact sends, with the payload shown when connecting (monitorID, name, url, alertType, details, duration, time). alertType 1 posts a card for the monitor and 2 turns it green with how long it was down; other alerts post nothing. Same answers as Zabbix's.

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}/feedsAdmins

    The feeds: url, siteUrl, name, iconUrl, channelId, keywords, lastCheckedAt, lastItemAt, nextCheckAt, error (while it keeps failing) and brokenAt (paused after 3 days of failing), and the plan's limit (null for none).

  • POST/api/workspaces/{w}/feeds/previewAdmins

    Find and read a feed before adding it: url is the feed's address or a website's whose page links to its feed. Answers with the feed's own url, title, siteUrl, iconUrl and its newest 3 items, or 400 when there's no feed there.

    Example

    Body

    { "url": "blog.northwind.example" }
  • POST/api/workspaces/{w}/feedsAdmins

    Add a feed: url, channelId, and optionally name (the feed's title), keywords (only items with one of them in the title or summary post) and postLatest (default true: post the newest item now). Everything already in the feed counts as seen. Answers 201, or 409 at the plan's limit.

    Example

    Body

    { "url": "https://blog.northwind.example/feed.xml", "channelId": "01J…", "keywords": ["tea"] }
  • PATCH/api/workspaces/{w}/feeds/{id}Admins

    Change name, channelId or keywords, or turn it off and on with disabled. Saving a paused feed checks it again soon.

    Example

    Body

    { "keywords": ["tea", "coffee"] }
  • DELETE/api/workspaces/{w}/feeds/{id}Admins

    Stop checking the feed. Cards already posted stay. Answers 204.

  • POST/api/workspaces/{w}/feeds/{id}/checkAdmins

    Check the feed now and post anything new. Answers 204.

Zoho Recruit

The Zoho Recruit app: a connected Zoho Recruit organization, in any Zoho data center, its candidate, job opening and interview alerts and link previews. See the [Zoho Recruit guide](/docs/zoho-recruit/). Alerts come from Zoho Recruit's change notifications, which Freesbe sets up and renews itself. Admins see the alerts for public channels and the private channels they're in.

  • GET/api/workspaces/{w}/zoho-recruitAdmins

    Whether the server can connect Zoho (available), the connection (orgId, orgName, dataCenter, recruitUrl, botUserId, unfurls, listening while Zoho Recruit sends changes, brokenAt when Zoho stopped accepting it) or null, and the alerts.

  • POST/api/workspaces/{w}/zoho-recruit/connectAdmins

    The url of Zoho's page that asks to allow Freesbe. Send the browser there; Zoho sends it to the person's own data center, then back to Freesbe, which finishes connecting and opens the app with ?zoho-recruit=connected. Only the admin who started it can finish it, within 15 minutes.

  • PATCH/api/workspaces/{w}/zoho-recruitAdmins

    Turn link previews on or off with unfurls.

    Example

    Body

    { "unfurls": false }
  • DELETE/api/workspaces/{w}/zoho-recruitAdmins

    Disconnect: Zoho Recruit stops sending changes and Freesbe's access is revoked in Zoho. Alerts are kept for a reconnect. Answers 204.

  • POST/api/workspaces/{w}/zoho-recruit/alertsAdmins

    Add an alert: channelId and kinds (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}Admins

    Change channelId or kinds, or turn it off and on with disabled.

    Example

    Body

    { "disabled": true }
  • DELETE/api/workspaces/{w}/zoho-recruit/alerts/{id}Admins

    Delete it. Answers 204.

  • POST/api/workspaces/{w}/zoho-recruit/alerts/{id}/testAdmins

    Post a sample alert into its channel. Answers 204.

  • POST/hooks/recruiting/zoho-recruit/{token}No token

    Where Zoho Recruit delivers one organization's change notifications. The body's token has to be the one Freesbe gave Zoho with the channel. Answers 200 and posts afterwards, 401 to a wrong token, and 404 after a disconnect.

Workflows

Workflows run steps for people: post a message, send a form, add someone to a channel, give a to-do, wait, ask for approval, ask a question with buttons, set a status, or branch with if. Guests can't make or run them. A workflow's maker and workspace admins can change it and see its runs; everyone else sees only the button workflows that are on. Text in steps can use {{variables}} such as {{trigger.person}}. See [Workflows](/docs/workflows) for what each trigger and step does.

  • GET/api/workspaces/{w}/workflowsMembers

    The workflows you manage, plus the button workflows that are on, by name. canCreate says whether you can make one. People who can't edit a workflow don't get its steps.

    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 guests

    Make a workflow: name, optional description and emoji, a trigger, up to 30 steps, and status on or off. Only admins can when the workspace's workflowBuilders is admins. A workflow with problems can be saved, but stays off. Triggers: manual, schedule, reaction, member_joined, message, webhook, clock, booking, meeting_ended, huddle_ended, missed_call, todo_done. Steps: message, form, add_to_channel, todo, wait, approval, ask, set_status, branch (with conditions, then and else). Answers 201.

    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}Members

    One workflow you can see.

    Example

    Answer

    { "workflow": { "id": "01JBK4…", "name": "IT help request", "trigger": { "type": "manual", … }, "steps": [ … ], … } }
  • PATCH/api/workflows/{id}Creator or admins

    Change any of the fields above. Turning it on needs it free of problems; a schedule's next run is worked out again.

    Example

    Body

    { "status": "off" }
  • DELETE/api/workflows/{id}Creator or admins

    Delete it and stop its runs that are still going. Answers 204.

  • POST/api/workflows/{id}/runMembers, not guests

    Run a workflow that starts from a button. Pass channelId to start it from a channel. Its maker can try it while it's off. A form for you comes back as request, ready to fill in. Up to 200 runs an hour each.

    Example

    Body

    { "channelId": "01JB9S…" }

    Answer

    { "run": { "id": "01JBK7…", "status": "waiting", … }, "request": { "id": "01JBK8…", "title": "Ask IT for help", … } }
  • GET/api/workflows/{id}/runsCreator or admins

    The 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 admins

    A new secret URL for a workflow that starts from a webhook, as the workflow's hookUrl. The old one stops working at once.

  • POST/api/workflow-runs/{id}/cancelCreator or admins

    Stop a run that is still going. Answers 204.

  • GET/api/workspaces/{w}/workflow-requestsMembers

    Forms, approvals and questions that workflows sent you and that are still open. kind is form, approval or ask. Questions posted in a channel for anyone to answer aren't listed.

    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 owner

    One request sent to you, open or not, or a question in a channel you're in.

  • POST/api/workflow-requests/{id}Its owner

    Answer it, and the workflow carries on at once. A form takes answers by question id; an approval takes decision (approved or rejected) and an optional comment; a question takes one of its choices as choice. Answers 204, or 409 when someone already answered.

    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 owner

    Close a form without sending it, which stops the workflow that sent it. Only forms can be closed this way. Answers 204.

  • POST/hooks/workflows/{token}No token

    Start a workflow that starts from a webhook, with a JSON object of its variables. The token in the URL is the secret. Answers { "ok": true }, or ok: false with an error.

    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}/phoneMembers

    Your SIP account, if it's set up. Add ?reveal=1 for the password. Guests get provider: "off".

    Example

    Answer

    { "provider": "sipio", "account": { "username": "dana.whitfield", "domain": "northwind.sip.example",
      "createdAt": 1761200000000, "rotatedAt": null } }
  • POST/api/workspaces/{w}/phoneMembers, not guests

    Set up your SIP account. The answer includes the password.

  • POST/api/workspaces/{w}/phone/rotateMembers, not guests

    A new SIP password. The old one stops working.

  • POST/api/workspaces/{w}/phone/webphoneMembers, not guests

    A short-lived login for calling from Freesbe itself, on a phone system from Freesbe: a SIP over WebSocket server, the uri, username and password, iceServers for the call's audio, and when it runs out (expiresAt). One at a time per person: a new one ends the last. 409 when the workspace has no phone system from Freesbe or it's turned off. inApp on GET /phone says 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 guests

    Your calls on the phone system from Freesbe from the last 30 days, newest first, 50 at a time (?page=2 for older ones while more is true). direction is from your side; userId is the colleague on the other end, and number is null for them. seconds is time talking, 0 when unanswered. 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:call

    Click-to-call: ring someone's Freesbe phone, and once they pick up, dial to from the workspace's number. People start calls for themselves; an app with the phone:call scope names whose phone rings with userId or email (for example a CRM's Call button, from its server). leadId and vars (up to 20 short values) come back in the phone.call_ended event, with how it went and how long it lasted. 409 with call_unreachable when the person isn't signed in to Freesbe's phone anywhere (the app on a computer or phone), call_busy when they're on a call, call_duplicate for the same call twice within 5 seconds, and conflict without a phone system from Freesbe or while it's off.

    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-connectionAdmins

    Whether the workspace is connected to sip.io, and how.

  • PUT/api/workspaces/{w}/sip-connectionAdmins

    Connect sip.io: apiBase, accountId, apiKey, and linkByEmail to put each new device on the sip.io user with the same email (off by default). Freesbe checks the key first.

    Example

    Body

    { "apiBase": "https://northwind.sip.example/api", "accountId": "4411", "apiKey": "…", "linkByEmail": false }
  • DELETE/api/workspaces/{w}/sip-connectionAdmins

    Disconnect sip.io. Answers 204.

  • GET/api/workspaces/{w}/phone-systemAdmins

    The phone system from Freesbe. available says whether this server offers one, included whether the workspace has the Phone system add-on, and enabled whether it's on. status is active, outbound_off while a payment is a week overdue (incoming calls still ring), or off, with offReason (admin or unpaid) and releaseAt, when the kept numbers are released.

    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-systemAdmins

    Turn the phone system on, for a country (a two-letter code; the United States or Canada for now). Answers like GET.

    Example

    Body

    { "country": "US" }
  • DELETE/api/workspaces/{w}/phone-systemAdmins

    Turn the phone system off. Calls, texts and phone logins stop, and the numbers are kept until releaseAt. Answers like GET.

  • POST/api/workspaces/{w}/phone-system/turn-on-againAdmins

    Turn a phone system that was turned off back on, with the numbers it kept. When it was turned off for an overdue payment, pay the invoice on the Billing page first, or this answers 409.

  • GET/api/workspaces/{w}/phone-system/identityAdmins

    Where the workspace is with identity verification, which buying numbers needs once: required, and status: none, waiting (the link is ready for a phone, until linkExpiresAt), started, checking, review, approved, rejected or expired. POST starts a check, or answers the one that's open.

    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=415Admins

    Numbers for sale. type is local (the default) or tollfree; areaCode is optional. Each offer has its monthly and setup price in currency, and needsDocuments when the carrier needs documents before it works. included is how many local numbers the workspace can still get at no extra cost (one comes with each person on the Phone system add-on).

    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/numbersAdmins

    The workspace's numbers: each one's status (active, pending, documents_needed, releasing or other), place, type, who it rings (ringsUserIds, everyone it rings at once; ringsUserId is set when that's one person), whether it can send and receive texts, and whether it goes to voicemail after hours (afterHoursVoicemail).

    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/numbersAdmins

    Buy a number: send back an offer from the search, and optionally ringsUserId, the person its calls ring. Needs identity verification first. status is active, pending or documents_needed. Answers 201.

    Example

    Body

    { "offer": "…", "ringsUserId": "01JB8Q…" }

    Answer

    { "number": { "id": "01JC5N…", "e164": "+14155550134", "status": "active", … }, "status": "active" }
  • PATCH/api/workspaces/{w}/phone-system/numbers/{id}Admins

    Change who a number rings: ringsUserIds, 1 to 20 people, who all ring at once and the first to answer takes the call (ringsUserId still works for one person). The first time a number rings someone, Freesbe makes their phone account. afterHoursVoicemail: true sends its calls outside the business hours to the voicemail of the first person it rings (set the hours first). menuId answers it with a phone menu instead, and queueId with a call queue (both turn voicemail after hours off). Send what changes. Answers the number, with its menuId and queueId.

    Example

    Body

    { "ringsUserIds": ["01JB8R…", "01JB8S…"], "afterHoursVoicemail": true }
  • GET/api/workspaces/{w}/phone-system/menusAdmins

    The phone menus (IVR): each one's greeting (typed text read in a voice, or an uploaded recording with text null), the options callers press, noChoice (where the call goes when nobody presses a key, after the greeting plays twice), waitSeconds, and the numberIds it answers. An action is people (userIds, all ringing at once), voicemail (a person's userId), menu (menuId), queue (queueId) or hangup.

    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/menusAdmins

    Make a phone menu: a name, a greeting (text and a voice from GET …/voices, a recordingId from uploading one, or null for none), up to 12 options with a key (0 to 9, * or #) and an action, a noChoice action, and waitSeconds (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}Admins

    Change a phone menu. Send it whole, like POST. Answers the menu.

  • DELETE/api/workspaces/{w}/phone-system/menus/{id}Admins

    Delete a phone menu. Answers 204, or 409 while a number answers with it or another menu sends calls to it.

  • GET/api/workspaces/{w}/phone-system/queuesAdmins

    The call queues: each one's memberIds (who takes its calls, in order), ring (longestIdle, all, turns, inOrder, fewestCalls or random), greeting (like a menu's), holdMusicId (null for the standard music), announcePosition and announceEverySeconds, ringSeconds, wrapUpSeconds, maxWaitMinutes and maxCallers (null for no limit), where callers go afterWait and when noOneAvailable is signed in (actions, like a menu's), and the numberIds it answers.

    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/queuesAdmins

    Make a call queue, with every field above but id and numberIds: 1 to 100 memberIds, announceEverySeconds 30 to 600, ringSeconds 5 to 120, wrapUpSeconds 0 to 600, maxWaitMinutes 1 to 120 and maxCallers 1 to 500 (or null). holdMusicId is a recordingId from uploading a file. New members are signed in to take its calls. Needs the Call center add-on (403 without it). Answers 201 with the queue. A workspace can have 30.

    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}Admins

    Change a call queue. Send it whole, like POST. Answers the queue.

  • DELETE/api/workspaces/{w}/phone-system/queues/{id}Admins

    Delete a call queue. Answers 204, or 409 while a number answers with it or a menu or another queue sends calls to it.

  • GET/api/workspaces/{w}/phone-system/break-reasonsAdmins

    The reasons people in call queues pick for a break, each with a name, a color ("#RRGGBB", or null for gray) and a limitMinutes time limit (null for none).

    Example

    Answer

    { "reasons": [{ "id": "pr_2a…", "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 }] }
  • POST/api/workspaces/{w}/phone-system/break-reasonsAdmins

    Add a break reason: a name (40 characters at most), a color, and an optional limitMinutes (1 to 480; people are told when a break runs over it). Needs the Call center add-on. A workspace can have 20. Answers 201 with the reason.

    Example

    Body

    { "name": "Lunch", "color": "#F59E0B", "limitMinutes": 60 }
  • PATCH/api/workspaces/{w}/phone-system/break-reasons/{id}Admins

    Rename a break reason or change its color or time limit. Send the name and color; leaving out limitMinutes takes the limit off. Answers the reason.

  • DELETE/api/workspaces/{w}/phone-system/break-reasons/{id}Admins

    Delete a break reason. Answers 204.

  • GET/api/workspaces/{w}/phone-system/supervisorsAdmins

    Who 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/supervisorsAdmins

    Replace 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=Admins

    Call center numbers for a range of dates (up to 90 days, counted in tz): each queue's calls, answers, callers who hung up or timed out, service level, waits and talk time; calls per day; each person's answered calls and time talking, in wrap-up, on a break and signed in; and break time per reason. queues (comma-separated ids) narrows the queue numbers and days. Needs the Call center add-on.

    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/call-log?from=2026-10-01&to=2026-10-31&tz=America/New_York&people=&direction=&result=&q=&minSeconds=&page=1&limit=50Admins

    Every call on Freesbe's phone system for a range of dates (up to 90 days, counted in tz), newest first: who called whom, in, out or between colleagues, who answered a call that rang a queue or several people, and how long they talked. people (comma-separated user ids) keeps calls those people made, took or were rung for. direction is in, out or internal; result is answered or missed; q matches any number (three digits or more) on either end, or a name; minSeconds keeps longer calls. With direction, result, q or minSeconds, up to the newest 5,000 calls in the range are searched, and searchedCalls says when the search stopped there. limit is up to 500 a page; more says another page may have older calls. recordingId is set on recorded calls.

    Example

    Answer

    { "calls": [{ "id": "cdr_1", "startedAt": 1793536200000, "direction": "in", "from": { "userId": null, "name": null, "number": "+14155550142" }, "to": { "userId": "01J…", "name": "Support", "number": "+18135550111" }, "answeredBy": "01J…", "answered": true, "seconds": 184, "recordingId": "rec_1" }], "more": false }
  • GET/api/workspaces/{w}/phone-system/recordings/{id}/audioAdmins

    A recorded call's audio (usually Ogg Opus), the whole file. 409 not_ready while the recording is still being processed.

  • GET/api/workspaces/{w}/phone-system/recordings/{id}/transcriptAdmins

    A recorded call's transcript and summary, or null before one is made. side is agent for the person in the workspace and caller for the other end; start is seconds into the call.

    Example

    Answer

    { "transcript": { "state": "done", "summary": "The caller asked to move their delivery…", "sentiment": "positive", "keyPoints": ["…"], "actionItems": ["…"], "lines": [{ "side": "caller", "speaker": null, "start": 0.4, "text": "Hi, I'm calling about my order." }] } }
  • POST/api/workspaces/{w}/phone-system/recordings/{id}/transcriptAdmins

    Make the transcript and summary now, then answer it like the GET. 409 transcription_disabled until transcripts are turned on under Phone → Call recording.

  • GET/api/workspaces/{w}/phone-system/recordingAdmins

    Which calls the phone system records: record is off, everyone or some (the people in userIds), calls says which of their calls, and queues records every call queue call. Also whether recordings are transcribed and summarized with AI, how many days they're kept (0 for good), and lineUserIds, the people with a phone line who can be picked.

    Example

    Answer

    { "record": "everyone", "calls": { "incoming": true, "outgoing": true, "internal": false }, "queues": true, "userIds": [], "transcribe": true, "summarize": true, "keepDays": 365, "lineUserIds": ["01J…"] }
  • PUT/api/workspaces/{w}/phone-system/recordingAdmins

    Change the call recording settings, sending them all as GET answers them, without lineUserIds. keepDays is one of 30, 90, 365, 730 or 0, and applies to calls recorded from then on. Answers the settings as saved.

    Example

    Body

    { "record": "some", "calls": { "incoming": true, "outgoing": false, "internal": true }, "queues": false, "userIds": ["01J…"], "transcribe": true, "summarize": false, "keepDays": 90 }
  • GET/api/workspaces/{w}/phone-system/voicesAdmins

    The 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/greetingsAdmins

    Hear a typed greeting before saving it: ?text= read in ?voice=. Answers the audio (WAV).

  • POST/api/workspaces/{w}/phone-system/greetingsAdmins

    Upload a recorded greeting as a WAV file in the multipart field audio (10 MB at most). Answers 201 with the recordingId to save in a menu, or as a queue's greeting or hold music.

    Example

    Answer

    { "recordingId": "media_4b…", "seconds": 8 }
  • GET/api/workspaces/{w}/phone-system/greetings/{id}/audioAdmins

    Play a menu's greeting. Answers the audio.

  • GET/api/workspaces/{w}/phone-system/hoursAdmins

    The workspace's business hours, or null until they're set: a timezone, open windows by day (0 = Sunday) with open and close times, and closedDates.

    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/hoursAdmins

    Set the business hours, replacing them all. close can be 24:00 for 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/emergencyAdmins

    Emergency addresses (E911): the workspace's locations with how many people work from each, the defaultLocationId used for everyone who hasn't picked one, and alertChannelId, the channel told when someone dials 911 (null: each admin gets a message from Phone). A location's status is saved until it's registered with the carrier (registration false means that isn't turned on yet), then pending, confirmed, or needs_fixing with a problem.

    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/emergencyAdmins

    Change the defaultLocationId (one of the workspace's locations) or alertChannelId (a channel, or null for the admins). Answers like GET.

    Example

    Body

    { "alertChannelId": "01JC…" }
  • POST/api/workspaces/{w}/phone-system/emergency/locationsAdmins

    Add an emergency address in the United States or Canada: a name and the street address with its number, two-letter region and ZIP or postal code. The first one becomes the default. Answers 201 with the location.

    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}Admins

    Change a location's name or address, like POST. It's checked with the carrier again.

  • DELETE/api/workspaces/{w}/phone-system/emergency/locations/{id}Admins

    Remove a location. 409 for the default, or while people work from it. Answers 204.

  • GET/api/workspaces/{w}/phone/queue-statusMembers, not guests

    Your place in the call queues: the queues you're in (empty when none, or without the Call center add-on), status (taking, break or out), the breakReasonId of a break, since when, phoneOffline when you take calls but your Freesbe phone isn't open anywhere, and the breakReasons you can pick.

    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 guests

    Take your queues' calls (taking), take a break (with a breakReasonId or null), or sign out (out). Calls to your own number ring either way. 409 when you're in no queue. Answers your status.

    Example

    Body

    { "status": "break", "breakReasonId": "pr_2a…" }
  • GET/api/workspaces/{w}/phone/call-centerMembers, not guests

    The live Call center board, for admins and supervisors (403 for anyone else, or without the Call center add-on): the queues with their people, breakReasons, and live: each queue's callers waiting and numbers for today, everyone's state and how long they've been in it, and the callers waiting (times in seconds). agentIds maps the ids in live to people. socket is a one-time, one-minute wss:// URL that sends a fresh live as a snapshot frame whenever something changes; null when live updates aren't available.

    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 guests

    A new one-time socket URL 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 guests

    Nudge someone in a call queue with a direct message (message, 500 characters at most), from you (me) or from the Call center bot (bot). Admins and supervisors only. Answers 204.

    Example

    Body

    { "userId": "01J…", "message": "Callers are waiting. Can you come back from your break?", "from": "bot" }
  • GET/api/workspaces/{w}/phone/directoryMembers, not guests

    The colleagues with a phone, by name, with their extension (null when they have none) and Freesbe userId (null for a phone system user who isn't a member), and the call queues (empty without the Call center add-on): who the phone can call and pass calls to.

    Example

    Answer

    { "people": [{ "userId": "01J…", "name": "James Okafor", "extension": "102" }], "queues": [{ "id": "q_4d…", "name": "Support" }] }
  • POST/api/workspaces/{w}/phone/transferMembers, not guests

    Pass your current call on. start with mode now (the caller holds until the target answers, then goes to them; nobody answering leaves the caller with you) or talk (you talk to the target first while the caller holds), to a target: { kind: "person", userId }, { kind: "queue", id } (always now) or { kind: "number", number } (a colleague's extension goes to them). Talking first answers a transferId; then complete hands the caller over and ends your side, merge makes it a 3-way call, and cancel goes back to the caller. 409 with a message when it can't be done, such as when you're not on a call or the target hasn't answered yet.

    Example

    Body

    { "action": "start", "mode": "talk", "target": { "kind": "person", "userId": "01J…" } }

    Answer

    { "transferId": "call_7f…" }
  • GET/api/workspaces/{w}/phone/voicemailsMembers, not guests

    Your own voicemails, newest first: who called (number, and name for a colleague), when (at), how long (seconds), whether it's unread, and the unread count.

    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 guests

    Mark one of your voicemails heard (unread: false) or not. Answers 204.

    Example

    Body

    { "unread": false }
  • DELETE/api/workspaces/{w}/phone/voicemails/{id}Members, not guests

    Delete one of your voicemails. The copy in your DM from Phone stays. Answers 204.

  • GET/api/workspaces/{w}/phone/voicemails/{id}/audioMembers, not guests

    The recording, as WAV. Playing it marks it heard.

  • GET/api/workspaces/{w}/phone/emergencyMembers, not guests

    Where your 911 calls say you are: the workspace's locations and your own, the one you picked (locationId, null for the workspace's defaultLocationId).

  • PUT/api/workspaces/{w}/phone/emergencyMembers, not guests

    Say where you work from: locationId (one of the workspace's or your own, null for the default), or own to add a place of yours, like Home, and pick it (up to 5). Answers like GET.

    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 guests

    Remove one of your own places. If you worked from it, your calls use the workspace's default again. Answers like GET.

  • DELETE/api/workspaces/{w}/phone-system/numbers/{id}Admins

    Release a number back to the carrier. Calls and texts to it stop, and you may not get it again. Answers 204.

Slack import

An admin uploads a Slack export zip in parts, then starts the import. Imported people get an account that waits for them; a sign-in link lets each one take theirs over.

  • GET/api/workspaces/{w}/importsAdmins

    Imports, newest first, with their status, phase and counts.

  • POST/api/workspaces/{w}/importsAdmins

    Start an upload: the zip's fileName and size. The answer's partSize is how big each part is.

    Example

    Body

    { "fileName": "Northwind Slack export.zip", "size": 734003200 }
  • GET/api/workspaces/{w}/imports/{id}Admins

    One import and its progress.

  • PUT/api/workspaces/{w}/imports/{id}/parts/{part}Admins

    Upload part 1, 2, 3… of the zip as raw bytes. Every part but the last is exactly partSize. Answers partNumber and etag.

  • POST/api/workspaces/{w}/imports/{id}/startAdmins

    The upload is complete: send every part's partNumber and etag, and the import starts.

    Example

    Body

    { "parts": [ { "partNumber": 1, "etag": "…" }, { "partNumber": 2, "etag": "…" } ] }
  • POST/api/workspaces/{w}/imports/{id}/cancelAdmins

    Stop an upload or a running import.

  • POST/api/workspaces/{w}/claimsAdmins

    Sign-in links for imported people who haven't signed in: everyone, or userIds. email: true also emails each person theirs.

    Example

    Body

    { "email": true }
  • GET/api/claims/{token}No token

    What a sign-in link shows: the workspace, the person's name and email from Slack, and whether that email already has an account.

  • POST/api/auth/claim-emailNo token

    Email a sign-in link if the address belongs to an imported account. Answers 204 either way.

  • POST/api/auth/claimNo token

    Take over an imported account: the link's token, a password, and an email when Slack had none. Answers like login.

  • POST/api/claims/{token}/acceptSigned in

    Add the imported account's history to the account you're signed in to.

Realtime

  • WS/api/workspaces/{w}/rtMembers

    The workspace WebSocket: every change as it happens. See Realtime events above.

Operator

  • GET/api/operator/usageFreesbe staff

    Usage per workspace for the people who run the server: ?days= (0 for all time) and &workspace= for its people.

  • GET/api/operator/call-qualityFreesbe staff

    How 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 staff

    This 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 staff

    Pause or resume video calls everywhere (paused).

  • PATCH/api/operator/workspaces/{w}/planFreesbe staff

    Set a workspace's plan: free, business, corporate or enterprise. Billing isn't built yet. Answers 204.

  • PATCH/api/operator/workspaces/{w}/limitsFreesbe staff

    Set a workspace's own monthly limits: videoGb, storageGb, aiRequests and spendUsd (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; leaving aiRequests or spendUsd out 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.

WhatTodayInstead
Bot tokens from other workspaces' appsApps 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 guestsEach 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 meetingsOutgoing 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 emailThere's only the full member list.Fetch the list once and keep a map of emails to user ids.
Search filtersSearch takes text and one channel.Filter the results by userId and createdAt yourself.
Passkeys, Google and Microsoft sign-in, and the calls themselvesThey 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 ownershipThese aren't in the app either.Ask a workspace admin to deactivate the account.