REST API

Base URL, authentication, the workspace header, errors, rate limits and pagination of the Fishcamp REST API, with the list of endpoints.

Updated

The REST API is the same one the Fishcamp app uses. Everything is JSON over HTTPS.

https://api.fishcamp.space/api/v1

Authentication

Every request carries a bearer token:

Authorization: Bearer cwk_YOUR_KEY
  • Scripts, bots and AI agents use an API key (cwk_…). Every GET needs the read scope; creating or changing things needs write. A key sees what the admin who created it sees, and an AI agent's key only reaches the agent's channels.
  • The Fishcamp app calls the same endpoints with the signed-in person's short-lived session token.
  • Admin actions need a person. Endpoints marked Admins refuse API keys with 403 forbidden, and so do those marked People. A key never acts as an admin, even when an admin created it.
  • Some routes need no key or session. Incoming webhooks carry their secret in the URL. Guests use the /meet/:code routes without an account (sending audio takes the guest token they get when they join), and the invite link preview, GET /invite-links/:code, is public.

A missing, invalid or expired token gets 401 unauthorized.

The workspace header

Endpoints that work inside a workspace (channels, messages, members, audio rooms, invite and meeting links, webhooks, API keys, AI agents, transcripts, integrations) need the workspace's ID:

X-Workspace-Id: 9b2f6c1d-3e4a-4f5b-8c7d-1a2b3c4d5e6f

GET /workspaces lists the workspaces of the signed-in person with their IDs; for an API key, the MCP tool whoami returns its workspace. Without the header, or for a workspace you're not a member of, the answer is 403 forbidden. A key only works in its own workspace: with a key the header is optional, and another workspace's ID gets 403.

A first request

curl "https://api.fishcamp.space/api/v1/channels" \
  -H "Authorization: Bearer cwk_YOUR_KEY" \
  -H "X-Workspace-Id: YOUR_WORKSPACE_ID"
[
  {
    "id": "0b6f3c55-2a51-4e0a-a3d6-6a3f1f2e9d10",
    "workspaceId": "9b2f6c1d-3e4a-4f5b-8c7d-1a2b3c4d5e6f",
    "kind": "STANDARD",
    "visibility": "PUBLIC",
    "name": "general",
    "topic": "Team conversations",
    "voiceEnabled": true,
    "cameraEnabled": true,
    "screenShareEnabled": true,
    "transcriptionEnabled": false,
    "createdAt": "2026-11-02T13:05:11.000Z",
    "lastMessageAt": "2026-11-05T14:32:08.000Z",
    "directPeer": null,
    "isMember": true,
    "unread": false
  }
]

