# Call Workspace369 over HTTP (Builder API)

> Call Workspace369 from apps built with Lovable, v0, Bolt or Replit over HTTPS and JSON, with a URL per resource, an OpenAPI spec and MCP permissions.

- Product: Workspace369
- Documentation version: 2026.3
- Section: Build with AI
- Last updated: 2026-10-20
- Canonical source: https://docs.workspace369.com/v/2026.3/build-with-ai/call-workspace369-over-http-builder-api/

---

Apps built with Lovable, v0, Bolt or Replit can't connect to an MCP server while they run. They call HTTP. The Builder API gives them the same Workspace369 tools as the MCP server, over plain HTTPS and JSON.

It runs the same tools as MCP, so it has the same permissions, approvals, audit log and usage. A call made through it is billed exactly like the same call made over MCP.

## Who can use it

- The Specialist plan and up (Specialist, Commander, Enterprise). On any lower plan (Cadet, Voyager) calls return `403` with `plan_required`. MCP itself works on every plan.
- An admin must turn on AI connections for the workspace.
- You need a personal access token (it starts with `mcp_pat_`). Sign-in tokens from Claude or ChatGPT don't work here.
- The token's scopes decide what it can do. A call outside its scopes returns `403` with `insufficient_scope`, and `details.required_scope` names the scope it needs.

Keep the token on your server. Never put it in browser code or a public repo.

## Base URL

`https://mcp.workspace369.com/v1`

The full spec is public (no token needed) at `GET /v1/openapi.json`. Point your code generator at it. The spec lists paths only; set the base URL `https://mcp.workspace369.com` in your generator.

## Two ways to call

**Any tool by name:** `POST /v1/tools/{name}` with the tool's input as the JSON body. The answer is the tool's output. Keys the tool doesn't know are ignored. The tool names are in the MCP tools reference.

```bash
curl -s https://mcp.workspace369.com/v1/tools/clients_search \
  -H "Authorization: Bearer $W369_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"acme"}'
```

Some tools run only inside the app and can't be called here (`batch_apply`, and the workflow tools `automations_create_draft`, `automations_activate`, `automations_retry`). They return `404` with `tool_not_found`.

**Resource paths:** shortcuts onto the same tools.

| Method and path | Tool | Scope |
|---|---|---|
| `GET /v1/clients`, `GET /v1/clients/{id}` | `clients_search`, `clients_get` | `clients:read` |
| `POST /v1/clients`, `PATCH /v1/clients/{id}` | `clients_create`, `clients_update` | `clients:write` |
| `GET /v1/proposals`, `GET /v1/proposals/{id}` | `proposals_search`, `proposals_get` | `proposals:read` |
| `POST /v1/proposals`, `PATCH /v1/proposals/{id}` | `proposals_create_draft`, `proposals_update_draft` | `proposals:write` |
| `POST /v1/proposals/{id}/send` | `proposals_send` | `proposals:write` |
| `GET /v1/invoices`, `GET /v1/invoices/{id}` | `invoices_search`, `invoices_get` | `invoices:read` |
| `POST /v1/invoices`, `PATCH /v1/invoices/{id}` | `invoices_create_draft`, `invoices_update_draft` | `invoices:write` |
| `POST /v1/invoices/{id}/send` | `invoices_send` | `invoices:write` |
| `GET /v1/contracts`, `GET /v1/contracts/{id}` | `contracts_search`, `contracts_get` | `contracts:read` |
| `POST /v1/contracts` | `contracts_create_draft` | `contracts:write` |
| `POST /v1/contracts/{id}/send` | `contracts_send` | `contracts:send` |
| `GET /v1/projects`, `GET /v1/projects/{id}` | `projects_search`, `projects_get` | `projects:read` |
| `POST /v1/projects`, `PATCH /v1/projects/{id}` | `projects_create`, `projects_update` | `projects:write` |
| `GET /v1/tasks`, `GET /v1/tasks/{id}` | `tasks_search`, `tasks_get` (also needs `?board_id=`) | `tasks:read` |
| `POST /v1/tasks`, `PATCH /v1/tasks/{id}` | `tasks_create`, `tasks_update` | `tasks:write` |
| `GET /v1/requests`, `GET /v1/requests/{id}` | `requests_search`, `requests_get` | `requests:read` |
| `PATCH /v1/requests/{id}` | `requests_update_status` | `requests:write` |
| `GET /v1/calendar/events` | `calendar_list_events` | `calendar:read` |
| `POST /v1/calendar/events`, `PATCH /v1/calendar/events/{id}` | `calendar_create_event`, `calendar_update_event` | `calendar:write` |

There is no `PATCH` for contracts, no `POST` for requests, and no `DELETE` anywhere. Use `POST /v1/tools/{name}` for any tool without a shortcut.

On `GET`, put the tool's arguments in the query string. Numbers and `true`/`false` are read as such, and a list is comma-separated. The `{id}` in the path always wins over an `id` in the body or query.

A successful call returns `200` with the tool's output as JSON. A get for an id that doesn't exist also returns `200`, with the record as `null`, so check for it.

## Sends wait for approval

Sending a proposal, contract, invoice or message returns `202` with the approval details. Nothing goes out until a person approves it in the app. A few other actions that reach a customer are held the same way (bulk messages, contract reminders and voids, replying to a request, pay links).

```json
{
  "held_for_approval": true,
  "approval_id": "2b89220a51485f343f1ae00407384707",
  "approval_url": "https://app.workspace369.com/settings/approvals/2b89220a...",
  "approval_expires_at": "2026-10-21T06:24:27.070Z"
}
```

Show "awaiting approval" in your app for a `202`. To check the result later, call the `approvals_get` tool with the `approval_id`. If the person who connected turned on "send without asking", the send runs at once and you get `200`.

Pass a stable `idempotency_key` on sends so a retry doesn't send twice.

## Errors

Every error has the same shape: `{"error":{"code","message","details"}}`.

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_json` | The body isn't valid JSON. Some malformed bodies are rejected before they reach the API and get a plain `400 Bad Request` with no JSON body, so treat any 400 as "fix the request". |
| 401 | `unauthorized` | Missing or invalid token. |
| 402 | `usage_exhausted` | The workspace is out of Tokens. |
| 403 | `insufficient_scope` | The token lacks a scope; `details.required_scope` names it. |
| 403 | `forbidden` | `details.reason` is `permission_denied` (the person lacks the permission), `mcp_not_enabled` (AI connections are off) or `app_not_approved`. |
| 403 | `plan_required` | The workspace is below the Specialist plan. |
| 404 | `not_found` | No such path. A record that doesn't exist is not an error: a get returns `200` with `null` (for example `{"client":null}`). |
| 404 | `tool_not_found` | No such tool, or one that only runs inside the app. |
| 409 | `conflict` | The record isn't in a state this call can act on. |
| 413 | `payload_too_large` | The body is over 1 MB. |
| 422 | `invalid_request` | The input failed validation; `details.issues` lists each problem. |
| 422 | `tool_error` | The input was valid but the tool refused it; `message` says why. |
| 429 | `rate_limited` | Too many requests. Wait for `Retry-After` seconds (also in `details.retry_after_seconds`). Also returned when too many approvals are waiting. |
| 500 | `internal_error` | Something failed on our side. Not charged. |
| 503 | `service_unavailable`, `tool_disabled` | Try again shortly. `tool_disabled` means that one tool is switched off for now. |

Rate limits: 120 requests a minute per token, 600 a minute per workspace. The limit is shared with your MCP calls.
