Integrations

API Keys & MCP

How scoped API keys work, and how to hand the same key to an AI agent over MCP.

The big idea

A Public API Key is a scoped credential your org issues itself, separate from the single all-or-nothing key used for the Zapier connection (see the Zapier guide for that one). You can issue as many of these as you need — one per integration, one per script, one per AI agent — each with only the scopes that integration actually requires.

Every key works two ways with zero extra setup: as a normal REST API credential, and as an MCP (Model Context Protocol) connection an AI agent can use to read and act on your data directly. Same key, same scopes, same rate limit — you don't choose one or the other when you create it.

This page explains the concepts and walks through a real setup. For the exhaustive, endpoint-by-endpoint reference — every route, request/response shape, required scope, and the exact MCP tool name it maps to — see the full endpoint reference.

Creating a key

Requires the API Access entitlement. The public API/MCP server is gated behind the api_access add-on — bundled on Enterprise, purchasable standalone on Starter/Equipt/Growth from Settings → Subscription. Without it, Create Key returns a 403 ("API access isn't included on your plan") and no key is created. This is checked live, not just at creation time: if the org later cancels the add-on or downgrades off a plan that bundles it, every key and MCP connection it already issued stops working on its very next request — a 403 with the same message, not just a block on new keys.
  1. Go to Master Account Settings → Integrations and find the Public API Keys card — it sits below the Zapier connection, and is unrelated to it.
  2. Click Create Key, give it a descriptive name (e.g. "Reporting integration" or "Ops agent"), then check the boxes for exactly the scopes it needs from the resource checklist.
  3. Click Create Key in the dialog. The plaintext key is shown once, right there — copy it before closing. It is never displayed again in full; the list view afterward only shows a short prefix.
  4. The same dialog also shows a ready-to-paste MCP client config block — see Using the key with MCP below.
There is no "edit scopes" on an existing key. If a key needs different access later, create a new key with the right scopes and revoke the old one — scopes are fixed at creation time.

Resources & scopes

Every request — REST or MCP — is checked against the connecting key's scopes before it touches the database. A scope is written as resource:tier, e.g. clients:read or requisitions:write:safe. There are 14 resources spanning both products, each offering Read and, where a create/update endpoint actually exists, Write.

ResourceAvailable tiers
ClientsRead, Write
EstimatesRead, Write*
JobsRead, Write
InvoicesRead
ContractsRead, Write*
AssetsRead, Write
Work OrdersRead, Write
PM SchedulesRead, Write
PartsRead, Write
RequisitionsRead, Write*
Purchase OrdersRead, Write*
VendorsRead, Write
ProductsRead, Write
ProjectsRead, Write

Invoices is intentionally read-only end to end — no create or update endpoint exists, by any key, under any scope. The ones marked Write* above are deliberately narrower than a normal create/update pair:

  • Requisitions and Purchase Orders can only be created — there is no update endpoint for either. A created requisition lands in draft; a created PO always lands at requested — the same starting point the app's own "New PO" dialog produces. Neither can be advanced, approved, or rejected via the API under any scope: the DB itself blocks setting a PO to approved/rejected without a resolved approval chain, so approval only ever happens in-app.
  • Contracts can also only be created, no update. This one exists to record an agreement already executed outside the app (e.g. signed via DocuSign) — it accepts a historical signedAt/signedBy, unlike the app's own contract UI. It defaults to status draft, which does not bill anyone — a caller has to pass an explicit signed/active status to start real invoicing on that contract's billing cadence.
  • Estimates offers a Write scope, but the only thing it unlocks is adding a single catalog-priced line to a new estimate — every dollar figure is still computed by the app's own budget engine, never supplied by the caller, and there is no update or full-estimate-build path via the API. Full estimate authoring stays a human, in-app action.

For the full mapping of REST verb → required scope → MCP tool name, per endpoint, see the full endpoint reference.

Walkthrough: a read-only reporting key

