MavenOS API
The MavenOS API lets a script or another system read and change tasks and projects, and add or update sales companies, contacts and deals, in one workspace, as the person or service the key belongs to. Send `Authorization: Bearer mos_<prefix>_<secret>`. Every POST and PATCH needs an `Idempotency-Key` uuid. Bodies are JSON, at most 64 KB (32 KB on the Sales calls). There are no deletes, and nothing about HR is ever returned. Money is returned only as deal amounts (`one_time_cents`, `monthly_cents`), and only to a key that acts as an owner, admin or finance person. Per-key limits by rate class: basic: 60 reads and 20 writes a minute, 300 writes a day; standard: 120 reads and 30 writes a minute, 2000 writes a day; scale: 300 reads and 60 writes a minute, 10000 writes a day. Hard caps, no overage. A day is the UTC calendar day: the daily write count starts again at 00:00 UTC, and the 80 and 100 percent notices follow the same day. The per-minute limits are a rolling minute. A 429 for the daily cap sets Retry-After to the seconds left until 00:00 UTC. Writes need the API access add-on and a key with the `work:write` scope (or `crm:write` for the sales calls, which read with `crm:read`). Workspaces that require two-step sign-in refuse API keys. Browsers cannot call the API directly (no CORS); call it from a server.
Connect Claude or another MCP client to your MavenOS workspace with an Assistant key, or call REST API v2 from your own server. Either way, the connection acts as one person and sees only what that person can see.
Connect with MCP
MavenOS runs an MCP (Model Context Protocol) server at `/api/mcp`. It uses MCP over Streamable HTTP and is stateless: every request is a single `POST` with a JSON-RPC message, and every answer is a single JSON body. There are no sessions and no streams, and `GET` and the other methods return `405`. It supports `initialize`, `ping`, `tools/list` and `tools/call`. Batches are not supported.
The endpoint is on your MavenOS address: `https://os.mavenx.co/api/mcp`. The Assistant page in MavenOS shows the exact address for your workspace.
Make an Assistant key
Open the Assistant page in MavenOS and create a key. Give it a name, choose how long it lasts, and decide whether to turn on Allow changes. Allow changes is off by default and is not available to viewers. The key is shown once, when you create it, so copy it straight away.
A workspace owner can turn Assistant keys off for a role. When that happens, keys made by people in that role stop working.
Add MavenOS to Claude
Send the key as a Bearer token in the `Authorization` header. In Claude Code, run this in a terminal with your own address and key:
claude mcp add --transport http mavenos https://os.mavenx.co/api/mcp \
--header "Authorization: Bearer YOUR_ASSISTANT_KEY"Cursor and other clients that accept a remote server with headers use the same address and header:
{
"mcpServers": {
"mavenos": {
"url": "https://os.mavenx.co/api/mcp",
"headers": { "Authorization": "Bearer YOUR_ASSISTANT_KEY" }
}
}
}Claude Desktop starts remote servers through `mcp-remote`. Keep the key in an environment variable so the space in `Bearer ...` is not split:
{
"mcpServers": {
"mavenos": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://os.mavenx.co/api/mcp", "--header", "Authorization:${MAVENOS_AUTH}"],
"env": { "MAVENOS_AUTH": "Bearer YOUR_ASSISTANT_KEY" }
}
}
}To check the connection from a terminal, call `initialize`. A wrong key fails here, at connect time:
curl -X POST https://os.mavenx.co/api/mcp \
-H "Authorization: Bearer YOUR_ASSISTANT_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}'The server accepts protocol versions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. Ask Claude something like "What is on my plate today?" once it is connected.
What a key can see
Reads run as the key's person under their own access rules. A member sees their own work, and Sales only if they have it. A private task, its comments and the notes about it stay hidden from everyone except the people the app shows them to. A record the person cannot see is reported as not found.
Money is shown only to owners, admins and finance. For anyone else, deal amounts come back as `null` (and the pipeline shows counts without totals), the invoice list is empty, and a request that sets deal amounts is refused.
A key cannot read HR records, pay, write-ups, reviews, incidents, contracts, private notes or free-text notes, and it cannot change HR, pay or money. It cannot reply to customers, send quotes or email, publish, or approve anything. A tool for a part of MavenOS that is switched off in your workspace is left out of the tool list.
Changes are available only when the key was created with Allow changes and its person is not a viewer or otherwise read-only. Nothing can be deleted except a project phase, and only after the person confirms. Articles are only ever created and edited as drafts. Each change is logged against the key.
Read tools
These tools only read. Lists return at most `limit` rows (1 to 50, default 15) with `has_more` and `next_cursor`; pass `next_cursor` back as `cursor` for the next page. The older short lists (marked below) take only `limit`.
| Tool | What it does |
|---|---|
| whoami | Says which workspace the key belongs to, whose access it uses and that person's role. |
| search | Searches clients, contacts, deals, projects, tasks, articles, expenses, support tickets and people by name or text. |
| get_record | Reads one record found by search: client, contact, deal, project, task, article, ticket, person or expense. |
| list_tasks | Lists tasks, soonest due first. Filter by project, assignee ("me" or a person id), status, due dates and title text. |
| list_tasks_due | Lists open tasks due within some days, or overdue, with project and assignees (short list). |
| list_task_comments | Lists the comments on one task, oldest first. |
| list_projects | Lists projects with status, client and dates, so you can find a project id (short list). |
| get_project_detail | Gets one project with its phases, task counts, members and links. Budgets and rates are not included. |
| list_project_notes | Lists the notes on one project, pinned first. |
| list_project_templates | Lists the project templates, with phase and task counts. |
| list_clients | Lists the workspace's clients by name. |
| list_people | Lists the people in the workspace with name and role. No pay, HR or contact records (short list). |
| list_companies | Lists Sales companies with owner, contact count and open-deal count. |
| list_contacts | Lists Sales contacts, including do_not_email and do_not_call flags. |
| list_deals | Lists Sales deals, most recently changed first. Amounts are null unless the person may see money. |
| list_open_deals | Lists deals still in play, with stage and priority (short list). |
| pipeline_by_stage | Shows every pipeline stage with its deal count, and totals only for owner, admin or finance. |
| list_tickets | Lists support tickets with status, priority, requester, assignee and deadline. |
| list_open_tickets | Lists open, in-progress and waiting tickets, most urgent first (short list). |
| get_ticket_messages | Reads one ticket's thread, oldest first. Attachments are not included. |
| list_articles | Lists knowledge articles the person may read. Drafts appear only for people who can write articles. |
| list_content_items | Lists the content calendar for a date range. Bodies and briefs are not included. |
| my_inbox | Lists what waits on the person in Needs you: approvals, mentions, reviews, tickets and tasks. It never carries an amount. |
| get_my_time | Shows the person's own logged time, totals and any running timer, in minutes only. Never anyone else's time, and no rates or pay. |
| list_time_off_pending | Lists time off requests waiting for a decision that the person may see. Never the reason (short list). |
Money tools
These tools read financial data. They return data only to people who can read Finance in your workspace: owners, admins and finance by default, and anyone who has been given Finance read access. For everyone else the answer is empty or says the figures are not available.
| Tool | What it does |
|---|---|
| list_invoices | Answers "who owes me money": open Stripe invoices with amount left in cents, due date and whether each is late. Only people who can read Finance get rows. |
| list_expenses_to_review | Lists filed expenses waiting for approval, with vendor and amount in cents (short list). |
| get_safe_to_withdraw | Says how much the owner can safely take out of the business, as plain lines in cents. The tax line is an estimate, not tax advice. |
Write tools
These tools appear only for a key made with Allow changes. Each accepts an optional `request_id` (a UUID you make up): send the same one again and the change is made once, with the first answer returned. The person's role and the app's own rules decide each call, and none of these tools deletes anything except `delete_project_phase`.
| Tool | What it does |
|---|---|
| create_task | Creates one task, with optional description, project, phase, priority, due date, client, team, estimate and assignees. Assigning people notifies them. |
| update_task | Changes title, description, priority, due date or estimate on one task. Send null to clear a clearable field. |
| set_task_status | Sets a task to open, in_progress, done or cancelled. |
| assign_task | Adds one person as an assignee and notifies them. It never removes anyone. |
| add_task_comment | Adds one comment to a task, written as the key's person. |
| create_project | Creates one project with a name and optional client, dates and status. It adds no phases or tasks. |
| update_project | Changes name, client, dates or status of one project. |
| add_project_note | Adds one note to a project, optionally tied to a task or phase and pinned. |
| add_project_phase | Adds a phase at the end of a project. Not gating a phase is for owners and admins. |
| update_project_phase | Renames a phase or changes whether it is gated. Changing gated is for owners and admins. |
| delete_project_phase | Deletes one phase; its tasks are kept. Owners and admins only, and it requires confirm: true after the person agrees. |
| unlock_project_phase | Opens a locked phase early. Owners and admins only, and it requires confirm: true. |
| set_phase_tasks_status | Marks every open task in a phase done, or reopens every done task. Tasks the person cannot change are skipped. |
| create_project_from_template | Creates an active project from a project template, copying its phases and tasks. |
| create_article | Creates one knowledge article as a draft. It is never published; a person publishes it in Knowledge. |
| update_article | Changes fields of a draft article. Published or archived articles are refused. |
| create_company | Adds one Sales company. Needs Sales access. |
| update_company | Changes fields of one company. It cannot archive or restore a company. |
| create_contact | Adds one Sales contact. It never changes Do Not Email or Do Not Call and sends nothing. |
| update_contact | Changes fields of one contact. It never changes Do Not Email or Do Not Call. |
| create_deal | Creates one Sales deal in an open stage. Setting amounts needs an owner, admin or finance person. |
| update_deal | Changes fields of one deal other than its stage. Setting amounts needs an owner, admin or finance person. |
| move_deal_stage | Moves a deal to another stage. Moving to won or lost, or reopening, is for owners and admins; lost needs a reason. |
| log_interaction | Records a call, email, meeting or other contact in Sales history. It sends nothing. |
| create_ticket | Creates a support ticket by hand with a subject, a body and a requester. It sends no email. |
| add_ticket_note | Adds an internal note to a ticket. Never sent to the customer. |
| set_ticket_status | Moves a ticket to open, waiting_on_client, in_progress, resolved or closed. |
| set_ticket_priority | Sets a ticket's priority to low, normal or high. |
| assign_ticket | Gives a ticket to a teammate with Support access, or removes the assignee with null. |
| log_time | Logs one time entry for the key's own person, as minutes or a start and end time. Weeks already submitted or approved are refused. |
| start_timer | Starts the person's timer on a project. Only one timer can run. |
| stop_timer | Stops the running timer and logs the time. A timer under 30 seconds logs nothing. |
Limits and errors
| Limit | Value |
|---|---|
| Reads | 60 calls a minute per key |
| Changes | 20 a minute and 300 a day per key |
| Request body | 16 KB |
| Answer size | About 100,000 characters; a larger answer is replaced by a message asking for fewer rows |
| Page size | 1 to 50 rows (search takes 1 to 10 per kind) |
| HTTP status | Meaning |
|---|---|
| 401 | The key is missing, malformed, revoked or expired. The response carries a `WWW-Authenticate` header. |
| 403 | The key's person can no longer use Assistant keys (removed, changed role, or keys turned off for the role), the request carried a browser `Origin`, or the person needs a second sign-in step in a workspace that requires one. |
| 405 | Anything other than `POST`. |
| 413 / 415 | The body is over 16 KB, or the content type is not `application/json`. |
| 400 | The body is not valid JSON, or it is a batch. |
| 429 | Too many requests. Wait the number of seconds in `Retry-After`. |
| 503 | Temporarily unavailable. Try again. |
A tool that fails returns a normal JSON-RPC answer with `isError: true` and a plain sentence: access refused, not found, a part of MavenOS switched off, an invalid argument, or a change refused (not allowed, workspace read-only, or too many changes). When a change cannot be confirmed, check whether it was made before you retry, or send it again with the same `request_id`.
Using the REST API
REST API v2 lets a script or another system read and change tasks and projects, list people, and read and upsert Sales companies, contacts and deals in one workspace. Every call acts as one workspace member (the person who made the key, or a service person made for it), and that person's role decides what it can do. There are no deletes, and nothing about HR is returned. The endpoint reference on this page is generated from MavenOS's own OpenAPI description.
Paths are scoped to a workspace: `https://os.mavenx.co/api/v2/YOUR_WORKSPACE/...`. The API access page in MavenOS shows the exact address with your workspace name filled in.
export MAVENOS_API_KEY="YOUR_API_KEY"
export MAVENOS_ROOT="https://os.mavenx.co/api/v2/YOUR_WORKSPACE"
# Who is this key?
curl -H "Authorization: Bearer $MAVENOS_API_KEY" "$MAVENOS_ROOT/me"
# Create a task
curl -X POST "$MAVENOS_ROOT/tasks" \
-H "Authorization: Bearer $MAVENOS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Follow up with the client"}'REST keys and scopes
API keys are separate from Assistant keys. Owners and admins create them under Settings, API access. A key looks like `mos_<prefix>_<secret>` and is shown once. Send it as `Authorization: Bearer ...`; keys in the address are not accepted. A key works only in its own workspace.
Work calls use the `work:read` and `work:write` scopes. Sales calls use `crm:read` and `crm:write`. Writes also need the API access add-on. Workspaces that require two-step sign-in refuse API keys. Deal amounts are returned and accepted only when the key acts as an owner, admin or finance person.
Pagination, idempotency and rate limits
Lists answer `{ "data": [...], "next_cursor": ... }`. Use `limit` (default 50, at most 100) and pass `next_cursor` back as `after`. The cursor is opaque. Many lists also accept `updated_since`.
Every `POST` and `PATCH` needs an `Idempotency-Key` header: a UUID, or 8 to 64 letters, digits, underscores or hyphens. Repeat a call with the same key within 24 hours and nothing is written twice; you get the first answer back with `Idempotent-Replayed: true`. Reusing a key for a different call or body returns `422 idempotency_conflict`. Optionally send `MavenOS-Version: 2026-10-21`; it is the only accepted value.
| Rate class | Reads a minute | Writes a minute | Writes a day |
|---|---|---|---|
| basic | 60 | 20 | 300 |
| standard | 120 | 30 | 2,000 |
| scale | 300 | 60 | 10,000 |
Each key has one rate class. These are hard caps with no overage. The daily count resets at 00:00 UTC. Every answer carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a `429` carries `Retry-After` in seconds. JSON bodies are limited to 64 KB (32 KB on the Sales calls).
REST errors
Errors have the shape `{ "error": { "code": "...", "message": "...", "request_id": "..." } }`, with a `fields` list on `bad_request` that names each input and its problem (`required`, `invalid` or `unknown_field`). Quote the `request_id` when you ask for help.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request: unknown field, value out of range, bad date, cursor or filter. |
| 400 | idempotency_key_required | A POST or PATCH had no valid Idempotency-Key. |
| 400 | unsupported_version | MavenOS-Version is not 2026-10-21. |
| 401 | invalid_key | Missing, wrong, revoked or expired key, or a different workspace. The answer is the same for every cause. |
| 403 | forbidden | The key or its person may not do this. |
| 403 | module_off | Work, People, Sales or the API access add-on is off for this workspace. |
| 403 | two_factor_required | The workspace requires two-step sign-in, which API keys cannot provide. |
| 403 | origin_not_allowed | The request came from a browser page. |
| 403 | money_not_allowed | A deal call sent amounts but the key's person is not an owner, admin or finance. |
| 404 | not_found | No such path or record, or a record the key cannot see. |
| 405 | method_not_allowed | The path exists but not with that method. Nothing can be deleted through the API. |
| 409 | workspace_read_only | The workspace or add-on is read-only. Reads still work. |
| 409 | ambiguous_match | A Sales upsert matched more than one record. Send the external_id of the one you mean. |
| 413 | payload_too_large | The body is over the size limit. |
| 415 | unsupported_media_type | Send application/json. |
| 422 | idempotency_conflict | That Idempotency-Key was used for a different request. |
| 429 | rate_limited | Wait Retry-After seconds. |
| 500 / 503 | internal / unavailable | Retry with the same Idempotency-Key. |
Webhooks
With the API access add-on, MavenOS can post a signed JSON message to an https address you choose when something happens. Owners and admins manage webhooks under Settings, Integrations; finance can subscribe only to the money events. Events: `lead.created`, `contact.created`, `deal.created`, `deal.stage_changed`, `deal.won`, `deal.lost`, `task.completed`, `ticket.created`, `invoice.paid` and `invoice.overdue`, plus `webhook.test` from the Send test button.
Each delivery carries `MavenOS-Event-Id`, `MavenOS-Event-Type`, `MavenOS-Delivery-Id`, `MavenOS-Timestamp` and `MavenOS-Signature` (for example `v1=...`, an HMAC-SHA256 over the timestamp and body, keyed with the signing secret shown once when you create the webhook). Only the timestamp and body are signed, so read the event id and type from the body.
Delivery is at least once, so deduplicate on the `id` in the body. A `2xx` answer within 8 seconds is success. Failed attempts are retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then dropped after the eighth attempt. A `410` answer switches the webhook off at once.
Workspace
1 endpointReturns the key's name, scopes, expiry, the person it acts as, the workspace, which tools are on, the rate limits that apply, and whether a write would be accepted right now (`can_write`). A good first call to test a key.
Tasks
8 endpointsTasks the key's person can see, newest change first. Private tasks appear only when that person created them, is assigned, or is an owner or admin.
Pass as `after` for the next page. Null on the last page.
Creates one task as the key's person. It is public to the workspace unless `visibility` is `private`. Assigning people notifies them.
One task. A task the person cannot see answers 404, the same as one that does not exist.
Overwrites only the fields you send. Send null for `description`, `due_on` or `estimate_minutes` to clear it. It does not change status, assignees or who can see the task; use the status and assignees calls for those.
Comments on one task, oldest first.
Pass as `after` for the next page. Null on the last page.
Adds one comment, written as the key's person. A comment cannot be edited or deleted through the API.
Sets the status. Sending the status it already has changes nothing.
Adds one workspace member as an assignee and notifies them. It never removes anyone.
Projects
6 endpointsProjects the key's person can see, newest change first.
Pass as `after` for the next page. Null on the last page.
Creates one project as the key's person. It adds no phases or tasks.
One project.
Overwrites only the fields you send. Send null for `client_id`, `start_date` or `end_date` to clear it.
Notes on one project, newest first. Notes about a task the person may not read are left out.
Pass as `after` for the next page. Null on the last page.
Adds one note, written as the key's person, optionally tied to a task or phase of that project and pinned. A note cannot be edited or deleted through the API.
Sales
11 endpointsThe workspace's pipeline stages in board order, including archived ones, so you can see which stage keys exist. Needs a key with the `crm:read` scope. The key's person must be able to read Sales.
Pass as `after` for the next page. Null on the last page.
The workspace's companies, oldest change first, so a sync can run forward and never miss one: pass `updated_since` (the `updated_at` of the last record you saved; it is inclusive, so one record can repeat) and `after` (the `next_cursor` of the previous page). `updated_since` must be a real moment between 2000 and tomorrow. Needs a key with the `crm:read` scope; the key's person must be able to read Sales. `external_id` is the id your own integration sent when it created the company, or null. Companies carry no money.
Pass as `after` for the next page. Null on the last page.
One company, with the same fields as the list. A company the key's person cannot read answers 404, the same as one that does not exist.
The workspace's contacts, oldest change first, paged and filtered the same way as the company list (`company_id` keeps one company's contacts). The email address is included because the contact upsert matches on it; phone numbers, notes and addresses are never returned. Contacts carry no money.
Pass as `after` for the next page. Null on the last page.
One contact, with the same fields as the list. A contact the key's person cannot read answers 404.
The deals the key's person can see (a member limited to their own deals sees only those), oldest change first, paged and filtered like the company list; `stage` keeps one stage key from GET /crm/stages. `one_time_cents` and `monthly_cents` are present only when the key acts as an owner, admin or finance person; for anyone else the two fields are left out of every deal, not set to zero. Owner and agent names are never returned.
Pass as `after` for the next page. Null on the last page.
One deal, with the same fields and the same money rule as the list. A deal the key's person cannot see answers 404.
Finds the company by your `external_id`, then by exact name (not case sensitive), and creates it when none matches. `on_match` says what a match does: `update` (the default) overwrites the fields you send, `fill_blanks` only fills fields that are empty, `skip` changes nothing. The answer is 201 with `match: created` for a new company and 200 with `match: updated` or `unchanged` otherwise. If two or more companies match, nothing is changed and the answer is 409 `ambiguous_match`: send the `external_id` of the one you mean. Needs the `crm:write` scope and the API access add-on. Deals and their money are separate calls.
Finds the contact by your `external_id`, then by email (not case sensitive), and creates it when none matches. Send an `external_id` or an `email`, or both. Link the contact to a company with `company_id` or with `company_external_id` (the `external_id` you gave when you upserted the company), not both. `on_match` works as for companies. The answer is 201 with `match: created` for a new contact and 200 with `match: updated` or `unchanged` otherwise. If two or more contacts match, nothing is changed and the answer is 409 `ambiguous_match`. Do Not Email and Do Not Call are never changed through the API. Needs the `crm:write` scope and the API access add-on. Deals and their money are separate calls.
Finds the deal by your `external_id` (required; a deal is never matched by name) and creates it when none matches. `name` and `stage` are used only when the deal is created: for a deal that already exists they are ignored, so use the stage call to move it. Link the deal to a company with `company_id` or with `company_external_id` (the `external_id` you gave the company), not both. `on_match` works as for companies. The answer is 201 with `match: created` for a new deal and 200 with `match: updated` or `unchanged` otherwise. Money: `one_time_cents` and `monthly_cents` are whole cents (integers from 0 to 1000000000000). Sending either needs a key that acts as an owner, admin or finance person; for anyone else the answer is 403 `money_not_allowed` and nothing is changed. For those people the amounts are also left out of the deal in the answer. Won, lost and not applicable go through the stage call. Needs the `crm:write` scope and the API access add-on.
Moves one deal, named by its `deal_id` or by your `external_id` (send exactly one of them), to the stage with the key you give. Moving between open stages works for anyone who can write in Sales. Moving a deal to won, lost or not applicable, or reopening a closed deal, needs a key that acts as an owner or admin; for anyone else the answer is 403 `forbidden`. Moving to lost needs a `reason` (up to 500 characters). The answer is 200 with the deal as it now stands. A deal already in that stage is left as it is. A deal that does not exist, or belongs to another workspace, answers 404. Needs the `crm:write` scope and the API access add-on.
People
1 endpointWorkspace members by name and role, so you can pick an assignee. `name` is the full name; `first_name` and `last_name` are the same name in two parts, `null` when a person has no split yet. Nothing else about a person is shown.
Pass as `after` for the next page. Null on the last page.