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

# OpenAI compatibility

> Talk to QX agents with OpenAI's Python and TypeScript SDKs, or plain HTTP calls, using your QX API key and the QX API's address.

The agents endpoints follow OpenAI's Responses API, so OpenAI's SDKs work as they are: point the client at `https://api.qxlabs.com/v1` with your QX API key and call `responses.create`. `model` names the agent, `builtin:qx_agent` for the QX agent or an agent's ID from its URL in the app. Each snippet below has cURL, Python and TypeScript tabs, and each one runs as it is once your key replaces `<YOUR_QX_API_KEY>`.

## Set up the client

Install the SDK with `pip install openai` or `npm install openai`, then create a client with your key. With cURL there's nothing to set up: every call sends the key as a bearer token.

```bash cURL theme={null}
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "builtin:qx_agent", "input": "Say hello in five words"}'
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

response = client.responses.create(model="builtin:qx_agent", input="Say hello in five words")
print(response.output_text)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

const response = await client.responses.create({ model: "builtin:qx_agent", input: "Say hello in five words" });
console.log(response.output_text);
```

## Send a message

A message is one call. The reply's text is in `output_text`, and `output` holds everything the agent did: its thinking as `reasoning` items, each tool call as an `mcp_call` item, and its message. To send a file, make the message a list with `input_text` and `input_file` parts, with the file as a base64 data URL.

```bash cURL theme={null}
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "builtin:qx_agent", "input": "What is on my calendar tomorrow?"}'

# With a file
DATA=$(base64 < notes.pdf | tr -d '\n')
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{"model": "builtin:qx_agent", "input": [{"role": "user", "content": [
  {"type": "input_text", "text": "Summarise this in three lines"},
  {"type": "input_file", "filename": "notes.pdf", "file_data": "data:application/pdf;base64,$DATA"}
]}]}
EOF
```

```python Python theme={null}
import base64

from openai import OpenAI

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

response = client.responses.create(model="builtin:qx_agent", input="What is on my calendar tomorrow?")
print(response.output_text)

# With a file
data = base64.b64encode(open("notes.pdf", "rb").read()).decode()
response = client.responses.create(
    model="builtin:qx_agent",
    input=[{"role": "user", "content": [
        {"type": "input_text", "text": "Summarise this in three lines"},
        {"type": "input_file", "filename": "notes.pdf", "file_data": f"data:application/pdf;base64,{data}"},
    ]}],
)
print(response.output_text)
```

```typescript TypeScript theme={null}
import { readFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

let response = await client.responses.create({ model: "builtin:qx_agent", input: "What is on my calendar tomorrow?" });
console.log(response.output_text);

// With a file
const data = readFileSync("notes.pdf").toString("base64");
response = await client.responses.create({
  model: "builtin:qx_agent",
  input: [{ role: "user", content: [
    { type: "input_text", text: "Summarise this in three lines" },
    { type: "input_file", filename: "notes.pdf", file_data: `data:application/pdf;base64,${data}` },
  ] }],
});
console.log(response.output_text);
```

## Continue a conversation

Send the last response's `id` as `previous_response_id`, or the conversation's ID as `conversation`. Either one keeps the whole conversation, which you can also open in the app's chat. A message with neither starts a new conversation.

```bash cURL theme={null}
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "builtin:qx_agent",
    "previous_response_id": "resp_8d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f_4f1a2b3c4d5e",
    "input": "Move the first one to 3 pm"
  }'
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

response = client.responses.create(model="builtin:qx_agent", input="What is on my calendar tomorrow?")
response = client.responses.create(
    model="builtin:qx_agent",
    previous_response_id=response.id,
    input="Move the first one to 3 pm",
)
print(response.output_text)
print(response.conversation.id)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

let response = await client.responses.create({ model: "builtin:qx_agent", input: "What is on my calendar tomorrow?" });
response = await client.responses.create({
  model: "builtin:qx_agent",
  previous_response_id: response.id,
  input: "Move the first one to 3 pm",
});
console.log(response.output_text);
console.log(response.conversation?.id);
```

## Stream the reply

Set `stream` to true to get the Responses API's events as they happen, or use the SDK's stream helper, which also gives you the final response. A call without streaming must finish within 240 seconds, so stream anything long. To stop an agent while it works, cancel any response of its conversation. What the agent already did stays.

```bash cURL theme={null}
curl -N https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "builtin:qx_agent", "input": "Write a short story about a lighthouse", "stream": true}'

# Stop the agent working on the conversation
curl -X POST https://api.qxlabs.com/v1/responses/resp_8d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f_4f1a2b3c4d5e/cancel \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>"
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

with client.responses.stream(model="builtin:qx_agent", input="Write a short story about a lighthouse") as stream:
    for event in stream:
        if event.type == "response.output_text.delta":
            print(event.delta, end="", flush=True)
    response = stream.get_final_response()

# Stop the agent working on the conversation
client.responses.cancel(response.id)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

const stream = client.responses.stream({ model: "builtin:qx_agent", input: "Write a short story about a lighthouse" });
for await (const event of stream) {
  if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}
const response = await stream.finalResponse();

// Stop the agent working on the conversation
await client.responses.cancel(response.id);
```

## Approvals

Some tools need approval before they run, like sending an email. `permission_mode` decides what happens, for that message only: `delegated` (the default) refuses the tool, `normal` makes the agent ask first, and `auto` lets it run. It's QX's own field, so Python sends it in `extra_body` and TypeScript needs a type that allows it.