Say you want an AI agent to pull numbers for a weekly ops summary — clients, jobs, and invoices — and nothing else. It should never be able to create or change anything. Here's the exact setup:

  1. Create Key → name it something like "Weekly reporting (read-only)".
  2. In the scope checklist, check only three boxes: Clients → Read, Jobs → Read, Invoices → Read. Leave everything else — including Write on those same three resources — unchecked.
  3. Click Create Key. Copy the plaintext key from the reveal dialog, and copy the MCP config block underneath it too (it's pre-filled with this exact key).
  4. If you use Claude Code, run the command shown in that same dialog — it looks like this, and you paste the whole thing at your terminal prompt (it's one command, not a file to edit):
claude mcp add --transport http landscapt https://<your-domain>/api/mcp --header "Authorization: Bearer <your-api-key>"
  1. If you use Claude Desktop (or any other MCP client) instead, open its settings → Developer → Edit Config, and paste the JSON block from the same dialog into claude_desktop_config.json under mcpServers (merge it in if other servers are already configured there) — this is a config-file snippet, not a command, so it goes in that file, never into a terminal. It looks like this:
{
  "mcpServers": {
    "landscapt": {
      "url": "https://<your-domain>/api/mcp",
      "headers": {
        "Authorization": "Bearer <your-api-key>"
      }
    }
  }
}
  1. Restart Claude Desktop (or Claude Code). It connects to the MCP server and negotiates available tools.
  2. Because this key only has three read scopes, the agent sees exactly six tools: whoami, list_clients / get_clients, list_jobs / get_jobs, and list_invoices / get_invoices. No create or update tool exists for it at all — not because it was denied at call time, but because those tools were never registered for this key in the first place.
Least privilege, by design. If this key ever leaked — pasted in the wrong Slack channel, committed to a repo — the blast radius is read access to clients, jobs, and invoices. It could not create a client, edit a job, touch a purchase order, or see anything in Equipt's asset or parts inventory. Scoping tightly at creation time is the actual security control here, not an afterthought — always grant the narrowest set of scopes a key needs and nothing more.

Using the key with MCP

/api/mcp is the same API you'd call over REST, exposed as an MCP server instead. There is no separate MCP credential to generate — any Public API Key you've already created works immediately as an MCP connection, with the same scopes it was granted for REST.

Connect any MCP client — Claude Desktop, Claude Code, or any other MCP-compatible tool — by pointing it at that URL with the standard header:

Authorization: Bearer <your-api-key>
The command vs. JSON block trips people up: the key-creation dialog shows two different snippets — a command (for Claude Code, starts with claude mcp add) and a JSON config block (for every other MCP client). The command is meant to be pasted at a terminal prompt; the JSON is meant to be pasted into a config file. Pasting the JSON directly into a terminal will fail with a shell parse error — it isn't a command.

Sign in instead — Claude.ai's connector (OAuth)

Claude.ai's web and desktop app also support connecting by signing in, instead of pasting an API key at all:

  1. In Claude.ai, go to Settings → Connectors → Add custom connector.
  2. Name it (e.g. "Landscapt") and enter the same https://your-domain/api/mcp URL as above. Leave authentication on "Always required" (Claude detects this automatically) with no request header — this is a different auth path from the bearer-token one above.
  3. Save. Claude redirects you to sign in with your normal Landscapt account, then shows a consent screen listing every resource with a checkbox per scope. Check what you want this connection to have, then Approve.
  4. Test it — ask Claude something like "show me my 5 most recent open work orders."
Every signed-in user can do this themselves — it's not a shared, org-wide key. Each connection is tied to whoever signed in and approved it, with whatever scopes they picked. By default, only admins can grant Write access — every other role's consent screen only ever shows Read checkboxes, so a manager, technician, purchaser, or viewer connecting this way gets read-only access no matter what they check, enforced server-side, not just hidden in the UI. An admin can opt specific roles into write access from Settings → Public API Keys → OAuth Write Access — e.g. letting managers or purchasers grant write scopes too, while everyone else stays read-only.

Manage or disconnect an OAuth sign-in from Settings → Public API Keys → Connected Apps — a separate list from the manually-created keys above, since these aren't issued directly by an admin.

Tool names follow one fixed convention, generated directly from the REST route each tool calls:

REST verbShapeMCP tool name
GET (list)no id in the pathlist_<resource> — e.g. list_clients
GET (single)id in the pathget_<resource> — e.g. get_clients
POSTcreates a recordcreate_<resource> — e.g. create_jobs
PATCHupdates a recordupdate_<resource> — e.g. update_jobs

A key with zero resource scopes still connects successfully and gets three tools — whoami, search_docs, and get_guide — so an agent can at least confirm which org and scopes it's connected as, and answer "how do I…" questions from the help docs, before deciding what else to do — it never sees an empty, broken-looking connection.

Every tool call is charged against the key's rate limit exactly once, the same as a direct REST call — there's no separate, more generous allowance for agent traffic. An agent that calls a tool in a loop burns the same budget a script hammering the REST endpoint would.

Answering "how do I…" from the help docs

Two tools are always available, regardless of the key's scopes, because they only read the same non-sensitive help content already visible at Support and Docs — no org data: search_docs (full-text search across every short Support article and every long-form Docs guide, e.g. Purchase Orders, Work Orders, Estimating) and get_guide (the full text of one guide, by the slug a search result points at).

This is what lets a connected agent answer a genuine "how do I convert a requisition to a PO" or "what does FIFO costing actually do here" question from your own documentation instead of guessing from general knowledge — search first, then fetch the full guide if one result looks like the right one.

Guide content is a generated index, not live. The long-form guides are ordinary app pages (JSX, not structured data), so their searchable text is pre-extracted into src/lib/docs-guides-content.json by npm run docs:index rather than rendered on every search call. If you're maintaining this app: re-run that script after adding or editing a guide, or search_docs and get_guide will keep returning the old text.

Why REST and MCP share one key

It would have been possible to build MCP as its own credential system — a separate "MCP key" with its own scope picker, issued and revoked independently of the REST keys. We didn't do that, on purpose.

Under the hood, every MCP tool delegates to the exact same REST route handler the API uses — not a re-implementation of the same logic, the literal same function, called with the request's own Authorization header. That means a tool can never do anything, or see anything, that the equivalent REST call couldn't. There is only one place scopes are defined, one place they're enforced, and one mental model to reason about: a key's scopes are what it can do, full stop, regardless of which protocol asks.

Practically, this means you scope a key once, thinking only in terms of "what should this integration be able to see or change" — never twice, and never having to remember which credential type governs which surface.

Rate limits, errors & revoking

Keys are rate-limited per minute. A request — REST or MCP — that exceeds the limit gets back a 429. A request for a scope the key wasn't granted is rejected before it reaches any business logic, rather than returning partial or filtered data. Every request also re-checks the org's api_access entitlement first (see Creating a key above) — a 403 from a key that used to work almost always means the add-on lapsed, not a scope or credential problem.

Revoking a key (from the same Public API Keys card) takes effect immediately — any REST client or MCP connection using it loses access right away, mid-session included. This cannot be undone; if the integration needs access again, issue a new key.

Revoked keys stay listed, greyed out, for your own audit trail — they just stop working. There's no "pause" short of revoking; if you need a key temporarily disabled, revoke it and create a fresh one when it's needed again. Once a key is revoked, a Remove button next to it lets you clear it from the list entirely if it's just clutter — that only hides it from view, it doesn't erase the underlying audit record.
© 2026 Landscapt. All rights reserved.