HTTP API

This page: call an app's actions from an external system over HTTP. Use it when a support-desk bot, a CI job, a cron script, a Lambda, or any backend service needs to invoke your app without a browser and without an MCP client.

If you want to… Read
Call one action from a script or backend service This page — HTTP API
Let an AI host discover and call every action MCP Server
Connect Claude, ChatGPT, Codex, Cursor to your app External Agents
Have another agent-native app delegate work to yours A2A Protocol
Define the operation in the first place Actions

Every defineAction in actions/ is already an HTTP endpoint. The frontend calls it through useActionQuery / useActionMutation with a session cookie, but the same route accepts an Authorization: Bearer token, so anything that can make an HTTPS request can call it. There is no separate "public API" to build and no REST wrapper to maintain — the action you wrote for the agent and the UI is the API.

When to use this surface

Pick HTTP when the caller is code and already knows exactly which operation it wants:

Caller Use
A script, backend service, CI job, or webhook consumer HTTP — this page
An LLM host that should discover tools and choose MCP
Another agent-native app delegating a fuzzy task A2A
A browser page in your own app useActionQuery / useActionMutation

HTTP is the cheapest of the three: one request, one JSON body, one JSON response, no handshake, no session, no model in the loop. MCP and A2A both sit on top of the same actions — reach for them when the caller needs a tool catalog or the whole agent loop, not a single known operation.

Mint a token

Tokens come from the connect CLI. There are two kinds, and the choice matters.

Personal token — bound to the person who runs the command. Use it for your own scripts and local development.

npx @agent-native/core@latest connect https://clips.example.com

This opens a browser sign-in, then writes the token into your local MCP client configs. Rows the token creates are owned by you.

Org service token — bound to a synthetic, organization-owned identity rather than a person. Use it for anything that must keep running after people change teams: CI, a support-desk bot, a scheduled integration.

npx @agent-native/core@latest connect https://clips.example.com \
  --service-token support-desk --ttl-days 90

The command still authenticates you in the browser first — minting requires org owner or admin — but the resulting token's subject is svc-support-desk@service.<orgId>. It survives you leaving the org or revoking your own personal tokens, and the rows it creates are org-scoped, so every org member can see them. The token value is printed exactly once and is never written to a local config file; copy it straight into your secret store.

Personal token Org service token
Identity The person who minted it svc-<name>@service.<orgId>
Survives that person leaving No Yes
Rows it creates Owned by that person Org-scoped
Who can mint Any signed-in user Org owner/admin only
Best for Your own scripts, local dev CI, bots, scheduled integrations

Personal tokens default to 365 days in the CLI device flow; to choose a lifetime from 1 to 365 days, use the app's Connect page at https://<app>/mcp/connect. Org service tokens also default to 365 days and accept --ttl-days values between 1 and 3650. If a machine cannot open a browser, the app's Connect page is the fallback: sign in there, copy the token, and pass it with connect <url> --token <token> or paste it directly into your secret store.

For a self-hosted, single-tenant app where per-person identity does not matter, the static ACCESS_TOKEN / ACCESS_TOKENS environment variables are the simplest option — see Authentication — Static MCP Bearer Tokens.

Call an action

The route is POST /_agent-native/actions/<action-id>. The action id defaults to the action's filename, so actions/import-loom-recording.ts mounts at /_agent-native/actions/import-loom-recording.

The request body is the raw JSON of the action's zod schema — no envelope, no { args: ... } wrapper. The response is the action's return value serialized bare at the top level, not wrapped in { result: ... }.

curl -sS -X POST \
  https://clips.example.com/_agent-native/actions/import-loom-recording \
  -H "Authorization: Bearer $AGENT_NATIVE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.loom.com/share/EXAMPLE_SHARE_ID"}'
{
  "recordingId": "rec_8f2a91",
  "title": "Checkout fails on Safari 17",
  "status": "imported"
}
Call an action over HTTP

GET actions and query params

