describe, which returns the TypeScript declarations of the API, and execute, which runs a script the agent wrote. Inside the script, each call is await ps.<tool>(args) — the same tools, the same arguments and the same permissions as on /mcp.
/mcp endpoint does not change. Pick one per connection.
When to use it
Use/code when:
- the task chains several calls — list, filter, then read or act on each item;
- you loop over many items (numbers, webhooks, messages, contacts);
- you want a report computed from many reads, returned as one result;
- your agent’s context is tight and the 139 tool definitions cost too much.
/mcp when the task is one or two calls, or when you want to approve every call one by one in your client.
A chain that takes dozens of round trips through the model on /mcp is a single execute on /code: the intermediate results stay inside the script, and only what it prints comes back to the model.
What it costs in context
Measured withcount_tokens on Claude Sonnet:
The agent only pays for the themes it actually reads.
Connect
Authentication is the same as on/mcp: an OAuth 2.1 token (Authorization: Bearer, handled by the claude.ai custom connector, with whole-tenant or per-number grants) or a ps_* API key in the x-api-key header — with an account key (ps_account_), plus x-workspace-id naming the workspace (details). Each call the script makes goes to the public /v1 API with that credential, so the key’s scope, roles, per-number grants, rate limits and billing apply exactly as they do on /mcp.
In claude.ai, add a custom connector with the URL https://mcp.pilotstatus.com.br/code. Clients configured by file:
Code Mode exists only on the hosted server. The npm package
@pilot-status/mcp-server (self-hosted) serves the 139 tools only.describe
Returns what the script can call. It takes at most one argument:
Themes:
messages, conversations, referrals, leads, ads, groups, numbers, templates, flows, branding, analytics, api-keys, subscription, media, billing, webhooks, calls, chatwoot, pricing, workspace, phone-lines, docs.
Every method except docs_search also accepts whatsappNumberId, the target number for number-scoped tools. It is required with an OAuth or tenant-scoped credential (it is sent as the x-whatsapp-number-id header) and ignored when the API key is already scoped to one number.
Errors: passing both theme and search returns INVALID_ARGUMENTS; an unknown theme returns UNKNOWN_THEME with the list of valid themes.
execute
Takes one argument, code: the body of a TypeScript script.
- Top-level
awaitworks. Call the API withawait ps.<tool>(args); the result is the API’s JSON response. - Print with
console.log(alsoinfo,warn,error,debug). Objects are printed as JSON. - A
returnvalue is appended to the output as=> <value>. - Types are only stripped, not checked: a type error does not stop the script, a syntax error does.
- There is no network and nothing from the host: no
fetch, noimport, norequire, noprocess, nosetTimeout/setInterval. The only way out of the script isps.
Example: list, then read each
Reads the recent deliveries of every webhook on the number, in parallel, and prints only the summary.Example: handle an API refusal
When the API refuses a call,ps.<tool> throws inside the script. The error has name === "ApiError", the HTTP status and the API’s code. Catch it to keep going.
InvalidArguments (the arguments do not match the declaration; code is INVALID_ARGUMENTS), CallLimitExceeded and SendLimitExceeded (see Limits).
Example: send safely with idempotencyKey
Pass an idempotencyKey to every ps.messages_send. Derive it from what makes the send unique — here the order id and the recipient — so that running the same script again does not send the same message twice.
409 IDEMPOTENCY_KEY_REUSED. Idempotency
Writing scripts that run fast
Eachps call takes seconds, and the 30-second clock counts them all. The description of execute — what the agent reads in the tool list — tells it to:
- Reuse ids it already has. With a workspace-wide credential, pass
whatsappNumberIdto every number-scoped call, and reuse the id (and any other id) already obtained earlier in the conversation. Callps.numbers_list({})only when the id is not known yet. - Run independent calls together with
await Promise.all([...]), neverawaitone by one in a loop — as in the first example. - Ask only for what it needs: a small
pageSizeand filters such asstatus,startDateandsearch. - Use a real URL for
media: an http(s) URL it was given, never invented base64. For audio and voice this is enforced: a data URI is refused with400 MCP_AUDIO_URL_REQUIREDand nothing is sent. MCP server
Limits per run
- Every call counts, refused ones included. A
messages_sendwith invalid arguments still counts as a send. - Only
messages_sendcounts as a send.call_initiateandcall_permission_requestcount only in the 50 calls. - There is no hourly limit on runs: the rate limits of the
/v1API apply to each call, as they do on/mcp.
Failure and the report
A failure stops the script, and nothing is undone. What was sent stays sent. Everyexecute that ran returns a report:
calls is how the agent knows what already happened before deciding what to do next. To run a script again safely, it must pass idempotencyKey to every ps.messages_send — without it, a re-run sends again.
Refused before running
When the script cannot start,execute returns isError: true with an error, a code and no report:
With no credential at all,
/code answers 401 and starts the OAuth flow, as /mcp does. A credential the API rejects refuses the run with the API’s status and code.
What is recorded
Each run — the script, the arguments of every call and the output — is kept for 30 days for support and security, then deleted. Arguments and output can contain contact data (phone numbers, message text). Runs are not available in the dashboard or the API: the report returned byexecute is your copy.