---
title: "OpenAI Responses and Agents API"
description: "Attach Anlyon to the OpenAI Responses API or Agents API as a remote MCP server with an API key, so the model calls your actions without a credential and an approver decides the risky ones."
icon: "bot"
---

An agent you build on OpenAI's Responses API or Agents API can call remote MCP servers as tools. Point it at Anlyon and the model sees your workspace's actions as tools. It names an action and passes input; Anlyon adds the credential at dispatch, applies your limits, waits for an approver when a policy says so, and records a receipt. Your OpenAI request never carries a provider credential, only an Anlyon API key whose scopes are exactly what the model can do.

This covers calls routed through Anlyon. A tool your own code calls directly is not governed by Anlyon.

The statements about OpenAI's APIs on this page come from OpenAI's documentation as checked on 2026-10-27. Check [OpenAI's MCP tool guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp) and the [Responses API reference](https://developers.openai.com/api/reference/resources/responses/methods/create) for current behaviour.

## Decide where the approval happens

OpenAI can pause an MCP tool call for approval (`require_approval`). Your own code answers that pause, in your process, and nothing records it outside your logs. Use it when the developer running the agent is the reviewer.

Use Anlyon's approval when the decision belongs to someone else, such as a teammate, a second approver, or the end user the call acts for, or when you need the decision and the call on record. Anlyon parks the call, routes it to the approver, runs exactly the request they reviewed, and records the outcome.

## Choose the server and the key

The Responses API sends the `authorization` value as a bearer token on every request and does not store it. It does not run an OAuth flow for you, so give it an Anlyon API key.

| Server | Set `ANLYON_MCP_URL` to |
| --- | --- |
| Anlyon's hosted MCP server | `https://api.anlyon.com/mcp` |
| One integration's server | `https://api.anlyon.com/mcp/<integration>`, such as `https://api.anlyon.com/mcp/github` |
| The npm package, run over HTTP yourself | Your host's HTTPS address, ending in `/mcp` |

Both hosted servers accept an API key as the bearer. To run the MCP server over HTTP yourself instead, on a host OpenAI can reach over HTTPS:

```bash
npx -y @anlyonhq/mcp-server --http   # listens on PORT (default 3001), serves POST /mcp
```

It holds no credential of its own. Each request's bearer is the key the tools act with.

Create two keys in the [console](https://console.anlyon.com) under **Settings → API keys**:

- **Agent key** (`ANLYON_AGENT_KEY`): `actions:read`, `actions:invoke`, `approvals:read`. This is the MCP bearer. Do not give it `approvals:decide`.
- **Operator key** (`ANLYON_OPERATOR_KEY`): `approvals:decide` and `actions:read`, held by the person or service that reviews. A key can never decide an approval it requested. Add `actions:resolve` if this key will also record the outcome of an `unknown` invocation.

## Call a gated action from the Responses API

Set `require_approval: "never"` on the OpenAI side. The approval lives in Anlyon, which parks the call for your approver. Asking OpenAI to pause as well would put a second approval, one Anlyon does not record, in your own process in front of the one that counts.

This example asks the model to change a support ticket's priority through an action named `update-ticket-priority` that requires approval, then to wait for the decision:

```javascript Responses API
// Node 22+. Needs OPENAI_API_KEY, OPENAI_MODEL, ANLYON_MCP_URL and ANLYON_AGENT_KEY.
const response = await fetch('https://api.openai.com/v1/responses', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: process.env.OPENAI_MODEL,
    tools: [{
      type: 'mcp',
      server_label: 'anlyon',
      server_url: process.env.ANLYON_MCP_URL,
      authorization: process.env.ANLYON_AGENT_KEY,
      // The approval happens in Anlyon, not here.
      require_approval: 'never',
      allowed_tools: ['action_update-ticket-priority', 'wait_for_approval'],
    }],
    input:
      'Set ticket T-1042 to priority high with action_update-ticket-priority. ' +
      'If it is parked for approval, call wait_for_approval with the approvalId and report the decision.',
  }),
});

const result = await response.json();
if (!response.ok) throw new Error(JSON.stringify(result));
for (const item of result.output ?? []) {
  if (item.type === 'mcp_call') {
    console.log(JSON.stringify({ tool: item.name, output: item.output, error: item.error }));
  }
}
```

`allowed_tools` limits the model to the two tools it needs. Each enabled action is a tool named `action_<name>`. See [Tools](/agents/mcp-servers#tools) for the rest.

What happens:

1. The model calls `action_update-ticket-priority`. Anlyon records the invocation, creates an approval, and returns `Awaiting human approval` with its `apr_...` id. Nothing has been sent to the destination.
2. The model calls `wait_for_approval`, which holds the call open (60 seconds by default, 120 at most) until an approver decides. If it comes back still `pending`, the model calls it again.
3. Your approver approves in the Anlyon inbox, or with the operator key:

```bash Decide
curl -sS -X POST "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/approvals/$APPROVAL_ID/approve" \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"note": "Checked the ticket; priority change is correct."}'
```

4. Anlyon executes the approved request snapshot using the credentials bound to that action. The snapshot freezes what the approver reviewed. It does not preserve a credential that has since been revoked, and destination bindings are re-checked at dispatch.
5. `wait_for_approval` returns the approval record. The model reads the decision (`approved`, `denied` or `expired`) and who made it. `payload.invocationId` names the invocation it gated:

```json
{
  "id": "apr_...",
  "kind": "action",
  "status": "approved",
  "payload": { "invocationId": "inv_...", "actionName": "update-ticket-priority", "method": "PATCH" },
  "decidedBy": { "type": "api_key", "id": "...", "displayName": null },
  "requiredApprovals": 1,
  "approvedCount": 1
}
```

A denied or expired approval comes back with that status, and the gated request is never sent.

## Read the outcome

The approval says who decided. The invocation says what happened when Anlyon made the request. Read it with any key that has `actions:read`:

```bash Outcome
curl -sS "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/actions/invocations/$INVOCATION_ID" \
  -H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
```

`data.status` is `succeeded`, `failed` or `unknown`, with `responseStatus` and the response body. A governed action also carries `effectId` and a receipt `grade`. The same record is in the console's invocation log.

### When the outcome is `unknown`

Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying: check the destination, then record what happened with `POST /api/v2/actions/invocations/{id}/resolve`. That call needs the `actions:resolve` scope. No key receives it unless its creator names it, and the key that made the invocation cannot resolve it. See [When the outcome is unknown](/execution/when-the-outcome-is-unknown).

## Act for one of your users

When the agent acts in one of your users' accounts, your server names the user in the `headers` field. The model never sees or sets it. Use a key created with **Require an end user** so a request without the header is refused.

```javascript For one user
// userId comes from your own session, never from the model.
const tool = {
  type: 'mcp',
  server_label: 'anlyon',
  server_url: 'https://api.anlyon.com/mcp',
  authorization: process.env.ANLYON_API_KEY,
  headers: { 'Anlyon-End-User': userId },
  require_approval: 'never',
  allowed_tools: ['action_gmail-send', 'wait_for_approval'],
};
```

Each call then uses that user's own connected Gmail, counts toward that user's [limits](/execution/impact-limits), and waits for that user's approval when the policy is **decided by the end user**. If the user has not connected Gmail, the tool result is `{ "status": "connect_required", "integration": "gmail", "connectUrl": "...", "message": "..." }`: nothing was sent, and your app shows `connectUrl` as a **Connect** button. See [Act for one of your users](/agents/mcp-servers#act-for-one-of-your-users).

## Credentials

The model never sees a provider credential: an action references `{{secret:NAME}}` or a connection, and Anlyon resolves it at dispatch. A secret is bound to a destination and a placement, checked again at fire time. The MCP bearer itself is an Anlyon API key. OpenAI receives it on each request and does not store it.

## More than one approver

To require more than one decision, set an approval policy on the action. N counts distinct **credentials** for API-key deciders and distinct **users** for dashboard and OAuth deciders: two keys held by one person are two deciders.

## The Agents API

The Agents API takes the same MCP server as a tool with an HTTP transport. OpenAI's example passes `authorization` with the `Bearer` prefix:

```json
{
  "type": "mcp",
  "server_label": "anlyon",
  "transport": {
    "type": "http",
    "server_url": "https://api.anlyon.com/mcp",
    "authorization": "Bearer anlyon_live_...",
    "headers": { "Anlyon-End-User": "user_123" }
  },
  "connection_origin": "service"
}
```

Leave out `headers` to act with your workspace's own connections. With `connection_origin: "service"` (the default), OpenAI makes the HTTP connection. The hand-off is identical: the call parks in Anlyon, an approver decides, and Anlyon executes and records the outcome. Check [OpenAI's MCP connection reference](https://developers.openai.com/api/docs/guides/agents-api/tools/mcp) for the current field names.

## Fix refused calls

| What you see | Why | What to do |
| --- | --- | --- |
| OpenAI cannot list the server's tools, and Anlyon answered `401` | The key is unknown, revoked or expired, or `authorization` is empty | Check `ANLYON_AGENT_KEY` |
| `API key is missing required scope: actions:invoke` in an `mcp_call` error | The key lacks that scope | Add the scope to the key |
| `400 END_USER_REQUIRED` | The key requires an end user and no `Anlyon-End-User` header was sent | Add the header |
| `400 INVALID_END_USER` | The id has characters outside letters, digits and `. _ : @ -`, or is over 128 characters | Use your own user id in that form |
| A tool in `allowed_tools` is missing | The action is disabled, renamed, or not on this integration's server | Check the action in the console |
| `"status": "connect_required"` | The end user has not connected the integration. Nothing was sent | Send the user to `connectUrl` |

## How this page is checked

A script runs the `Responses API`, `Decide` and `Outcome` code blocks on this page in CI, against the npm package's HTTP transport and a local API fixture.

## Related

- [MCP servers](/agents/mcp-servers): the full reference
- [Claude Agent SDK](/agents/claude-agent-sdk): the same pattern with Anthropic's agent SDK
- [Approvals](/trust-control/approvals): policies, approvers and N-of-M
- [Workspace connections and end-user connections](/execution/end-user-connections)