An action that declares an http override is reachable with the method it names, and GET args arrive as query params instead of a JSON body:

actions/get-lead.ts
export default defineAction({
  description: "Get details for a lead.",
  schema: z.object({ leadId: z.string() }),
  http: { method: "GET" },
  run: async ({ leadId }) => {
    /* ... */
  },
});
curl -sS \
  "https://crm.example.com/_agent-native/actions/get-lead?leadId=lead_123" \
  -H "Authorization: Bearer $AGENT_NATIVE_TOKEN"

http: { path: "..." } changes the mounted URL for direct HTTP callers only, and http: false removes the endpoint entirely. See Actions — HTTP config for the full option set.

Errors

Every failure returns { "error": string } with an appropriate status:

Status Meaning
400 Input failed the action's zod schema. The message names the offending field.
401 Missing, expired, revoked, or wrong-audience bearer token.
403 Authenticated, but not allowed to touch this data — see When it 403s.
405 Wrong method. The message tells you which one the action declares.
409 Only for browser clients running a stale build; server-to-server callers never see it.
500 The action threw. The response message is always Internal server error — the real detail is logged server-side with a capture id, never returned.

Treat a 500 as "check the app's logs", not "read the error text". Actions that want to return a specific, user-facing failure should throw with an explicit statusCode below 500; those messages do come back verbatim.

CSRF and CORS

A cookieless server-to-server call skips CSRF entirely — there is no session cookie to confuse, so there is nothing to protect against. You do not need to fetch a CSRF token, and you should not send cookies.

Cross-origin calls from a browser are different: they need an allowlisted origin. If you are calling from browser JavaScript on another domain, configure the app's CORS allowlist first. Server-side callers are unaffected.

Worked example: a support-desk bot

A support-desk bot watches inbound tickets. When a customer attaches a Loom share link to a bug report, the bot imports it into Clips so the support team can comment on the recording, clip the relevant 20 seconds, and attach it to the engineering issue.

1. Mint a token the bot owns. Run this once, as an org owner or admin:

npx @agent-native/core@latest connect https://clips.example.com \
  --service-token support-desk --ttl-days 365

Copy the printed value into the bot's secret store as AGENT_NATIVE_TOKEN. It is shown once and never stored by the app.

2. Call the action when a ticket arrives.

const res = await fetch(
  "https://clips.example.com/_agent-native/actions/import-loom-recording",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.AGENT_NATIVE_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: shareUrl }),
  },
);

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`Clips import failed (${res.status}): ${error}`);
}

const { recordingId } = await res.json();

3. Link back to the recording. Post https://clips.example.com/r/${recordingId} into the ticket thread. Because the bot used an org service token, the recording is org-scoped: every teammate who opens that link sees it, and it does not disappear when the person who set up the bot changes teams.

The action ran exactly as it would have from chat or the UI — same validation, same access checks, same app state write that focuses the new recording for anyone who has the app open.

Lifetime, rotation, and revocation

Tokens are JWTs signed by the app and bound to that app's audience. A token minted for one app cannot call another.

  • Lifetime — 365 days by default, --ttl-days between 1 and 3650. Shorter is better for anything that runs unattended.
  • Rotation — mint the new token, deploy it to the caller, confirm traffic is flowing, then revoke the old one. There is no in-place rotation; the two tokens are independent and can overlap.
  • Revocation — every token carries a jti, and revocation is a jti check on every request, so a revoked token stops working immediately. Use the list-org-service-tokens action to see names, who minted them, and last-used timestamps, then revoke-org-service-token with the id. Both are owner/admin-gated for writes; any org member can list.
  • Recovery — the token value is never stored, only its jti. If you lose it, mint a new one and revoke the old.

Because list-org-service-tokens reports lastUsedAt, it doubles as a cleanup tool: anything that has not been used in months is a candidate for revocation.

