# ZeroIdentity documentation

An agent talks to ZeroIdentity over MCP or plain HTTP. Both expose the same 12 tools, and both use the agent's API key.

## Quickstart

1. Create an agent at https://zeroidentity.app/login?mode=signup. Sign in with Google, GitHub or an email address, give the agent a name, and it gets an address like `atlas@zeroidentity.sh`.
2. Open its Connect tab and copy the command there. It already contains the agent's API key.
3. Paste it into your agent. For Claude Code that is one line in a terminal.
4. Ask the agent "what is your email address?" It will call `whoami` and tell you.

Claude Code:

```bash
claude mcp add --transport http zeroidentity \
  https://zeroidentity.app/mcp \
  --header "Authorization: Bearer $ZEROIDENTITY_KEY"
```

MCP config:

```json
{
  "mcpServers": {
    "zeroidentity": {
      "type": "http",
      "url": "https://zeroidentity.app/mcp",
      "headers": {
        "Authorization": "Bearer $ZEROIDENTITY_KEY"
      }
    }
  }
}
```

curl:

```bash
curl https://zeroidentity.app/v1/send_email \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "you@example.com", "subject": "Hello", "body": "It works."}'
```

TypeScript:

```ts
const api = "https://zeroidentity.app/v1";
const headers = { Authorization: `Bearer ${process.env.ZEROIDENTITY_KEY}` };

// Waits until mail, a text or a call arrives.
const res = await fetch(`${api}/wait_for_message`, { headers });
const { messages } = await res.json();

for (const message of messages) {
  console.log(message.from, message.subject, message.code);
}
```

Python:

```python
import os, requests

api = "https://zeroidentity.app/v1"
headers = {"Authorization": f"Bearer {os.environ['ZEROIDENTITY_KEY']}"}

# Waits until mail, a text or a call arrives.
res = requests.get(f"{api}/wait_for_message", headers=headers)

for message in res.json()["messages"]:
    print(message["from"], message.get("subject"), message.get("code"))
```

The examples read the key from a `ZEROIDENTITY_KEY` environment variable. Keys start with `zid_` and belong to one agent. Rotate a key from the Connect tab if it leaks.

## MCP

The MCP server is at `https://zeroidentity.app/mcp`. It speaks Streamable HTTP and keeps no session, so every request stands on its own. Send the API key as a bearer token. Any client that accepts a remote server with custom headers will work.

## HTTP API

Every tool is an endpoint at `https://zeroidentity.app/v1/<tool>`. Send arguments as a JSON body with POST, or as a query string with GET. Responses are JSON.

```bash
curl "https://zeroidentity.app/v1/whoami" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

```bash
curl https://zeroidentity.app/v1/send_email \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"dana@acme.dev","subject":"Access request","body":"Could you add me to the acme org?"}'
```

Sending is safe to retry. Pass an `idempotency_key` to `send_email` or `send_sms` and a repeat with the same key returns the first message instead of sending a second one.

## Receiving messages

`wait_for_message` is the simplest way to listen. It returns unread messages at once if there are any, and otherwise holds the request open until something arrives or the timeout passes (25 seconds by default, 50 at most). What it returns is marked read, so a loop sees each message exactly once.

When an email or a text plainly contains a one-time code, the message carries it in a `code` field, digits only. The agent can use it without parsing the body.

## Event delivery

To be called instead of polling, subscribe a URL with `subscribe` or from the Connect tab. Each inbound message is then sent to it as a POST:

```json
{
  "type": "message.received",
  "agent": {
    "id": "agt_60c75afc15f20fc3",
    "name": "Atlas",
    "email": "atlas@zeroidentity.sh"
  },
  "message": {
    "id": "msg_1a2b3c4d5e6f7a8b",
    "channel": "email",
    "direction": "inbound",
    "from": "noreply@github.com",
    "to": "atlas@zeroidentity.sh",
    "subject": "Your sign-in code",
    "body": "Your verification code is 482 913",
    "code": "482913",
    "status": "received",
    "unread": true,
    "at": "2026-10-03T18:04:11.000Z"
  }
}
```

Each request carries `X-ZeroIdentity-Signature: sha256=<hex>`, an HMAC-SHA256 of the raw body made with the subscription's secret. Check it before trusting the payload.

Answer with any 2xx status within eight seconds. Anything else is retried up to five more times with growing gaps, from about ten seconds to two hours, then dropped. The `X-ZeroIdentity-Delivery` header stays the same across retries, so a delivery that was already handled can be ignored.

## Inbound webhook

Every agent also has a URL of its own, shown in `whoami` and on the Connect tab. Anything posted to it lands in the agent's inbox as a message. If the payload is JSON with `from`, `subject` and `text` fields they are used. Otherwise the payload is kept as it is. Payloads can be up to 64 KB.

```bash
curl https://zeroidentity.app/hooks/hk_… \
  -d '{"from": "ci", "subject": "Build finished", "text": "main is green"}'
```

## SSH key

Each agent has an ed25519 key. The public half is in `whoami` and on the Connect tab. Add it to GitHub or a server the way you would add anyone's key. The agent can sign with it through `sign`, or fetch the private key with `get_private_key` to use with ssh and git.

## Phone numbers

Texts and calls are in early access. An agent has a phone number only once its owner has asked for access from the Connect tab and been given it. Until then `whoami` returns `phone: null`, and `send_sms` and `make_call` answer with an error that says so. Everything else works without one.

## Tool reference

The same list is served over MCP and at /v1.

### whoami

Who you are: your name, email address, phone number if you have one, public SSH key and inbound webhook URL.

```bash
curl "https://zeroidentity.app/v1/whoami" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### send_email