In `normal` mode the response ends with `mcp_approval_request` items, each with the call's arguments and the agent's `reason`. Answer with `mcp_approval_response` items in `input`, on the same conversation and with no message, and the turn carries on. Each answer covers its one call.

```bash cURL theme={null}
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "builtin:qx_agent", "input": "Email the report to the team", "permission_mode": "normal"}'

# Answer the mcp_approval_request items the response ended with
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "builtin:qx_agent",
    "previous_response_id": "resp_8d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f_4f1a2b3c4d5e",
    "input": [{"type": "mcp_approval_response", "approval_request_id": "mcpr_8d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f_call_01A2b3C4d5E6f7G8", "approve": true}]
  }'
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

response = client.responses.create(
    model="builtin:qx_agent",
    input="Email the report to the team",
    extra_body={"permission_mode": "normal"},
)
approvals = [item for item in response.output if item.type == "mcp_approval_request"]
response = client.responses.create(
    model="builtin:qx_agent",
    previous_response_id=response.id,
    input=[{"type": "mcp_approval_response", "approval_request_id": item.id, "approve": True} for item in approvals],
)
print(response.output_text)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

// QX's own fields aren't in OpenAI's types, so a call that sends them uses this type
type Params = OpenAI.Responses.ResponseCreateParamsNonStreaming & {
  permission_mode?: "delegated" | "normal" | "auto";
  connected_accounts?: Record<string, string>;
};

const params: Params = { model: "builtin:qx_agent", input: "Email the report to the team", permission_mode: "normal" };
let response = await client.responses.create(params);
const approvals = response.output.filter((item) => item.type === "mcp_approval_request");
response = await client.responses.create({
  model: "builtin:qx_agent",
  previous_response_id: response.id,
  input: approvals.map((item) => ({ type: "mcp_approval_response", approval_request_id: item.id, approve: true })),
});
console.log(response.output_text);
```

## Choose an app account

When you have more than one account for an app, like two Gmail accounts, the agent uses the app's default. To use another one, send `connected_accounts` with the app and the account's ID. The [connected accounts](https://docs.qxlabs.com/api-reference/agents/list-connected-accounts.md) endpoint lists yours. It's a QX endpoint, so the SDKs read it with their own `get`.

```bash cURL theme={null}
curl https://api.qxlabs.com/v1/connected-accounts \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>"

# Use one of them on a message
curl https://api.qxlabs.com/v1/responses \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"model": "builtin:qx_agent", "input": "Give me the subject of my latest email", "connected_accounts": {"gmail": "ca_9b2e7a13"}}'
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

accounts = client.get("/connected-accounts", cast_to=object)["data"]
gmail = next(account for account in accounts if account["app"] == "gmail" and not account["default"])
response = client.responses.create(
    model="builtin:qx_agent",
    input="Give me the subject of my latest email",
    extra_body={"connected_accounts": {"gmail": gmail["id"]}},
)
print(response.output_text)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

// QX's own fields aren't in OpenAI's types, so a call that sends them uses this type
type Params = OpenAI.Responses.ResponseCreateParamsNonStreaming & {
  permission_mode?: "delegated" | "normal" | "auto";
  connected_accounts?: Record<string, string>;
};

type Account = { id: string; app: string; label: string; connection: string | null; default: boolean };
const { data: accounts } = await client.get<{ data: Account[] }>("/connected-accounts");
const gmail = accounts.find((account) => account.app === "gmail" && !account.default)!;
const params: Params = {
  model: "builtin:qx_agent",
  input: "Give me the subject of my latest email",
  connected_accounts: { gmail: gmail.id },
};
const response = await client.responses.create(params);
console.log(response.output_text);
```

## Read the history

`conversations.items.list` returns a conversation's messages and tool calls, newest first. `limit` counts turns, a user message and everything after it, and the SDKs fetch the next pages as you go.

```bash cURL theme={null}
curl "https://api.qxlabs.com/v1/conversations/8d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f/items?limit=20" \
  -H "Authorization: Bearer <YOUR_QX_API_KEY>"
```

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

client = OpenAI(base_url="https://api.qxlabs.com/v1", api_key="<YOUR_QX_API_KEY>")

response = client.responses.create(model="builtin:qx_agent", input="What is on my calendar tomorrow?")
for item in client.conversations.items.list(response.conversation.id, limit=20):
    print(item.type, item.id)
```

```typescript TypeScript theme={null}
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.qxlabs.com/v1", apiKey: "<YOUR_QX_API_KEY>" });

const response = await client.responses.create({ model: "builtin:qx_agent", input: "What is on my calendar tomorrow?" });
for await (const item of client.conversations.items.list(response.conversation!.id, { limit: 20 })) {
  console.log(item.type, item.id);
}
```

## Good to know

* Tool calls come back as `mcp_call` items. A call to one of QX's own tools shows only the command, search words, URL or path it was given, a call to an app's tool shows no arguments, and `output` is always null. A failed call has its reason in `error`.
* The agent's thinking comes as `reasoning` items, and its tokens are in `usage.output_tokens_details.reasoning_tokens`.
* A message sent while the conversation is still answering the last one joins that turn, and the response carries `steered: true`.
* Errors come in OpenAI's shape, so the SDKs raise their usual errors. An org out of credits gets a 429 with code `insufficient_quota` before any stream starts. The SDKs don't resend a message that failed, because the API asks them not to.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.