AI access: developer documentation

Let an AI assistant read what is happening in your workspace (bookings, spaces, members, companies and invoices) using the Model Context Protocol (MCP). Access is read-only by default and always limited to your own organization. A key can optionally also be allowed to create and edit records, but only after the AI has shown the change and a person has approved it.

What you get

  • One HTTPS endpoint that speaks MCP (Streamable HTTP, JSON responses, stateless).
  • Read-only by default. A new key can only look at data. Changing data needs a write permission that someone ticks on purpose when creating the key.
  • Changes need a person's approval. A key with write permissions can create bookings, members, companies and draft invoices, and edit or cancel bookings. By default the AI cannot apply anything itself: its request waits in an approval queue inside Cowork until an owner or admin clicks Approve. See Making changes.
  • Nothing can be deleted. There is no delete tool of any kind. Cancelling a booking only sets its status.
  • Your organization only. A key is bound to the organization that created it. Nothing a client sends can select another organization.
  • Least privilege. Each key carries permissions you choose. Tools a key is not allowed to use are not listed and cannot be called.

Quick start

  1. An owner or admin opens Settings → AI access, clicks Create key, names it, ticks the permissions it needs and picks an expiry.
  2. Copy the key immediately. It starts with cwk_ and is shown only once.
  3. Point your client at the endpoint below and send the key as a bearer token.
Endpoint:       https://cowork.get200.africa/api/mcp
Authentication: Authorization: Bearer cwk_your_key_here

If the screen says API access is not enabled, ask your platform administrator to switch on "API access" for your organization.

Connect a client

Claude Code

claude mcp add --transport http cowork https://cowork.get200.africa/api/mcp --header "Authorization: Bearer $COWORK_API_KEY"

Claude on the web, Claude Desktop and mobile (claude.ai)

  1. Open Customize → Connectors → Add custom connector (on Team and Enterprise plans an Owner adds it under Organization settings → Connectors).
  2. Set MCP server URL to https://cowork.get200.africa/api/mcp and Authentication to No sign-in.
  3. Under Request headers add the header name authorization with the value Bearer cwk_your_key_here (the word Bearer, a space, then the key). Alternatively use the header name x-api-key with just the key.
  4. Click Add, then switch the connector on from the + → Connectors menu in a chat.

The Request headers section is a beta that only some Claude organizations have. If you don't see it, your account doesn't have access yet. Claude can't change a connector's headers after it's added: remove the connector and add it again.

Any MCP client that supports Streamable HTTP with custom headers

Add a remote server with the endpoint above and the header Authorization: Bearer <key> (an X-API-Key: <key> header also works). Client configuration formats differ, so follow your client's documentation for remote HTTP servers.

TypeScript (official SDK)

import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"

const transport = new StreamableHTTPClientTransport(new URL("https://cowork.get200.africa/api/mcp"), {
  requestInit: { headers: { Authorization: `Bearer ${process.env.COWORK_API_KEY}` } },
})
const client = new Client({ name: "my-app", version: "1.0.0" })
await client.connect(transport)

const { tools } = await client.listTools()
const result = await client.callTool({
  name: "list_bookings",
  arguments: { hasNotes: true, limit: 5 },
})
console.log(result.content)

Plain HTTP (curl)

curl -s https://cowork.get200.africa/api/mcp \
  -H "Authorization: Bearer $COWORK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The Content-Type and Accept headers are both required (without Accept the server answers 406). Every request is independent, so there is no session to open or close.

Permissions

Each key carries one or more permissions. Read permissions only look at data. Write permissions (marked below) additionally allow the AI to change data, always through the approval step, and each one needs the reading that goes with it.

PermissionWhat it allowsTools it unlocks
organization:readOrganization name and currency, locations, and headline counts (members by status, upcoming bookings).get_organization_overview, list_locations
spaces:readRooms, desks and offices with capacity and hourly, daily and monthly rates.list_spaces
bookings:readBookings with times, status, amounts, the special notes bookers add, and requirement items. Each booking shows the booker's name (not their email or phone).list_bookings, get_booking
members:readThe member directory including email, phone and job title. Also lets bookings and invoices include each person's email and be searched by email.list_members
companies:readClient companies, their booking contact and contract dates.list_companies
invoices:readInvoices, line items, payments and totals by status. Each invoice shows the billed member's name (not their email).list_invoices, get_invoice, get_invoice_summary
bookings:writechanges dataCreate a booking for an existing member, reschedule or edit one, or cancel one. Uses the same availability, duration and pricing rules as the app. Sends no emails.Also needs: bookings:read, spaces:read, members:readcreate_booking, update_booking, cancel_booking
members:writechanges dataAdd new members or prospects and edit their name, phone, job title, status or company. Email addresses cannot be changed.Also needs: members:readcreate_member, update_member
companies:writechanges dataAdd client companies and edit their details and booking contact.Also needs: companies:readcreate_company, update_company
invoices:writechanges dataCreate DRAFT invoices from existing bookings and edit a draft's dates and notes. It can never send, pay, void or change the amounts of an invoice.Also needs: invoices:read, bookings:readcreate_draft_invoice, update_draft_invoice

