# AY Inbox for OpenAI-compatible agents

> Use AY Inbox tools from ChatGPT-style and OpenAI-compatible agents: export tools in OpenAI function format, run the call loop over HTTP, or connect over MCP.

Source: https://docs.ayinbox.com/api/openai-agents/

Any agent that does OpenAI-style function calling can use AY Inbox. Fetch `GET /v1/ai/tools?format=openai`, pass the array as the model's `tools`, and execute each call with `POST /v1/ai/tools/{name}/call`, handing `result_text` back to the model. A client that supports remote MCP with standard OAuth discovery can instead connect to `/v1/mcp` and skip the loop.

An **OpenAI-compatible agent** here means any model or framework that takes JSON Schema function definitions in a `tools` array and answers with tool calls your code executes. AY Inbox does not run the model; it supplies the tools and runs each call under your credential's permissions. The same registry backs the [MCP server](/api/mcp/) and the [REST tool surface](/api/agent-tools/).

Export the tools [#export-the-tools]

`GET https://api.ayinbox.com/v1/ai/tools` returns the tools the caller may use. Send an API key or OAuth access token as a bearer header.

| Query      | Values                | Effect                                                                                                                                                                   |
| ---------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `format`   | `openai`              | `{ type: "function", function: { name, description, parameters } }` per tool, ready for a `tools` array                                                                  |
| `format`   | `hermes`, `functions` | Aliases of `openai`                                                                                                                                                      |
| `format`   | `ayinbox` (default)   | `{ name, description, input_schema }` per tool                                                                                                                           |
| `toolsets` | comma-separated list  | Keep only tools in these groups: `inbox`, `campaigns`, `contacts`, `leads`, `crm`, `mailboxes`, `workspace`, `automation`, `forms`, `developer`, `research`, `analytics` |

```bash
curl -s "https://api.ayinbox.com/v1/ai/tools?format=openai&toolsets=inbox" \
  -H "Authorization: Bearer aybox_..."
```

Three things are true of the list:

* **It is filtered by permission.** Every tool in it is callable with the same credential, and a tool the credential lacks the scope for is not in it. See [permissions](/api/permissions/) for the scope each tool checks.
* **It never contains a send-class tool.** Nothing in it puts mail on the wire.
* **It can be long.** The full catalog can be more tools than your model or framework accepts in one request. Use `toolsets` to stay under its limit; `toolsets=inbox` is a good start for a small model. An unknown toolset answers `400 toolset_unknown`. The filter narrows the list only: calls are gated the same either way.

Each schema is a JSON Schema object, and many properties are optional (not listed in `required`). If your framework has a strict schema mode that demands every property be required, leave it off or adapt the schemas first. Every write tool also carries an optional `idempotency_key` property (see [retries](#retries)).

A key limited to some mailboxes is refused with `403 api_key_mailbox_limited`, because tools act across the workspace. Use a key without mailbox limits, or the REST endpoints, which apply them.

The call loop [#the-call-loop]

1. Fetch the tools once at session start with `format=openai`, and pass the `data` array as the model's `tools`.
2. Send the conversation. When the model answers with tool calls, take each call's `name` and its `arguments` (a JSON string in OpenAI's format).
3. `POST /v1/ai/tools/{name}/call` with the parsed arguments object as the body. An empty body means no arguments.
4. On `200`, hand `data.result_text` back to the model as the tool message. On an error, hand back the error body (see below) so the model can correct itself.
5. Repeat until the model answers without a tool call.

```python
import json, requests
from openai import OpenAI

BASE = "https://api.ayinbox.com/v1"
HEADERS = {"Authorization": "Bearer aybox_..."}
client = OpenAI()

tools = requests.get(f"{BASE}/ai/tools", params={"format": "openai", "toolsets": "inbox"},
                     headers=HEADERS).json()["data"]

def call_tool(name, arguments):
    r = requests.post(f"{BASE}/ai/tools/{name}/call", headers=HEADERS,
                      json=json.loads(arguments or "{}"))
    body = r.json()
    if r.ok:
        return body["data"]["result_text"]  # fenced as data
    return json.dumps({"code": body.get("code"), "message": body.get("message")})

messages = [{"role": "user", "content": "Summarize my unread replies from today."}]
while True:
    reply = client.chat.completions.create(model="your-model", messages=messages, tools=tools)
    msg = reply.choices[0].message
    messages.append(msg)
    if not msg.tool_calls:
        break
    for call in msg.tool_calls:
        messages.append({"role": "tool", "tool_call_id": call.id,
                         "content": call_tool(call.function.name, call.function.arguments)})
print(msg.content)
```

The `200` answer carries both `result`, the tool's output as JSON for your own code, and `result_text`, the same output fenced between `<<<UNTRUSTED_CONTENT>>>` markers with one line telling the model it is data. Mail, form answers and imported lists are written by people outside your workspace, so give the model `result_text`, not `result`. `untrusted` is `false` only for `load_skill`, whose result is your workspace's own playbook.

Errors the model can read [#errors-the-model-can-read]

Errors use the standard [error envelope](/api/error-codes/): `error`, `message`, a stable `code` and a `request_id`.

| Status | Code                                              | What it means for the agent                                                                                  |
| ------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `400`  | `bad_request`                                     | The body was not valid JSON. Fix the arguments                                                               |
| `403`  | `forbidden`                                       | The credential lacks the scope for this tool. Do not retry                                                   |
| `403`  | `reauth_required`                                 | The change needs a person to confirm it is them                                                              |
| `404`  | `not_found`                                       | No such tool, or a tool this surface never runs                                                              |
| `409`  | `idempotency_key_reused`, `tool_call_in_progress` | See [retries](#retries)                                                                                      |
| `422`  | `unprocessable`                                   | The tool ran and refused: a validation message or a missing record, written for the model to read and act on |
| `500`  | `internal_error`                                  | A server-side failure, with a request id and no detail                                                       |

Pass a `422` back verbatim: it names the argument or record that was wrong.

Retries [#retries]

The call endpoint honors the `Idempotency-Key` header like every other write in the API. A model cannot set a header, so write tools also accept an `idempotency_key` argument: a retry with the same key and arguments returns the first result for 24 hours instead of doing the work again, and the key is bound to the credential that used it.

* A key reused with different arguments answers `409 idempotency_key_reused`.
* A retry while the first call still runs answers `409 tool_call_in_progress`.
* A call refused before it ran (missing scope, bad argument) is not stored, so a retry runs.
* With the header present, the argument is ignored. Read tools store nothing.

`generate_reply_draft` requires `idempotency_key` (`422 idempotency_key_required` without one), and one key pays for one draft. Use a new key for a new draft.

Connecting over MCP [#connecting-over-mcp]

A client that speaks MCP over streamable HTTP can connect to `https://api.ayinbox.com/v1/mcp` instead of running the loop above. If your chat assistant accepts a remote MCP server URL with OAuth sign-in, this is the path to try. AY Inbox documents the protocol, not each client's settings screen, so the names of the fields you fill in depend on the client.

The OAuth flow is standard, so no AY Inbox-specific setup is needed:

1. A request without a valid token gets `401` with a `WWW-Authenticate` header pointing at the protected-resource metadata.
2. The client reads `GET /.well-known/oauth-protected-resource` ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) and `GET /.well-known/oauth-authorization-server` ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)).
3. It registers itself with `POST /v1/oauth/register` ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) as a public client. Redirect URIs may be HTTPS, loopback or a private-use scheme.
4. You sign in and approve scopes in the browser; the client finishes an authorization code exchange with PKCE (S256).
5. It holds a one-hour access token and a 90-day refresh token that rotates on every use.

The consent screen marks a self-registered client as **Not affiliated with AY Inbox** and shows the scopes it will get: only those your role covers. A self-registered client is never granted `SEND_CAMPAIGNS`, `API_KEYS` or `WRITE_WORKSPACE`. The full reference is on the [OAuth](/api/oauth/) page, and the tool list over MCP is on the [MCP server](/api/mcp/) page. If the client cannot do OAuth, an API key in the `Authorization` header works on the same URL.

What the agent cannot do [#what-the-agent-cannot-do]

The tool surfaces have no person in the loop, so some actions are not tools at all:

* **No tool sends mail.** `send_reply`, `compose_email`, placement tests and `book_meeting` are never listed or callable. A key with `WRITE_UNIBOX` can still send over REST, so do not give a model one unless you mean it to.
* **No tool lifts a suppression or opens a lasting way out.** `remove_suppression`, the webhook management tools and `fetch_url` stay with the dashboard assistant.
* **A tool that starts mail needs `SEND_CAMPAIGNS`**, for example `set_campaign_status` or linking a segment to a sending campaign.
* **No tool writes what the AI reads as instructions**, approves a first email, records consent or changes team and billing.

For any model that reads mail, mint the key from the **Agent: read and draft** preset. It reads the inbox, campaigns and contacts and can have the AI write replies with `generate_reply_draft`, but it holds no scope that writes or sends. The reasoning behind giving agents narrow keys is in [cold email for AI agents](/learn/cold-email-for-ai-agents/).

## Frequently asked questions

### Can I use AY Inbox with OpenAI function calling?

Yes. GET /v1/ai/tools?format=openai returns the tools your credential may use as OpenAI function objects. Pass them as the model's tools and execute each call with POST /v1/ai/tools/{name}/call.

### Can an OpenAI-compatible agent send email through AY Inbox tools?

No. Send-class tools such as send_reply, compose_email and book_meeting are never listed or callable on the tool surfaces. An agent reads, searches, labels and drafts; sending stays with REST routes and a person.

### Which API key should I give a model that reads my inbox?

A key made from the Agent: read and draft preset. It holds every read scope plus AI_DRAFT and no scope that writes or sends, so it cannot send over the tools or over REST.

### Can ChatGPT or another chat assistant connect to AY Inbox over MCP?

AY Inbox serves MCP at https://api.ayinbox.com/v1/mcp with standard OAuth discovery and dynamic client registration. A client that implements those standards can connect; AY Inbox documents the protocol, not each client's settings screen.

### How do I stay under a model's tool limit?

Narrow the list with ?toolsets=, for example toolsets=inbox. The filter only shortens the list; every call is still gated by the credential's scopes.
