> ## Documentation Index
> Fetch the complete documentation index at: https://agentheya.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI API

> Publish your agent as an OpenAI-compatible model so n8n, OpenRouter, Cursor and the OpenAI SDKs can call it directly.

## Overview

An OpenAI API endpoint gives your agent a **base URL** that speaks the OpenAI protocol. Anything that already knows how to talk to OpenAI can talk to your agent — you paste the URL where the tool asks for an OpenAI base URL, paste an API key, and your agent shows up as a model.

That covers, among others:

* **n8n** — the OpenAI credential's *Base URL* field, then any OpenAI node
* **OpenRouter** — adding your agent as a private / BYOK model
* **Cursor**, **Continue**, **LibreChat**, **Open WebUI** — custom OpenAI-compatible provider
* **OpenAI SDKs** — `OpenAI(base_url=…, api_key=…)` in Python, JS, and the rest
* **LangChain / LlamaIndex** — any `ChatOpenAI`-style client that accepts a base URL

Create as many endpoints as you need. Each has its own URL, its own settings, and its own processing pipeline — so a workflow automation and a customer-facing integration can point at the same agent with completely different instructions and limits.

To add one, click **Add Endpoint**, give it a name, and click Save. Each card has an **ON / OFF** toggle, drag-to-reorder, and a delete button.

Every endpoint speaks **both protocol families, always** — there is nothing to choose:

* **Chat Completions** (`/chat/completions`, plus the legacy `/completions`) — the classic protocol most integrations use.
* **Responses** (`/responses`) — OpenAI's newer stateful protocol, with server-stored responses, `previous_response_id` chaining, background mode and caller-supplied function tools.

Point any client at the same base URL; whichever route it calls, the same agent answers.

## Connecting a client

Two values go into the client: the **Base URL** from the card, and an API key from your **API Keys** page.

```
Base URL:  https://api.agentheya.com/v1/agents/youragentid/yourtokenhere
API key:   your-api-key
Model:     anything — pick from /models or type any value
```

Subscribers see the same endpoints with their own personal Base URL — the same
address with one extra segment, `…/v1/agents/youragentid/yoursubscriberid/yourtokenhere` —
which works only with that subscriber's own API key.

Do **not** add `/chat/completions` to the base URL — the client appends the route itself. If a tool asks for the "full endpoint URL" rather than a base URL, that is the one case where you add `/chat/completions` yourself.

The same call from the most common clients:

<CodeGroup>
  ```bash curl theme={null}
  curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/chat/completions' \
    -H 'Authorization: Bearer your-api-key' \
    -H 'Content-Type: application/json' \
    -d '{
      "model": "default",
      "messages": [{"role": "user", "content": "Where is my order #1001?"}]
    }'
  ```

  ```python Python theme={null}
  from openai import OpenAI

  client = OpenAI(
      base_url="https://api.agentheya.com/v1/agents/youragentid/yourtokenhere",
      api_key="your-api-key",
  )

  reply = client.chat.completions.create(
      model="default",
      messages=[{"role": "user", "content": "Where is my order #1001?"}],
  )
  print(reply.choices[0].message.content)
  ```

  ```javascript JavaScript theme={null}
  import OpenAI from "openai";

  const client = new OpenAI({
    baseURL: "https://api.agentheya.com/v1/agents/youragentid/yourtokenhere",
    apiKey: "your-api-key",
  });

  const reply = await client.chat.completions.create({
    model: "default",
    messages: [{ role: "user", content: "Where is my order #1001?" }],
  });
  console.log(reply.choices[0].message.content);
  ```

  ```python LangChain theme={null}
  from langchain_openai import ChatOpenAI

  llm = ChatOpenAI(
      base_url="https://api.agentheya.com/v1/agents/youragentid/yourtokenhere",
      api_key="your-api-key",
      model="default",
  )
  print(llm.invoke("Where is my order #1001?").content)
  ```
</CodeGroup>

### Streaming

Set `"stream": true` and the answer arrives as standard SSE chunks; add `stream_options.include_usage` for a final usage chunk:

```python theme={null}
stream = client.chat.completions.create(
    model="default",
    messages=[{"role": "user", "content": "Summarize your return policy."}],
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="")
```

### Sending an image

Message content can be multi-part, exactly like OpenAI's vision format — useful when the agent should read a photo, screenshot or scanned document:

```bash theme={null}
curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/chat/completions' \
  -H 'Authorization: Bearer your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "default",
    "messages": [{
      "role": "user",
      "content": [
        {"type": "text", "text": "What does this delivery label say?"},
        {"type": "image_url", "image_url": {"url": "https://example.com/label.jpg"}}
      ]
    }]
  }'
```