Personal contact details are protected separately: without members:read, bookings and invoices show a person's name only, and searching by email is refused.

Making changes (the approval step)

Tools that change data (the ones marked changes data in the reference below) never change anything by being called. How a person approves depends on the key's approval mode, chosen when the key is created.

Approval in Cowork (default, recommended)

The call is checked exactly as the real change would be, then stored as a request in Settings → AI approvals. The AI is told it is waiting. An owner or admin sees what will happen and clicks Approve or Reject. Nothing the AI can send back will apply the change, so it is safe even if the AI client is set to "always allow".

{"name":"create_booking","arguments":{"spaceId":"...","memberEmail":"ann@example.com",
  "startTime":"2026-11-03T09:00:00+03:00","endTime":"2026-11-03T11:00:00+03:00"}}
// -> {"status":"pending_approval","changesMade":false,"requestId":"...","summary":"Create a hourly booking of ... Total KES 2,000.00 ..."}

// later, to see what happened
{"name":"check_change_request","arguments":{"requestId":"..."}}
// -> {"status":"APPROVED","changesMade":true,"result":{"bookingId":"..."}}   (or REJECTED with the reason, EXPIRED, FAILED)
  • The change is checked again at the moment of approval. If the record, its price or the space's availability changed since the AI asked, it is refused and nothing is saved.
  • The person who approves is recorded as the author of the change, and the key as the requester.
  • Requests expire after 24 hours. A key can have at most 20 waiting, and make at most 60 requests per hour. Repeating an identical request returns the one already waiting.
  • If the key is revoked or expires while a request is waiting, the request can no longer be approved.
  • check_change_request only shows requests made by the same key.

Webhooks: get told instead of polling

When a key is created with a webhook URL, Cowork sends a signed POST to it as soon as a person approves or rejects one of that key's requests, or when approving it fails the final check. The AI platform no longer needs to call check_change_request in a loop. The URL must be a public https address on the standard port. The signing secret (whsec_…) is shown once, next to the key.

POST https://your-platform.example.com/hooks/cowork
Content-Type: application/json
X-Cowork-Event: change_request.approved      (or change_request.rejected, change_request.failed)
X-Cowork-Delivery: 5d0b…                      (unique per delivery attempt series)
X-Cowork-Signature: t=1767225600,v1=9a3f…

{"id":"evt_<requestId>","type":"change_request.approved","createdAt":"2026-11-03T09:14:02.000Z",
 "data":{"requestId":"...","tool":"create_booking","summary":"...","status":"APPROVED",
         "changesMade":true,"result":{"bookingId":"..."}}}

Verify every message before trusting it. The signature is an HMAC-SHA256, in hex, of the timestamp, a dot and the raw request body, using your secret:

import crypto from "crypto"

function verify(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false   // too old: possible replay
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
  return v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}
  • Answer with any 2xx status. Anything else, or no answer within 5 seconds, is retried (up to 3 tries in all, over about 5 seconds). The event id stays the same on retries, so treat it as idempotent.
  • If every try fails, nothing is lost: the request keeps its outcome and check_change_request still returns it. The failure is shown in Settings → AI approvals.
  • Events are sent for approved, rejected and failed requests. A request that simply expires sends nothing; check_change_request reports it as EXPIRED.
  • Redirects are not followed, and addresses on private networks are refused.

Approval in the AI chat

Keys created with this mode work in two calls. The first call changes nothing and returns status: "needs_approval", a summary, the preview and a confirmationToken. The AI shows the summary to the person and, only if they explicitly say yes, calls the same tool again with the same arguments plus the confirmationToken. The result is status: "done".

  • The token is signed, expires after 10 minutes, and is tied to the key, the tool and the exact arguments.
  • If the arguments differ from the preview, or the record, its price or availability changed since, the change is refused.
  • A key can make at most 30 changes per hour.

The limit of this mode: the server can prove a preview was requested and echoed back, not that a person read it. An AI client set to "always allow" can approve on the person's behalf. Prefer approval in Cowork.

