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.
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 and the REST tool surface.
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 |
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 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
toolsetsto stay under its limit;toolsets=inboxis a good start for a small model. An unknown toolset answers400 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).
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
- Fetch the tools once at session start with
format=openai, and pass thedataarray as the model'stools. - Send the conversation. When the model answers with tool calls, take each call's
nameand itsarguments(a JSON string in OpenAI's format). POST /v1/ai/tools/{name}/callwith the parsed arguments object as the body. An empty body means no arguments.- On
200, handdata.result_textback to the model as the tool message. On an error, hand back the error body (see below) so the model can correct itself. - Repeat until the model answers without a tool call.
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 use the standard error envelope: 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 |
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
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
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:
- A request without a valid token gets
401with aWWW-Authenticateheader pointing at the protected-resource metadata. - The client reads
GET /.well-known/oauth-protected-resource(RFC 9728) andGET /.well-known/oauth-authorization-server(RFC 8414). - It registers itself with
POST /v1/oauth/register(RFC 7591) as a public client. Redirect URIs may be HTTPS, loopback or a private-use scheme. - You sign in and approve scopes in the browser; the client finishes an authorization code exchange with PKCE (S256).
- 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 page, and the tool list over MCP is on the MCP server page. If the client cannot do OAuth, an API key in the Authorization header works on the same URL.
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 andbook_meetingare never listed or callable. A key withWRITE_UNIBOXcan 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 andfetch_urlstay with the dashboard assistant. - A tool that starts mail needs
SEND_CAMPAIGNS, for exampleset_campaign_statusor 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.
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.