### Routes

Every endpoint answers all of these routes under its base URL:

| Route                             | Purpose                                                                                                       |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `POST /chat/completions`          | The main one. Streaming (`"stream": true`) and non-streaming.                                                 |
| `POST /completions`               | The legacy text-completion protocol, for older clients.                                                       |
| `POST /responses`                 | Create a response. Streaming and non-streaming, full parameter surface.                                       |
| `GET /responses/{id}`             | Retrieve a stored response (`?stream=true&starting_after=N` replays its events).                              |
| `DELETE /responses/{id}`          | Delete a stored response.                                                                                     |
| `POST /responses/{id}/cancel`     | Cancel a `background: true` response.                                                                         |
| `GET /responses/{id}/input_items` | The response's full input list, cursor-paginated.                                                             |
| `POST /responses/input_tokens`    | Approximate input-token count for a request body.                                                             |
| `GET /models`                     | Lists this endpoint's single model. Most clients probe this on first connect.                                 |
| `GET /models/{id}`                | Retrieves a model. Any id succeeds — the model value never selects anything, so validation probes can’t fail. |

## Authentication

Use any API key from your **API Keys** page:

```http theme={null}
Authorization: Bearer your-api-key
```

The key must belong to the same agent as the endpoint. A key for a different agent is rejected as an invalid key.

Limits: **50 MiB** per request, **60 requests per minute** per key, and **5 concurrent** requests per key. This budget is shared with your agent's other API surfaces, so a key can't get more by switching between them.

## The model name

There is nothing to configure. The endpoint advertises your agent's name in `/models` (for clients with a model picker), and the `model` a caller sends is recorded and then ignored — `default`, the agent name, a typo, anything at all still gets an answer from your agent.

The model value does **not** pick a model. Which LLM actually answers is decided by your agent's own **LLM Routing** settings.

<Note>The **Advertised context window** and **Advertised max output** fields are informational. Some clients (OpenRouter, LiteLLM) display them or use them to decide when to truncate. They don't change what the model actually accepts — that comes from whichever model your agent routes to.</Note>

## The Responses API

Every endpoint implements the stateful Responses protocol the way api.openai.com does:

* **Stored responses.** Responses are stored by default (`"store": false` opts out) and stay retrievable for 30 days at `GET /responses/{id}`.
* **Conversation chaining.** Pass `previous_response_id` and the agent sees the whole prior exchange — you send only the new input. Or pass `conversation: "your-thread-id"` and the endpoint threads turns implicitly under that id. A stored response is only visible to the API-key user that created it, on the endpoint that created it.
* **Caller function tools.** Send `tools: [{type: "function", …}]` and the agent can call *your* functions: the turn ends with a `function_call` output item, you execute the function, and you continue with `previous_response_id` plus a `function_call_output` input item — the standard OpenAI tool loop. The agent's own server-side tools keep running invisibly as usual; `tool_choice` (`auto`/`none`/`required`/a specific function) and `parallel_tool_calls` are honoured.
* **Background mode.** `"background": true` returns immediately with `"status": "queued"`; poll `GET /responses/{id}` for the result, or cancel it with `POST /responses/{id}/cancel`.
* **Structured output.** `text.format` with `json_object` or a `json_schema` instructs the agent to answer as pure JSON (best-effort — the schema is enforced by instruction, not by a grammar).
* **The rest of the surface.** `instructions` (applied per call, never inherited across a chain), `metadata` (up to 16 keys, echoed back), `max_output_tokens` (a real ceiling for the turn), `include`, `stream_options.include_obfuscation`, `truncation`, `service_tier`, `safety_identifier` and `user` are all accepted; sampling knobs (`temperature`, `top_p`, `top_logprobs`) are echoed back but the agent's own LLM Routing settings decide actual sampling. `prompt` templates and hosted tool types (`web_search`, `file_search`, …) are refused with a clear error — your agent brings its own tools.

### Responses examples

**Multi-turn with `previous_response_id`** — send only the new input; the endpoint replays the rest:

```python theme={null}
first = client.responses.create(model="default", input="What is the capital of France?")
followup = client.responses.create(
    model="default",
    previous_response_id=first.id,
    input="And its population?",
)
```

**Your own function tools** — the standard OpenAI tool loop, with your function running on your side:

```python theme={null}
tools = [{
    "type": "function",
    "name": "get_stock_level",
    "description": "Current stock for a SKU in our warehouse system.",
    "parameters": {
        "type": "object",
        "properties": {"sku": {"type": "string"}},
        "required": ["sku"],
    },
}]

r = client.responses.create(model="default", input="Is SKU A-1042 in stock?", tools=tools)
call = next(item for item in r.output if item.type == "function_call")

result = my_warehouse_lookup(call.arguments)   # you run the function

final = client.responses.create(
    model="default",
    previous_response_id=r.id,
    input=[{"type": "function_call_output", "call_id": call.call_id, "output": result}],
    tools=tools,
)
print(final.output_text)
```

**Background mode** — kick off long work, poll for the result:

```bash theme={null}
# start (returns immediately with "status": "queued")
curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/responses' \
  -H 'Authorization: Bearer your-api-key' \
  -H 'Content-Type: application/json' \
  -d '{"model": "default", "input": "Compare our three suppliers on price and lead time.", "background": true}'

# poll until "status": "completed"
curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/responses/resp_abc123' \
  -H 'Authorization: Bearer your-api-key'
```

**Structured output** — ask for JSON your automation can parse directly:

```python theme={null}
r = client.responses.create(
    model="default",
    input="Extract the customer name and order number from: 'Hi, Dana Miller here about order 88123'",
    text={"format": {
        "type": "json_schema",
        "name": "order_ref",
        "schema": {
            "type": "object",
            "properties": {"customer": {"type": "string"}, "order_number": {"type": "string"}},
            "required": ["customer", "order_number"],
        },
    }},
)
print(r.output_text)   # {"customer": "Dana Miller", "order_number": "88123"}
```

## Conversations

With **Chat Completions** the protocol is stateless: the client sends the whole conversation on every call, and that history is what the agent reads. So multi-turn works out of the box — keep appending to `messages` the way you would with OpenAI. With the **Responses API**, use `previous_response_id` or `conversation` instead (see above).

For the **Inbox**, consecutive calls that begin with the same first message are grouped into one thread, so a conversation appears as one entry instead of one per turn.

To control that grouping explicitly, send a chat id:

| Where        | Names                                                                 |
| ------------ | --------------------------------------------------------------------- |
| Header       | `X-Chat-Id`, `X-Conversation-Id`, `X-Session-Id`                      |
| Query string | `?chat_id=`, `?conversation_id=`, `?session_id=`                      |
| Body         | `metadata.session_id`, `metadata.conversation_id`, `metadata.chat_id` |

An id may be up to 128 characters of letters, digits and `. _ : @ + -`. This is useful when each run of a workflow should be its own thread — n8n's execution id makes a good chat id.

Two calls with the same id land in the same conversation:

```bash theme={null}
curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/chat/completions' \
  -H 'Authorization: Bearer your-api-key' \
  -H 'Content-Type: application/json' \
  -H 'X-Chat-Id: ticket-4821' \
  -d '{"model": "default", "messages": [{"role": "user", "content": "Customer says the invoice total looks wrong."}]}'

# later, same thread — the Inbox shows one conversation, not two
curl 'https://api.agentheya.com/v1/agents/youragentid/yourtokenhere/chat/completions' \
  -H 'Authorization: Bearer your-api-key' \
  -H 'Content-Type: application/json' \
  -H 'X-Chat-Id: ticket-4821' \
  -d '{"model": "default", "messages": [{"role": "user", "content": "Customer says the invoice total looks wrong."}, {"role": "assistant", "content": "…previous answer…"}, {"role": "user", "content": "They replied — the VAT rate is the issue."}]}'
```

## Tool activity in responses

Your agent runs its tools server-side; a caller never sees `tool_calls`. **Show tool activity in the answer** decides whether that work is visible:

* **On** — tool use appears inline as italic lines (`_searching the knowledge base…_`), the way a chat UI shows it. Good for anything a person reads.
* **Off** — the response is only the final answer. Usually what you want for an automation that parses the output.

## Processing pipeline

Each endpoint card exposes the shared incoming-event pipeline. Every request flows through these stages before the agent sees it.

**By default every request reaches the agent.** The stages below exist to change that.

Because the caller is waiting on an open connection, an endpoint always answers: if a stage decides not to run the agent, the caller receives a standard OpenAI error rather than an empty completion or a hang.

### Event pre-processor

Optional code that runs in a sandbox **before** the classifier and the agent. Define `handle(event, ctx)` and return a verdict — `pass_to_agent`, `drop`, `run_tool`, or `respond`.

`event.payload` is the parsed request body (`{ model, messages, … }`), so you can inspect or rewrite the whole conversation. A `pass_to_agent` verdict returning `{ messages: [...] }` **replaces** the history the agent sees — that is how you redact, truncate or rewrite an incoming conversation. A `respond` verdict answers the caller directly without an agent run, which is a cheap way to serve canned replies.