Send an email from your address. To reply in a thread, reuse its subject prefixed with "Re: ".

- `to` (string, required): Recipient email address.
- `subject` (string, required): Subject line.
- `body` (string, required): Plain-text body.
- `idempotency_key` (string): Optional. Any unique string. Sending again with the same key returns the first message instead of sending twice.

```bash
curl https://zeroidentity.app/v1/send_email \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"dana@acme.dev","subject":"Access request","body":"Could you add me to the acme org?"}'
```

### send_sms

Send a text message from your phone number. Needs a phone number, which is in early access; whoami shows whether you have one.

- `to` (string, required): Recipient phone number in E.164 format, e.g. +14155550123.
- `body` (string, required): Message text.
- `idempotency_key` (string): Optional. Any unique string. Sending again with the same key returns the first message instead of sending twice.

```bash
curl https://zeroidentity.app/v1/send_sms \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+14155550123","body":"On my way."}'
```

### make_call

Place a phone call from your number (early access, like send_sms). Your words are spoken to whoever answers; anything they say back is transcribed into the call's `reply` field, readable later with get_message.

- `to` (string, required): Phone number to call in E.164 format.
- `say` (string, required): What to say when the call is answered.

```bash
curl https://zeroidentity.app/v1/make_call \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+14155550123","say":"Hello, this is Atlas, calling to confirm Thursday's delivery."}'
```

### list_messages

List your recent messages across email, SMS, calls and webhooks, newest first. Does not mark anything as read.

- `channel` (string): Only this channel. Omit for all. One of email, sms, call, webhook.
- `direction` (string): Only received or only sent messages. One of inbound, outbound.
- `unread_only` (boolean): Only inbound messages you have not read yet.
- `limit` (number): How many to return (default 20, max 100).
- `before` (string): Only messages older than this message id. Use it to page back.

```bash
curl "https://zeroidentity.app/v1/list_messages?channel=email&unread_only=true" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### get_message

Fetch one message by id and mark it as read.

- `id` (string, required): Message id, e.g. msg_1a2b3c.

```bash
curl "https://zeroidentity.app/v1/get_message?id=msg_1a2b3c4d5e6f7a8b" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### wait_for_message

Wait for incoming messages. Returns unread inbound messages right away if there are any, otherwise blocks until one arrives or the timeout passes. Returned messages are marked as read, so calling this in a loop yields each message once.

- `channel` (string): Only this channel. Omit for all. One of email, sms, call, webhook.
- `timeout_seconds` (number): How long to wait (default 25, max 50).

```bash
curl "https://zeroidentity.app/v1/wait_for_message?timeout_seconds=25" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### sign

Sign data with your agent key (ed25519). Returns a base64 signature that verifies against your public key.

- `data` (string, required): The exact text to sign.

```bash
curl https://zeroidentity.app/v1/sign \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"data":"hello"}'
```

### get_private_key

Your agent key as an OpenSSH private key, for ssh and git. Write it to a file with mode 600 and never share it. The matching public key is in whoami.

```bash
curl "https://zeroidentity.app/v1/get_private_key" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### subscribe

Have every inbound message POSTed to a URL as JSON. Requests carry an X-ZeroIdentity-Signature header: sha256=HMAC(secret, body). Failed deliveries are retried for a few hours.

- `url` (string, required): The URL to deliver events to.

```bash
curl https://zeroidentity.app/v1/subscribe \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/zeroidentity-events"}'
```

### list_subscriptions

List your webhook subscriptions.

```bash
curl "https://zeroidentity.app/v1/list_subscriptions" \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY"
```

### unsubscribe

Remove a webhook subscription.

- `id` (string, required): Subscription id from list_subscriptions.

```bash
curl https://zeroidentity.app/v1/unsubscribe \
  -H "Authorization: Bearer $ZEROIDENTITY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id":"sub_1a2b3c4d5e6f"}'
```

## The message object

Email, texts, calls and webhook payloads all come back in one shape.

```json
{
  "id": "msg_1a2b3c4d5e6f7a8b",
  "channel": "email",
  "direction": "inbound",
  "from": "noreply@github.com",
  "to": "atlas@zeroidentity.sh",
  "subject": "Your sign-in code",
  "body": "Your verification code is 482 913",
  "code": "482913",
  "status": "received",
  "unread": true,
  "at": "2026-10-03T18:04:11.000Z"
}
```

- `channel`: email, sms, call or webhook.
- `direction`: inbound or outbound.
- `from`, `to`: addresses for email, E.164 numbers for texts and calls.
- `code`: a one-time code found in the message. Present only when there is one.
- `status`: received for inbound. For outbound: sent, delivered (to another agent), queued, completed, failed, or sandbox when the deployment is not sending for real.
- `unread`: true until the agent reads it with `get_message` or `wait_for_message`.
- `reply`, `duration`: on calls, what the other side said and the length in seconds.

## Errors and limits

Errors come back as JSON with an `error` message written for the agent to read. Over MCP the same text is returned as a tool error.

- 400: something is wrong with the request. The message says what.
- 401: the API key is missing or wrong.
- 404: there is no tool by that name.
- 429: too many requests. Wait for the number of seconds in the Retry-After header.
- 500: our fault. Try again.

Limits:

- Each agent can make 300 requests a minute.
- Free accounts send up to 1,000 emails a month and 100 a day. Pro accounts send 5,000 a month for each agent.
- Email bodies can be up to 200,000 characters and texts up to 1,600.
- An agent can have 10 event subscriptions.
