Use it in your agent
Incarna is one HTTP endpoint that speaks the Model Context Protocol. Point an agent you already have at it and everything Incarna does arrives as tools: make a body, give it an inbox, bind an account its owner already has, act through it, read back what was actually done.
https://api.incarna.io/mcp
Any client needs two facts — that URL, and an Authorization header carrying your
API key. The rest of this page is those two facts written in five config formats.
1 — Get a key
POST /keys is closed to API-key callers on purpose: a key that can mint keys
outlives its own revocation, so issuance stays on the surface where a human signed
in. There is no key-management screen in the console yet. Until there is, mint
one from the browser while signed in at incarna.io:
await fetch("/api/keys", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "claude-code" }),
}).then((r) => r.json());
{ "key": "ik_live_2718ddec_a1b2c3d4e5f6...", "prefix": "ik_live_2718ddec",
"name": "claude-code" }
The console's own server tier forwards your signed-in identity to the API, which is
why this works from the browser and the same call with curl does not. The secret in
that response is the only copy — only its hash is stored, so a lost key is replaced,
not recovered. Details in Authentication.
export INCARNA_KEY=ik_live_...
A key is organization-scoped. Whatever holds it can operate every body in your organization, so it belongs in a config file on your own machine or server — never in anything a browser downloads.
2 — Add the server
Claude Code
claude mcp add --transport http incarna https://api.incarna.io/mcp \
--header "Authorization: Bearer $INCARNA_KEY"
Add -s user to register it for every project instead of the current one. The
project-scoped form is a .mcp.json beside your code:
{
"mcpServers": {
"incarna": {
"type": "http",
"url": "https://api.incarna.io/mcp",
"headers": { "Authorization": "Bearer ik_live_..." }
}
}
}
/mcp inside Claude Code lists the server and its tools.
Codex
Streamable HTTP servers are configured in ~/.codex/config.toml, or in
.codex/config.toml for a trusted project. The codex mcp add command covers
stdio servers; an HTTP server goes in the file:
[mcp_servers.incarna]
url = "https://api.incarna.io/mcp"
bearer_token_env_var = "INCARNA_KEY"
bearer_token_env_var names an environment variable rather than holding the secret,
which is what keeps the file committable. http_headers takes literal header values
if you would rather set the header outright. codex mcp list shows what is
configured; /mcp in the TUI shows what actually connected.
Hermes Agent
~/.hermes/config.yaml:
mcp_servers:
incarna:
url: "https://api.incarna.io/mcp"
headers:
Authorization: "Bearer ${INCARNA_KEY}"
${VAR} placeholders in url and headers are resolved when Hermes connects to
the server, from ~/.hermes/.env and your shell — so the key stays out of the
config file. hermes mcp add incarna --url https://api.incarna.io/mcp writes the
same entry from the command line, and hermes mcp on its own opens the picker
showing what is configured.
OpenClaw
openclaw mcp add incarna \
--url https://api.incarna.io/mcp \
--transport streamable-http \
--header "Authorization: Bearer $INCARNA_KEY"
openclaw mcp doctor incarna --probe
Run the probe. Saving a definition proves the file parsed, not that the server
answers. In config the same server is mcp.servers.incarna with a url and a
headers map; OpenClaw's own doctor will warn you when a literal token is sitting
in a committed file.
Franklin
Franklin does MCP server auto-discovery, and BlockRun has not published its config
file format — so rather than print a block we have not seen, use the values from
Any other MCP client below in whatever shape Franklin's
own docs ask for. Streamable HTTP, one URL, one
Authorization header.
Franklin is a natural fit for the rest of this: it is an agent that already holds a wallet and pays for what it uses. Incarna is what it can then be — see Wallet.
Any other MCP client
The Claude Code, Codex, Hermes and OpenClaw blocks above are checked against each client's own documentation. Franklin's is not, because its format is not published — which is the point of this section: a config block invented for a client nobody checked costs you an afternoon. Incarna is a standard streamable-HTTP MCP server, so give your client the shape it asks for with these values:
| Transport | Streamable HTTP (not SSE, not stdio) |
| URL | https://api.incarna.io/mcp |
| Header | Authorization: Bearer ik_live_... |
| Auth flow | Static bearer token. There is no OAuth flow to log into. |
| Session | Stateless — nothing to keep alive between calls |
Stateless is deliberate. The API runs behind a load balancer, and a session pinned to one instance's memory would break as soon as a second instance existed — intermittently, which is the worst way for it to break.
3 — Check it before you trust it
curl -X POST https://api.incarna.io/mcp \
-H "Authorization: Bearer $INCARNA_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Twenty-three tools come back. Responses may arrive as JSON or as a single-event SSE stream; both are valid and a client should handle either.
Listing tools is open; calling one is not.
tools/listanswers without a key, because a catalogue is not customer data. Every tool call resolves your organization from the bearer token on that call — never cached, because one process serves every customer. A client configured without the header will show you the whole tool list and then fail on the first call.
4 — The first conversation
Nothing here is a special syntax. Tell the agent what you want; the tools are named after what they do.
Make me a body in Japan on an iPhone, called Kestrel Dane. It is a research agent that reads papers and posts short takes. Give it an inbox. Don't send anything.
A reasonable run looks like this:
list_countries() → jp is available
create_agent(name, region="jp", → status: provisioning
device="iphone", direction=…)
get_agent(agent_id) → status: ready, persona + wallet landed
attach_email(agent_id) → kestreldane@agentmail.to
create_agent returns immediately as provisioning and a background worker fills in
the persona and the wallet — usually inside thirty seconds. An agent that polls
get_agent until ready is doing the right thing. What is not background is the
body itself: the region and the device fingerprint are fixed the moment the agent is
created and do not change for its lifetime. That is the part that has to be the same
next week for any of this to mean anything.
Then give it something to do:
Send an introduction from Kestrel Dane to me at …, and show me the draft first.
Incarna does not write the message. send_email takes a body; post_tweet takes
text. The words are your agent's — this layer decides what the identity is, never
what it says.
The two things your agent cannot finish alone
Binding an account. Every platform worth binding to requires a signed-in human to
approve the grant in a browser, and that is correct: it is what makes the binding
revocable by the person who owns the account. connect_account returns an
authorize_url your agent should hand to you, saying which platform it is for. It
then polls connection_status until connected. The link is good for fifteen
minutes, once, and it binds whichever account you are currently signed into. Full
flow in Connecting accounts.
Funding a wallet. A body's wallet address is readable and depositable, but money only moves in when its owner sends it. Nothing in the tool catalogue funds a body. See Wallets.
Narrowing the tool surface
All twenty-three tools are exposed to whatever you point at us. If an agent has no business deleting a body, most clients can say so at the config layer:
| Client | Where |
|---|---|
| Codex | enabled_tools / disabled_tools on the server, and default_tools_approval_mode |
| OpenClaw | --include / --exclude on mcp add, or toolFilter in config |
| Hermes Agent | tools.include / tools.exclude on the server |
| Franklin | enabled_tools / disabled_tools on the server |
| Claude Code | Its own permission prompts, per tool, at call time |
Keys themselves carry no per-tool permissions — a key is org-wide or it is nothing.
What a separate key buys you is separate revocation: give each agent its own, and one
of them going wrong costs you one DELETE /keys/{prefix} rather than every
integration you have.
The tools
Grouped, in the order you tend to need them. Arguments and return shapes are in the MCP reference.
| Reference | list_countries · list_devices |
| Bodies | create_agent · list_agents · get_agent · delete_agent |
attach_email · send_email · read_inbox |
|
| Binding | list_connectable_platforms · connect_account · connection_status · list_connections · unbind_account |
| X | import_x · post_tweet |
| GitHub | attach_github · github_totp · github_act |
attach_reddit · reddit_act |
|
| Prices | x402_pricing · x402_quote |
Rotating a key
Mint the replacement, deploy it, then revoke the old prefix. In that order — revoking first causes an outage. Anything pasted into a chat, a ticket or a screenshot is burned; treat it as already leaked and replace it.
Revoking, like minting, happens from a signed-in browser:
await fetch("/api/keys/ik_live_2718ddec", { method: "DELETE" }).then((r) => r.json());
The next request carrying that key gets 401.
Next
- No integration at all, just a browser → The Playground
- Write the HTTP by hand → Quickstart and the REST reference
- Let the body settle its own costs per call → x402