In both modes

  • It uses the same rules as the app: no double-booking, duration limits, the same pricing, and price changes flow onto invoices. It sends no emails.
  • Invoices can only be created as drafts and only drafts can be edited. Sending, paying, voiding and changing amounts always stay with a person in the app.
  • Changing a member's company, deleting anything, and changing email addresses are not possible through this API.
  • Every applied change is written to Settings → Audit logs with what changed, and the key that asked for it (as mcp:cwk_…).
  • Give write permissions only to keys held by people you trust.

Tool reference

List endpoints return at most 100 rows per call (default 25); page with offset. Dates are ISO 8601. Amounts are returned as strings to avoid rounding.

get_organization_overview

organization:read

High-level snapshot: locations, spaces, member counts by status, upcoming confirmed bookings, and bookings that carry special notes.

No parameters.

list_locations

organization:read

Locations (buildings/branches) of this organization.

ParameterTypeDescription
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

list_spaces

spaces:read

Bookable spaces (rooms, desks, offices) with capacity and rates.

ParameterTypeDescription
locationId?stringOnly spaces in this location
type?PRIVATE_OFFICE | MEETING_ROOM | CONFERENCE_ROOM | DAY_PASS | BOARD_ROOM | MONTHLY_HOTDESK | VIRTUAL_OFFICE | PODCAST
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

list_bookings

bookings:read

Bookings overlapping an optional time window, newest or soonest first. Each booking includes the booker's special notes (`notes`) and requirement items.

ParameterTypeDescription
from?stringISO 8601; only bookings ending after this moment
to?stringISO 8601; only bookings starting before this moment
status?CONFIRMED | CANCELLED | COMPLETED
locationId?string
spaceId?string
memberEmail?stringExact email of the booker (requires the members:read permission)
hasNotes?booleantrue = only bookings with special notes
search?stringCase-insensitive text match on title and notes
newestFirst?booleanSort by start time descending (default ascending)
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

get_booking

bookings:read

One booking by id, including special notes and requirement items.

ParameterTypeDescription
bookingIdstring

list_members

members:read

Members and contacts (name, email, phone, status, company). Tax ids and internal notes are not exposed.

ParameterTypeDescription
search?stringCase-insensitive match on name, email or company
status?PROSPECT | ACTIVE | DROP_IN | FORMER
companyId?string
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

list_companies

companies:read

Client companies with member counts.

ParameterTypeDescription
search?stringCase-insensitive match on company name
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

list_invoices

invoices:read

Invoices filtered by status, issue-date range, member email or company.

ParameterTypeDescription
status?DRAFT | SENT | PAID | OVERDUE | CANCELLED | PARTIALLY_PAID
issuedFrom?stringISO 8601; issue date on or after
issuedTo?stringISO 8601; issue date before
memberEmail?stringExact email of the billed member (requires the members:read permission)
companyId?string
limit?integerMax rows to return (default 25, max 100)
offset?integerRows to skip, for paging

get_invoice

invoices:read

One invoice by id or invoice number, with line items and payments.

ParameterTypeDescription
invoiceId?string
invoiceNumber?string

get_invoice_summary

invoices:read

Invoice counts and totals grouped by status and currency for an optional issue-date range.

ParameterTypeDescription
issuedFrom?stringISO 8601; issue date on or after
issuedTo?stringISO 8601; issue date before

create_booking

changes databookings:write

