Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

OpenAI Responses and Agents API

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.

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 and the Responses API reference 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:

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 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:

// 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 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:
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."}'
  1. 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.
  2. 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:
{
  "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:

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.

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.

// 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, 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.

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:

{
  "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 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.

Was this page helpful?