Treat the token exactly like a password. It authenticates as a real identity in your organization, and it is long-lived by default.

  • Put it in a secret manager or CI secret, never in source, a Dockerfile, a log line, or a client-side bundle.
  • Give each caller its own named service token (--service-token support-desk, --service-token ci) so you can revoke one without breaking the others, and so lastUsedAt tells you who is actually calling.
  • Prefer the shortest --ttl-days the caller can live with.
  • A service token acts as an org member, and only ever member. The synthetic svc-*@service.<orgId> identity is never inserted into org_members; it resolves an implicit member role for the org it was minted against, and nothing higher. So it can read and write that org's data like any member, but it cannot mint further tokens, revoke existing ones, or manage org members and settings — every one of those is admin-gated. It can still do everything a normal member can do with the org's data, so scope it deliberately.
  • Actions you never want reachable this way should set http: false (agent and CLI only) or toolCallable: false. See Actions — Exposure flags.

Service principals

Each org service token is a service principal, and a governance record says who answers for it and what it may do. Owners and admins set it when minting (create-org-service-token takes optional ownerEmail, team, riskTier, purpose, and allowedActions) and change it later with set-service-principal-policy. list-org-service-tokens returns each principal's record in principals.

Field Meaning
ownerEmail The accountable human. Defaults to the admin who minted the token.
team, purpose Who to page and what the principal is for.
riskTier low, medium, or high.
lifecycle active, suspended, or retired.
allowedActions null is unrestricted. A list is a deny-by-default grant of exact action names or a trailing * prefix pattern; an empty list grants nothing.
  • Grant — tools/list shows only the granted actions, and any call outside the grant is refused, whether it arrives over MCP, an HTTP action route, or an agent run the principal started with ask-agent or ask_app. Agent built-ins such as tool-search and view-screen are covered too, so name them in the grant if the principal needs them. The grant narrows what the member role already allows; it never adds access.
  • Suspend and resume — set-service-principal-lifecycle with suspended refuses the principal on every request, refuses new agent runs for it, and aborts its in-flight runs. active resumes it. The MCP connect page (/mcp/connect) has the same suspend and resume controls. retired also revokes its tokens and cannot be undone.
  • Ungoverned — a service token minted before governance has no record. It stays active and unrestricted until an admin sets a policy.
  • Fails closed — if the policy cannot be read, the request is refused with a 503, never admitted.

These actions are owner/admin-only and are not callable by the agent, so neither an agent nor a service token can loosen its own grant.

When it 403s

A 401 means the token was not accepted at all. A 403 means the token was accepted and the request then failed an access check — a different problem with a different fix.

Symptom Likely cause
401 on every call, immediately after minting Token minted against a different app URL. It is audience-bound to {appUrl}/mcp; mint it against the app you are calling.
401 that started working and then stopped Expired (check --ttl-days) or revoked. Check list-org-service-tokens with includeRevoked: true.
403 on a data action The identity is not scoped to the org that owns the row. Confirm the token was minted with an active org, and that the row is org-scoped rather than owned by a person.
403 on a data action from a service token only The app authorizes with its own org_members role lookup and has not adopted implicitServiceOrgRole from @agent-native/core/org, so the synthetic identity resolves no role.
403 on create-org-service-token / revoke-org-service-token The caller is an org member, or is a service token. Minting and revoking require a human owner/admin.
403 from browser JavaScript, but curl works Cross-origin browser call from an origin that is not allowlisted. See CSRF and CORS.
405 with a method you did not expect The action declares http: { method: "GET" }. Send args as query params, not a JSON body.
404 Wrong action id, an http: { path: "..." } override, or http: false.
  • Actions — define the operation once; HTTP, MCP, A2A, CLI, and the UI all call the same one
  • Authentication — session auth, orgs, and static ACCESS_TOKEN bearers
  • MCP Server — the tool-catalog surface these tokens are audience-bound to
  • A2A Protocol — agent-to-agent delegation between agent-native apps
  • Security — data scoping, access guards, and secret handling