Create a confirmed booking for an existing member (or a company's representative) in a space. Uses the app's own availability, duration and pricing rules; sends no emails. Find ids with the read tools first. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
spaceIdstring
startTimestringISO 8601 with a timezone offset, e.g. 2026-11-03T09:00:00+03:00
endTimestringISO 8601 with a timezone offset
memberId?stringWho the booking is for. Give memberId, memberEmail or companyId.
memberEmail?stringExact email of an existing member
companyId?stringBook for a company; a representative member is chosen if no member is given
title?string
notes?stringSpecial notes from the booker
quantity?integer
pricingType?HOURLY | HALF_DAY | DAILY | MONTHLYDefaults to hourly when the space has an hourly rate
floorId?stringRequired when the space is on several floors
requirementItems?array
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

update_booking

changes databookings:write

Change a confirmed booking's title, notes, times, space, quantity or requirement items. Changes that affect price are recalculated and, if the booking is on an invoice, pushed onto it. Cancelled bookings cannot be edited. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
bookingIdstring
title?anynull clears the title
notes?anynull clears the notes
startTime?stringISO 8601; send together with endTime
endTime?stringISO 8601; send together with startTime
spaceId?stringMove the booking to another space
floorId?string
quantity?integer
requirementItems?arrayReplaces the whole list
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

cancel_booking

changes databookings:write

Cancel a confirmed booking. This only sets its status to cancelled (it is not deleted) and can be reversed from the booking screen. It does not change any invoice. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
bookingIdstring
reason?stringRecorded in the audit log
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

create_member

changes datamembers:write

Add a new member or prospect. Refused if a member with that email already exists. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
firstNamestring
lastNamestring
emailstring
phonestring
status?PROSPECT | ACTIVE | DROP_IN | FORMERDefaults to PROSPECT
companyId?string
jobTitle?string
locationId?stringRequired when the organization has more than one location
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

update_member

changes datamembers:write

Change a member's name, phone, job title or status. Email addresses and company membership cannot be changed this way (moving someone between companies also moves contracts, so that stays in the app). This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
memberIdstring
firstName?string
lastName?string
phone?string
jobTitle?anynull clears it
status?PROSPECT | ACTIVE | DROP_IN | FORMER
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

create_company

changes datacompanies:write

Add a client company. Refused if a company with the same name already exists. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
namestring
industry?string
size?STARTUP | SMALL | MEDIUM | LARGE | ENTERPRISE
website?string
description?string
bookingContactName?string
bookingContactEmail?string
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

update_company

changes datacompanies:write

Change a company's details or booking contact. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
companyIdstring
name?string
industry?any
size?any
website?any
description?any
bookingContactName?any
bookingContactEmail?any
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

create_draft_invoice

changes datainvoices:write

Create a DRAFT invoice from existing, uninvoiced bookings of one member or company. The invoice is never sent and never marked paid; a person finalizes it in the app. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
bookingIdsarray
dueInDays?integerDays until due, default 30
notes?string
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

update_draft_invoice

changes datainvoices:write

Change a DRAFT invoice's issue date, due date or notes. Only drafts can be edited, and amounts can never be changed this way. This is a two-step change. The first call only returns a preview and a confirmationToken and changes nothing: show the preview to the user, get their explicit approval, then call again with the same arguments plus the confirmationToken.

ParameterTypeDescription
invoiceIdstring
issueDate?stringISO 8601 date
dueDate?anyISO 8601 date; null clears it
notes?anynull clears the notes
confirmationToken?stringLeave empty on the first call to get a preview. After the user approves the preview, call again with the same arguments and this token.

Errors and limits

Limits are per API key unless stated, and are enforced by the server.

LimitValueWhat happens beyond it
Requests of any kind120 per minuteHTTP 429 with a Retry-After header
Rows per list call100 (default 25)Use offset to page
Change requests waiting for approval20 at onceThe tool call fails until a person approves or rejects some
New change requests60 per hourThe tool call fails until the hour has passed
How long a request can be approved24 hoursIt shows as EXPIRED and cannot be approved
Changes in chat-approval mode30 per hour; token valid 10 minutesThe tool call fails
Webhook delivery3 tries, 5 s eachMarked failed in Settings → AI approvals; the outcome stays available via check_change_request
Active keys per organization10 (all keys together)Creating another is refused until one is revoked
Time for one call30 secondsThe call is cut off

Responses you may see:

ResponseMeaning
401Missing, invalid, expired or revoked key, or the person who created it was deactivated.
403API access is not enabled for the organization, or the organization is suspended.
405Only POST is supported.
429More than 120 requests per minute for this key. Retry after the number of seconds in the Retry-After header.
200 with isErrorThe call reached the tool but failed (for example a record that does not exist, or a filter the key is not permitted to use). The message explains why.

Security and responsibility

  • Treat a key like a password. Store it in an environment variable or a secrets manager. Never put it in source code, a shared chat or a browser. We store only a hash, so a lost key cannot be recovered: revoke it and create another.
  • Give each tool its own key with only the permissions it needs, and choose an expiry. Revoke a key in Settings → AI access and it stops working immediately.
  • Everything is logged. Each call is recorded in Settings → Audit logs (filter by "API key") with the key, the tool and the arguments used.
  • Data leaves Cowork when you connect an AI. Anything the key can read is sent to whichever AI provider runs the assistant. You decide which providers to connect and which permissions to grant, and you are responsible for that choice, including any personal data of your members that you allow the key to read.
  • Changes are opt-in, approved and limited. Keys read only unless a write permission was ticked. Writes need the approval step, can never delete anything, cannot send emails, take payments or finalize invoices, and are all logged. You are responsible for who holds a key that can change data.