See the [Event pre-processor guide](/sidebar-menu/event-pre-processor) for the full contract and worked examples.

### Interpreters

When one endpoint serves several different callers or workflows, **Interpreters** recognise each kind of request and inject a plain-language hint so the agent knows what it's looking at. Each interpreter matches on **JSON field presence** (dot-paths, e.g. `metadata.workflow`) or **regex** against the raw request body, tested in order — first match wins.

See the [Interpreters guide](/sidebar-menu/event-interpreters) for the matching rules and examples.

### Event classifier

Optional. When enabled, an LLM reads each request before the agent runs and picks **React** (answer it) or **Drop** (refuse it). Off → every request goes straight to the agent.

Give it a prompt describing what this endpoint is for, e.g. *"Only answer questions about our products; drop anything else."* A dropped request gets HTTP `403` with `"code": "request_blocked"`, so the caller knows it was refused rather than failing silently.

### Context and Notes

* **Context** — instructions for the agent that apply to every call on this endpoint (e.g. *"Callers here are n8n workflows. Answer in one short paragraph, no markdown."*). Added to the agent's context for every request.
* **Notes** — private operator notes, never seen by the agent.

Context is the field that makes two endpoints on the same agent behave differently: one can answer conversationally for a chat UI while another returns terse, parseable output for an automation.

## Spend budget

<Note>Owner-only advanced control.</Note>

Each endpoint can carry its own AI **spend cap** so a runaway workflow can't drain your credits. Set a **Spend cap (credits)** and a **Budget period** — **Daily**, **Weekly** (Mon–Sun), or **Monthly** (calendar month). Once the cap is reached, callers receive HTTP `429` with `"code": "endpoint_budget_exceeded"` until the period resets at UTC boundaries. Leave the cap at `0` for no limit.

## Errors

Errors use the standard OpenAI envelope, so client libraries raise their normal exceptions:

```json theme={null}
{ "error": { "message": "…", "type": "invalid_request_error", "code": "request_blocked" } }
```

| Status | Code                       | Meaning                                                                                                 |
| ------ | -------------------------- | ------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_request`          | Malformed body, empty `messages`, or `n` greater than 1.                                                |
| `401`  | `invalid_api_key`          | Missing key, bad key, or a key belonging to another agent.                                              |
| `403`  | `request_blocked`          | A classifier or pre-processor refused the request.                                                      |
| `404`  | `endpoint_not_found`       | No endpoint at that base URL.                                                                           |
| `404`  | `response_not_found`       | No stored response with that id for this API key (deleted, expired, `store: false`, or someone else's). |
| `429`  | `rate_limit_exceeded`      | Too many requests, or too many at once, for this key.                                                   |
| `429`  | `endpoint_budget_exceeded` | The endpoint's spend cap is reached for this period.                                                    |
| `503`  | `endpoint_disabled`        | The endpoint's toggle is **OFF**.                                                                       |

## Testing your endpoint

Each card has a **Test** section. Pick a route, paste a request body, add an optional chat id, and click **Run Test**. The request goes through the real pipeline — interpreters, pre-processor and classifier all run — and the panel shows:

* which interpreter matched, what the pre-processor and classifier decided, and the resulting outcome;
* the **exact response the caller would receive**, including the HTTP status;
* the **exact prompt and data the agent receives** — the context block added to its system prompt, any pipeline hint, and the full message array.

The agent itself is not run and nothing is written to the conversation. Two things do happen for real, so the result is a rehearsal rather than a guess: your pre-processor executes (any side effects it has will occur) and the classifier is a real LLM call (it costs credits).

Recent traffic appears in **Recent API Requests** on this page — time, endpoint, route, requested model, source IP, status, outcome and detail — and in the **Inbox**, as the conversation each request belonged to.

## Setting it up in n8n

1. In n8n, add an **OpenAI** credential.
2. Set **API Key** to your Agentheya API key.
3. Set **Base URL** to the endpoint's Base URL (no `/chat/completions`).
4. In any OpenAI node, pick your agent from the model dropdown (n8n loads the list from the endpoint) — or type any value; the model name never changes which agent answers.

For a chat-style workflow, send n8n's execution id as `X-Chat-Id` so each run is its own conversation in the Inbox.

## Adding it to OpenRouter

Use OpenRouter's **BYOK / private model** flow, pointing it at the endpoint's Base URL with your Agentheya API key. Use your agent's name as the model id (any value works). OpenRouter probes `/models` to validate the provider, which the endpoint answers with your single model and its advertised context window.
