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/v1Authentication
Every request carries a bearer token:
Authorization: Bearer cwk_YOUR_KEY- Scripts, bots and AI agents use an API key (
cwk_…). EveryGETneeds thereadscope; creating or changing things needswrite. 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/:coderoutes 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-1a2b3c4d5e6fGET /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 Createdwith the new object. Deleting and actions without a result return204 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, never403, so the API doesn't reveal that it exists. (A whole workspace you're not in, named inX-Workspace-Id, answers403.) - Request IDs: every error carries a
requestId. Send your own in anX-Request-Idheader (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) andPOST /workspace/invitestake an optionallocale,en(the default),ptores, and send the email in that language. Any other value gets422. 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).
| Status | code | When |
|---|---|---|
| 400 | validation_error | The body isn't valid JSON. |
| 401 | unauthorized | No token, or an invalid, expired or revoked one. |
| 403 | forbidden | Not 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. |
| 404 | not_found | It doesn't exist, or you can't see it. |
| 409 | conflict | It clashes with something, for example a channel name that's taken or removing the last owner. |
| 409 | voice_disabled | Joining an audio room, or sharing a meeting link, in a channel with voice turned off. |
| 409 | waiting_for_host, meeting_full, meeting_unavailable | A 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. |
| 409 | transcript_busy, transcript_empty | Choosing where a transcript goes while it's still being made or decided, or when nothing was transcribed. |
| 409 | transcript_closed, transcription_off, no_audio_room | Sending audio for a transcript that's already closed, in a channel with transcription off, or with no audio room open. |
| 409 | no_webhook | Testing an AI agent, or rotating its webhook secret, when it has no webhook URL. |
| 410 | invite_expired, invite_unavailable | The email invite expired, was revoked or was already used. |
| 410 | invite_link_expired, invite_link_revoked, invite_link_used_up | The invite link expired, was turned off or reached its limit of uses. |
| 410 | meeting_link_revoked | The meeting link was turned off or replaced, including for a guest who was already in the room. |
| 413 | payload_too_large | The body is larger than 512 KB. |
| 415 | unsupported_media_type | The body isn't JSON. Send Content-Type: application/json. |
| 422 | validation_error | A field is missing, has the wrong type or breaks a limit. |
| 422 | integration_rejected | OpenSync refused the token or the space. |
| 429 | rate_limited | Too many requests. Wait a little and retry. |
| 500 | internal_error | Something failed on our side. Retry later, and include the requestId if you contact us. |
| 502 | integration_unreachable | OpenSync didn't answer, or its address leads to a private network. |
| 503 | media_unavailable | Audio rooms aren't available on this server. |
| 503 | email_unavailable | The 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.
| Endpoint | Limit per minute |
|---|---|
| Most endpoints | 300 |
POST /webhooks/inbound/:token | 60 |
POST /channels/:id/call/token | 20 |
POST /workspace/invites | 20 |
POST /workspaces | 10 |
GET /invites/token/:token (invite preview) | 30 |
POST /invites/token/:token/accept | 20 |
POST /workspace/invite-links | 20 |
GET /invite-links/:code (invite link preview) | 30 |
POST /invite-links/:code/join | 20 |
GET /meet/:code (meeting preview) | 60 |
POST /meet/:code/join | 10 |
POST /meet/:code/transcript | 12 |
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
| Method | Path | What it does |
|---|---|---|
GET | /workspaces | List your workspaces (people) |
POST | /workspaces | Create a workspace (people) |
GET | /workspace/members | List the people in the workspace |
POST | /workspace/invites | Invite someone by email (admins) |
GET | /workspace/invite-links | List invite links (admins) · New in November 2026 |
POST | /workspace/invite-links | Create an invite link (admins) · New in November 2026 |
DELETE | /workspace/invite-links/:id | Turn off an invite link (admins) · New in November 2026 |
GET | /invite-links/:code | Preview an invite link, no token · New in November 2026 |
POST | /invite-links/:code/join | Join with an invite link (people) · New in November 2026 |
GET | /channels | List channels and direct messages you can see |
POST | /channels | Create a channel |
PATCH | /channels/:id | Rename, describe, or switch voice, camera, screen and transcription |
DELETE | /channels/:id | Archive a channel (admins, people only) |
POST | /channels/direct | Open a direct message |
GET | /channels/:id/messages | List messages, newest page first |
POST | /channels/:id/messages | Send a message |
PATCH | /messages/:id | Edit your message |
DELETE | /messages/:id | Delete a message |
POST | /channels/:id/call/token | Get a token to join the audio room (people) |
GET | /channels/:id/call | See who's in the audio room |
GET | /channels/:id/meeting-link | Get the channel's meeting link (people) · New in November 2026 |
POST | /channels/:id/meeting-link | Create or copy the meeting link (people) · New in November 2026 |
POST | /channels/:id/meeting-link/reset | Replace the meeting link (people) · New in November 2026 |
DELETE | /channels/:id/meeting-link | Turn off the meeting link (people) · New in November 2026 |
POST | /channels/:id/call/guests/:identity/remove | Remove a guest from the audio room (people) · New in November 2026 |
GET | /meet/:code | Preview a meeting, no token · New in November 2026 |
POST | /meet/:code/join | Join a meeting as a guest, no token · New in November 2026 |
POST | /meet/:code/transcript | Send a guest's audio for transcription (guest token) · New in November 2026 |
GET | /channels/:id/webhooks | List incoming webhooks (admins) |
POST | /channels/:id/webhooks | Create an incoming webhook (admins) |
DELETE | /channels/:id/webhooks/:webhookId | Revoke an incoming webhook (admins) |
POST | /webhooks/inbound/:token | Post through an incoming webhook |
GET | /workspace/api-keys | List API keys (admins) · New in November 2026 |
POST | /workspace/api-keys | Create an API key (admins) · New in November 2026 |
DELETE | /workspace/api-keys/:id | Revoke an API key (admins) · New in November 2026 |
GET | /agents | List AI agents (people) · New in November 2026 |
POST | /agents | Create an AI agent (admins) · New in November 2026 |
PATCH | /agents/:id | Change, pause or resume an agent (admins) · New in November 2026 |
DELETE | /agents/:id | Delete an agent (admins) · New in November 2026 |
POST | /agents/:id/rotate-key | Rotate an agent's API key (admins) · New in November 2026 |
POST | /agents/:id/rotate-secret | Rotate an agent's webhook secret (admins) · New in November 2026 |
POST | /agents/:id/test | Send a test event to an agent (admins) · New in November 2026 |
GET | /channels/:id/transcripts | List a channel's transcripts · New in November 2026 |
GET | /transcripts/:id | Read a transcript · New in November 2026 |
POST | /transcripts/:id/route | Choose where a transcript goes (people) · New in November 2026 |
GET | /integrations | List the workspace's integrations · New in November 2026 |
PUT | /integrations/opensync | Connect OpenSync (admins) · New in November 2026 |
POST | /integrations/:provider/test | Test an integration (admins) · New in November 2026 |
DELETE | /integrations/:provider | Disconnect an integration (admins) · New in November 2026 |
GET /health needs no token and answers { "status": "ok", "version": "…" } while the API is up.