Conventions

  • IDs are UUIDs. Dates are ISO 8601 in UTC.
  • Creating returns 201 Created with the new object. Deleting and actions without a result return 204 No Content.
  • Visibility: a private channel you're not in, or a channel, message, member or invite ID from another workspace, answers 404 not_found, never 403, so the API doesn't reveal that it exists. (A whole workspace you're not in, named in X-Workspace-Id, answers 403.)
  • Request IDs: every error carries a requestId. Send your own in an X-Request-Id header (6 to 64 letters, digits, _ or -) to match logs on both sides.
  • Body size: up to 512 KB per request. Audio sent for transcription goes in chunks of up to 1 MB.
  • Email language: POST /auth/email-code (the sign-in code) and POST /workspace/invites take an optional locale, en (the default), pt or es, and send the email in that language. Any other value gets 422. New in November 2026

Errors

Every error has the same shape:

{
  "error": {
    "code": "validation_error",
    "message": "Some fields are invalid",
    "details": { "errors": [{ "path": "content", "message": "Messages must be 1 to 4000 characters" }] },
    "requestId": "req_5f0c2a9b1e7d4c3a"
  }
}

message is written for people and can be shown as is. details only appears when there's something useful in it, like the list of invalid fields (up to 20).

StatuscodeWhen
400validation_errorThe body isn't valid JSON.
401unauthorizedNo token, or an invalid, expired or revoked one.
403forbiddenNot a member of the workspace, missing X-Workspace-Id, a role that's too low, an API key on an admin or people-only action, or a key without read on a GET.
404not_foundIt doesn't exist, or you can't see it.
409conflictIt clashes with something, for example a channel name that's taken or removing the last owner.
409voice_disabledJoining an audio room, or sharing a meeting link, in a channel with voice turned off.
409waiting_for_host, meeting_full, meeting_unavailableA guest can't join yet: nobody from the workspace is in the room, it already has 20 guests, or the channel's voice is off. See Meeting link endpoints.
409transcript_busy, transcript_emptyChoosing where a transcript goes while it's still being made or decided, or when nothing was transcribed.
409transcript_closed, transcription_off, no_audio_roomSending audio for a transcript that's already closed, in a channel with transcription off, or with no audio room open.
409no_webhookTesting an AI agent, or rotating its webhook secret, when it has no webhook URL.
410invite_expired, invite_unavailableThe email invite expired, was revoked or was already used.
410invite_link_expired, invite_link_revoked, invite_link_used_upThe invite link expired, was turned off or reached its limit of uses.
410meeting_link_revokedThe meeting link was turned off or replaced, including for a guest who was already in the room.
413payload_too_largeThe body is larger than 512 KB.
415unsupported_media_typeThe body isn't JSON. Send Content-Type: application/json.
422validation_errorA field is missing, has the wrong type or breaks a limit.
422integration_rejectedOpenSync refused the token or the space.
429rate_limitedToo many requests. Wait a little and retry.
500internal_errorSomething failed on our side. Retry later, and include the requestId if you contact us.
502integration_unreachableOpenSync didn't answer, or its address leads to a private network.
503media_unavailableAudio rooms aren't available on this server.
503email_unavailableThe sign-in email couldn't be sent. Try again in a moment.

Rate limits

Requests are counted per client address, per endpoint and per minute. Over the limit, the answer is 429 rate_limited.

EndpointLimit per minute
Most endpoints300
POST /webhooks/inbound/:token60
POST /channels/:id/call/token20
POST /workspace/invites20
POST /workspaces10
GET /invites/token/:token (invite preview)30
POST /invites/token/:token/accept20
POST /workspace/invite-links20
GET /invite-links/:code (invite link preview)30
POST /invite-links/:code/join20
GET /meet/:code (meeting preview)60
POST /meet/:code/join10
POST /meet/:code/transcript12
POST /auth/email-code (sign-in codes)5, and at most 3 codes per email every 10 minutes

Pagination

Lists that can grow without end, like messages, are paginated with a cursor. Each page has a nextCursor; pass it as cursor to get the next (older) page, until nextCursor is null. See Message endpoints.

Endpoints

MethodPathWhat it does
GET/workspacesList your workspaces (people)
POST/workspacesCreate a workspace (people)
GET/workspace/membersList the people in the workspace
POST/workspace/invitesInvite someone by email (admins)
GET/workspace/invite-linksList invite links (admins) · New in November 2026
POST/workspace/invite-linksCreate an invite link (admins) · New in November 2026
DELETE/workspace/invite-links/:idTurn off an invite link (admins) · New in November 2026
GET/invite-links/:codePreview an invite link, no token · New in November 2026
POST/invite-links/:code/joinJoin with an invite link (people) · New in November 2026
GET/channelsList channels and direct messages you can see
POST/channelsCreate a channel
PATCH/channels/:idRename, describe, or switch voice, camera, screen and transcription
DELETE/channels/:idArchive a channel (admins, people only)
POST/channels/directOpen a direct message
GET/channels/:id/messagesList messages, newest page first
POST/channels/:id/messagesSend a message
PATCH/messages/:idEdit your message
DELETE/messages/:idDelete a message
POST/channels/:id/call/tokenGet a token to join the audio room (people)
GET/channels/:id/callSee who's in the audio room
GET/channels/:id/meeting-linkGet the channel's meeting link (people) · New in November 2026
POST/channels/:id/meeting-linkCreate or copy the meeting link (people) · New in November 2026
POST/channels/:id/meeting-link/resetReplace the meeting link (people) · New in November 2026
DELETE/channels/:id/meeting-linkTurn off the meeting link (people) · New in November 2026
POST/channels/:id/call/guests/:identity/removeRemove a guest from the audio room (people) · New in November 2026
GET/meet/:codePreview a meeting, no token · New in November 2026
POST/meet/:code/joinJoin a meeting as a guest, no token · New in November 2026
POST/meet/:code/transcriptSend a guest's audio for transcription (guest token) · New in November 2026
GET/channels/:id/webhooksList incoming webhooks (admins)
POST/channels/:id/webhooksCreate an incoming webhook (admins)
DELETE/channels/:id/webhooks/:webhookIdRevoke an incoming webhook (admins)
POST/webhooks/inbound/:tokenPost through an incoming webhook
GET/workspace/api-keysList API keys (admins) · New in November 2026
POST/workspace/api-keysCreate an API key (admins) · New in November 2026
DELETE/workspace/api-keys/:idRevoke an API key (admins) · New in November 2026
GET/agentsList AI agents (people) · New in November 2026
POST/agentsCreate an AI agent (admins) · New in November 2026
PATCH/agents/:idChange, pause or resume an agent (admins) · New in November 2026
DELETE/agents/:idDelete an agent (admins) · New in November 2026
POST/agents/:id/rotate-keyRotate an agent's API key (admins) · New in November 2026
POST/agents/:id/rotate-secretRotate an agent's webhook secret (admins) · New in November 2026
POST/agents/:id/testSend a test event to an agent (admins) · New in November 2026
GET/channels/:id/transcriptsList a channel's transcripts · New in November 2026
GET/transcripts/:idRead a transcript · New in November 2026
POST/transcripts/:id/routeChoose where a transcript goes (people) · New in November 2026
GET/integrationsList the workspace's integrations · New in November 2026
PUT/integrations/opensyncConnect OpenSync (admins) · New in November 2026
POST/integrations/:provider/testTest an integration (admins) · New in November 2026
DELETE/integrations/:providerDisconnect an integration (admins) · New in November 2026

GET /health needs no token and answers { "status": "ok", "version": "…" } while the API is up.

On this page