OpenAI API
There are two routes. In the first, OpenAI's servers call our MCP server for you. In the second, you give the model our function definitions and your own code makes each call. Choose the second when you want to keep the key and the data in your own code.
Not open yet. Opens with early access.
On this page
Before you start
- An OpenAI API key and a model that supports remote MCP tools (route one) or function calling (route two). Replace
YOUR_MODELbelow with yours. - An Understanding API key from the console. You need an account first. Keep it on your server.
- No sign-in. Both routes use the key. ChatGPT itself is different; see ChatGPT.
Set it up
Route one: the remote MCP tool
curl https://api.openai.com/v1/responses \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "YOUR_MODEL", "input": "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", "tools": [{ "type": "mcp", "server_label": "understanding", "server_url": "https://api.theunderstanding.company/mcp/chatgpt", "headers": { "Authorization": "Bearer '"$UNDERSTANDING_API_KEY"'" }, "allowed_tools": ["check_restricted_merchant", "classify_spend_type", "match_receipt", "pair_refund", "check_return_window", "route_support_message", "ask_closed_question"], "require_approval": "never" }] }'
import os from openai import OpenAI client = OpenAI() response = client.responses.create( model="YOUR_MODEL", input="Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", tools=[{ "type": "mcp", "server_label": "understanding", "server_url": "https://api.theunderstanding.company/mcp/chatgpt", "headers": {"Authorization": f"Bearer {os.environ['UNDERSTANDING_API_KEY']}"}, "allowed_tools": ["check_restricted_merchant", "classify_spend_type", "match_receipt", "pair_refund", "check_return_window", "route_support_message", "ask_closed_question"], "require_approval": "never", }], ) print(response.output_text)
Four settings decide whether this works the way you expect.
require_approvaldefaults toalways. Every tool call then comes back as anmcp_approval_requestthat your code must answer with anmcp_approval_responsebefore the model continues. The tools only read and classify, soneveris reasonable, or{ "never": { "tool_names": [ ... ] } }. It is your choice.allowed_toolsdefaults to every tool the server lists. Name the ones you need. The list above is the seven banking question tools the ChatGPT address offers;get_usageis left out, and so are the three health tools, which that address does not offer.- OpenAI documents two fields. OpenAI documents
authorizationas an OAuth access token and listsheadersin its API reference. Tryheadersfirst, as above. If OpenAI refuses it, use"authorization": "<your key>"and dropheaders. The server accepts a bare key as well asBearer <key>. - OpenAI does not store the credential. Send it on every request.
The server must be reachable from the internet over HTTPS, and it speaks Streamable HTTP. Nothing in the result is an instruction: it holds the answer, the probability, the reasons and the abstain flag.
Route two: function calling
The definitions come from our server, in OpenAI's strict form (additionalProperties is false, every property is required, and an optional one accepts null):
curl "https://api.theunderstanding.company/v1/tools?format=openai"The reply is { "format": "openai", "tools": [ ... ], "execute": { ... } }. Pass tools to the Responses API. When the model returns a function_call, send its arguments, exactly as it produced them, to POST https://api.theunderstanding.company/v1/tools/{name} with your key. Nulls for optional arguments are fine. Hand the answer back as the call's output. Save this as understanding-openai.mjs:
const API = "https://api.theunderstanding.company"; const ours = (path, init = {}) => fetch(API + path, { ...init, headers: { Authorization: `Bearer ${process.env.UNDERSTANDING_API_KEY}`, "Content-Type": "application/json" }, }); const { tools } = await (await ours("/v1/tools?format=openai")).json(); async function runTool(name, args) { const res = await ours(`/v1/tools/${name}`, { method: "POST", body: JSON.stringify(args) }); const body = await res.json(); if (!res.ok) return JSON.stringify({ error: body.error }); const { abstain, ...rest } = body; // put abstain first so the model sees it first return JSON.stringify({ abstain, ...rest }); } async function respond(request) { const res = 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(request), }); const json = await res.json(); if (!res.ok) throw new Error(json.error?.message ?? `HTTP ${res.status}`); return json; } const model = "YOUR_MODEL"; let response = await respond({ model, input: "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", tools }); for (let round = 0; round < 5; round++) { const calls = response.output.filter((item) => item.type === "function_call"); if (calls.length === 0) break; const outputs = []; for (const call of calls) { outputs.push({ type: "function_call_output", call_id: call.call_id, output: await runTool(call.name, JSON.parse(call.arguments || "{}")) }); } response = await respond({ model, previous_response_id: response.id, input: outputs, tools }); } const message = response.output.find((item) => item.type === "message"); console.log(message?.content?.map((part) => part.text).join("\n"));
import json import os import requests API = "https://api.theunderstanding.company" OURS = {"Authorization": f"Bearer {os.environ['UNDERSTANDING_API_KEY']}", "Content-Type": "application/json"} THEIRS = {"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", "Content-Type": "application/json"} MODEL = "YOUR_MODEL" tools = requests.get(f"{API}/v1/tools?format=openai", timeout=30).json()["tools"] def run_tool(name, args): res = requests.post(f"{API}/v1/tools/{name}", headers=OURS, json=args, timeout=60) body = res.json() if not res.ok: return json.dumps({"error": body["error"]}) return json.dumps({"abstain": body["abstain"], **{k: v for k, v in body.items() if k != "abstain"}}) def respond(request): res = requests.post("https://api.openai.com/v1/responses", headers=THEIRS, json=request, timeout=120) res.raise_for_status() return res.json() response = respond({"model": MODEL, "input": "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", "tools": tools}) for _ in range(5): calls = [item for item in response["output"] if item["type"] == "function_call"] if not calls: break outputs = [ {"type": "function_call_output", "call_id": c["call_id"], "output": run_tool(c["name"], json.loads(c["arguments"] or "{}"))} for c in calls ] response = respond({"model": MODEL, "previous_response_id": response["id"], "input": outputs, "tools": tools}) for item in response["output"]: if item["type"] == "message": print("\n".join(part["text"] for part in item["content"]))
Things to know about route two:
- Strict mode keeps the limits in the descriptions. OpenAI's strict mode has no keywords for length or range, so a description says "1 to 200", and the server enforces it with a 400 that names the field.
ask_closed_questiontakes itsinputas text. Strict mode has no open-ended object. Send JSON as a string if you need structure.- The tools are safe to call in parallel, so the model may ask for several at once. Each call is metered and counted against the rate limit. For a long list of merchants, ask the model to deduplicate first, or skip the model and use the batch endpoint directly.
- Add an
Idempotency-Keyheader toPOST /v1/tools/{name}if your loop retries. - Keep the model's view small. There are ten definitions at
GET /v1/tools(the seven banking tools and three health tools). OpenAI suggests staying under about twenty functions.
Codex and the ChatGPT desktop app
OpenAI's Codex command line reads MCP servers from ~/.codex/config.toml and can send a static key from an environment variable. The ChatGPT desktop app shares that configuration. This is the one place in the ChatGPT family where a pasted key works.
[mcp_servers.understanding] url = "https://api.theunderstanding.company/mcp/chatgpt" bearer_token_env_var = "UNDERSTANDING_API_KEY"
What we tuned for this service
Route one uses /mcp/chatgpt, the address tuned for OpenAI: descriptions in the form "Use this when... Do not use for...", a one-line result led by the abstain flag with structured content that matches a declared output schema, and read-only marks on every tool. See ChatGPT.
Route two uses our standard descriptions. The definitions are generated from the same schemas the MCP server uses, so a tool is named and argued the same way over both routes.
Check that it works
Run either route with the prompt above. The model should call check_restricted_merchant with merchant set to DRAFTKINGS and mcc set to 7995. Over route two, the output you hand back looks like this:
{"abstain":false,"decision":"restricted_merchant","type":"predicate","answer":true,"probability":0.97,"reasons":["mcc_7995","rule_gambling_category","kind_sportsbook"]}Over route one, look for an mcp_list_tools item and then an mcp_call item in the response's output. For a case that abstains, ask about LUCKY COINS LLC with code 5816: abstain is true, and the model should tell you it is unsure. The numbers show the format. They are not measured results.
If it doesn't connect
- The response contains
mcp_approval_requestitems and stops.require_approvalis stillalways. Set it tonever, or answer each request with anmcp_approval_response. - OpenAI reports it cannot list the tools. Check the address, and try
authorizationin place ofheaders, or the other way round. Check that the key is valid withcurl https://api.theunderstanding.company/v1/usage -H "Authorization: Bearer $UNDERSTANDING_API_KEY". - A 401 comes back from our side. The key is wrong or revoked. OpenAI does not store it, so check that every request sends it.
- Strict mode rejects a definition. Use the
openaiformat exactly as served. Do not edit the schemas. - A call fails with
sensitive_data. The arguments held what looks like a card, Social Security or account number. Remove it and send the merchant, amount and date. See Errors. - Too many requests. The limit is 60 requests a minute per key. See Rate limits.
# OpenAI API
There are two routes. In the first, OpenAI's servers call our MCP server for you. In the second, you give the model our function definitions and your own code makes each call. Choose the second when you want to keep the key and the data in your own code.
Not open yet. Opens with early access.
## Before you start
- An OpenAI API key and a model that supports remote MCP tools (route one) or function calling (route two). Replace `YOUR_MODEL` below with yours.
- An Understanding API key from the console. You need an account first. Keep it on your server.
- No sign-in. Both routes use the key. ChatGPT itself is different; see [ChatGPT](/api/docs/integrations/chatgpt).
## Set it up
### Route one: the remote MCP tool
The curl example:
```bash
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_MODEL",
"input": "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?",
"tools": [{
"type": "mcp",
"server_label": "understanding",
"server_url": "https://api.theunderstanding.company/mcp/chatgpt",
"headers": { "Authorization": "Bearer '"$UNDERSTANDING_API_KEY"'" },
"allowed_tools": ["check_restricted_merchant", "classify_spend_type", "match_receipt", "pair_refund", "check_return_window", "route_support_message", "ask_closed_question"],
"require_approval": "never"
}]
}'
```
The Python example:
```
import os
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="YOUR_MODEL",
input="Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?",
tools=[{
"type": "mcp",
"server_label": "understanding",
"server_url": "https://api.theunderstanding.company/mcp/chatgpt",
"headers": {"Authorization": f"Bearer {os.environ['UNDERSTANDING_API_KEY']}"},
"allowed_tools": ["check_restricted_merchant", "classify_spend_type", "match_receipt", "pair_refund", "check_return_window", "route_support_message", "ask_closed_question"],
"require_approval": "never",
}],
)
print(response.output_text)
```
Four settings decide whether this works the way you expect.
- `require_approval` defaults to `always`. Every tool call then comes back as an `mcp_approval_request` that your code must answer with an `mcp_approval_response` before the model continues. The tools only read and classify, so `never` is reasonable, or `{ "never": { "tool_names": [ ... ] } }`. It is your choice.
- `allowed_tools` defaults to every tool the server lists. Name the ones you need. The list above is the seven banking question tools the ChatGPT address offers; `get_usage` is left out, and so are the three health tools, which that address does not offer.
- OpenAI documents two fields. OpenAI documents `authorization` as an OAuth access token and lists `headers` in its API reference. Try `headers` first, as above. If OpenAI refuses it, use `"authorization": "<your key>"` and drop `headers`. The server accepts a bare key as well as `Bearer <key>`.
- OpenAI does not store the credential. Send it on every request.
The server must be reachable from the internet over HTTPS, and it speaks Streamable HTTP. Nothing in the result is an instruction: it holds the answer, the probability, the reasons and the abstain flag.
### Route two: function calling
The definitions come from our server, in OpenAI's strict form (`additionalProperties` is false, every property is required, and an optional one accepts null):
```
curl "https://api.theunderstanding.company/v1/tools?format=openai"
```
The reply is `{ "format": "openai", "tools": [ ... ], "execute": { ... } }`. Pass `tools` to the Responses API. When the model returns a `function_call`, send its arguments, exactly as it produced them, to `POST https://api.theunderstanding.company/v1/tools/{name}` with your key. Nulls for optional arguments are fine. Hand the answer back as the call's output. Save this as `understanding-openai.mjs`:
The JavaScript example:
```
const API = "https://api.theunderstanding.company";
const ours = (path, init = {}) =>
fetch(API + path, {
...init,
headers: { Authorization: `Bearer ${process.env.UNDERSTANDING_API_KEY}`, "Content-Type": "application/json" },
});
const { tools } = await (await ours("/v1/tools?format=openai")).json();
async function runTool(name, args) {
const res = await ours(`/v1/tools/${name}`, { method: "POST", body: JSON.stringify(args) });
const body = await res.json();
if (!res.ok) return JSON.stringify({ error: body.error });
const { abstain, ...rest } = body; // put abstain first so the model sees it first
return JSON.stringify({ abstain, ...rest });
}
async function respond(request) {
const res = 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(request),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? `HTTP ${res.status}`);
return json;
}
const model = "YOUR_MODEL";
let response = await respond({ model, input: "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", tools });
for (let round = 0; round < 5; round++) {
const calls = response.output.filter((item) => item.type === "function_call");
if (calls.length === 0) break;
const outputs = [];
for (const call of calls) {
outputs.push({ type: "function_call_output", call_id: call.call_id, output: await runTool(call.name, JSON.parse(call.arguments || "{}")) });
}
response = await respond({ model, previous_response_id: response.id, input: outputs, tools });
}
const message = response.output.find((item) => item.type === "message");
console.log(message?.content?.map((part) => part.text).join("\n"));
```
The Python example:
```
import json
import os
import requests
API = "https://api.theunderstanding.company"
OURS = {"Authorization": f"Bearer {os.environ['UNDERSTANDING_API_KEY']}", "Content-Type": "application/json"}
THEIRS = {"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}", "Content-Type": "application/json"}
MODEL = "YOUR_MODEL"
tools = requests.get(f"{API}/v1/tools?format=openai", timeout=30).json()["tools"]
def run_tool(name, args):
res = requests.post(f"{API}/v1/tools/{name}", headers=OURS, json=args, timeout=60)
body = res.json()
if not res.ok:
return json.dumps({"error": body["error"]})
return json.dumps({"abstain": body["abstain"], **{k: v for k, v in body.items() if k != "abstain"}})
def respond(request):
res = requests.post("https://api.openai.com/v1/responses", headers=THEIRS, json=request, timeout=120)
res.raise_for_status()
return res.json()
response = respond({"model": MODEL, "input": "Is the merchant DRAFTKINGS, MCC 7995, a restricted merchant?", "tools": tools})
for _ in range(5):
calls = [item for item in response["output"] if item["type"] == "function_call"]
if not calls:
break
outputs = [
{"type": "function_call_output", "call_id": c["call_id"], "output": run_tool(c["name"], json.loads(c["arguments"] or "{}"))}
for c in calls
]
response = respond({"model": MODEL, "previous_response_id": response["id"], "input": outputs, "tools": tools})
for item in response["output"]:
if item["type"] == "message":
print("\n".join(part["text"] for part in item["content"]))
```
Things to know about route two:
- Strict mode keeps the limits in the descriptions. OpenAI's strict mode has no keywords for length or range, so a description says "1 to 200", and the server enforces it with a 400 that names the field.
- `ask_closed_question` takes its `input` as text. Strict mode has no open-ended object. Send JSON as a string if you need structure.
- The tools are safe to call in parallel, so the model may ask for several at once. Each call is metered and counted against the rate limit. For a long list of merchants, ask the model to deduplicate first, or skip the model and use [the batch endpoint](/api/docs/integrations/banking-apps#batch-send-many-at-once) directly.
- Add an `Idempotency-Key` header to `POST /v1/tools/{name}` if your loop retries.
- Keep the model's view small. There are ten definitions at `GET /v1/tools` (the seven banking tools and three health tools). OpenAI suggests staying under about twenty functions.
### Codex and the ChatGPT desktop app
OpenAI's Codex command line reads MCP servers from `~/.codex/config.toml` and can send a static key from an environment variable. The ChatGPT desktop app shares that configuration. This is the one place in the ChatGPT family where a pasted key works.
```
[mcp_servers.understanding]
url = "https://api.theunderstanding.company/mcp/chatgpt"
bearer_token_env_var = "UNDERSTANDING_API_KEY"
```
## What we tuned for this service
Route one uses `/mcp/chatgpt`, the address tuned for OpenAI: descriptions in the form "Use this when... Do not use for...", a one-line result led by the abstain flag with structured content that matches a declared output schema, and read-only marks on every tool. See [ChatGPT](/api/docs/integrations/chatgpt).
Route two uses our standard descriptions. The definitions are generated from the same schemas the MCP server uses, so a tool is named and argued the same way over both routes.
## Check that it works
Run either route with the prompt above. The model should call `check_restricted_merchant` with `merchant` set to `DRAFTKINGS` and `mcc` set to `7995`. Over route two, the output you hand back looks like this:
```
{"abstain":false,"decision":"restricted_merchant","type":"predicate","answer":true,"probability":0.97,"reasons":["mcc_7995","rule_gambling_category","kind_sportsbook"]}
```
Over route one, look for an `mcp_list_tools` item and then an `mcp_call` item in the response's `output`. For a case that abstains, ask about LUCKY COINS LLC with code 5816: `abstain` is `true`, and the model should tell you it is unsure. The numbers show the format. They are not measured results.
## If it doesn't connect
- The response contains `mcp_approval_request` items and stops. `require_approval` is still `always`. Set it to `never`, or answer each request with an `mcp_approval_response`.
- OpenAI reports it cannot list the tools. Check the address, and try `authorization` in place of `headers`, or the other way round. Check that the key is valid with `curl https://api.theunderstanding.company/v1/usage -H "Authorization: Bearer $UNDERSTANDING_API_KEY"`.
- A 401 comes back from our side. The key is wrong or revoked. OpenAI does not store it, so check that every request sends it.
- Strict mode rejects a definition. Use the `openai` format exactly as served. Do not edit the schemas.
- A call fails with `sensitive_data`. The arguments held what looks like a card, Social Security or account number. Remove it and send the merchant, amount and date. See [Errors](/api/docs/errors).
- Too many requests. The limit is 60 requests a minute per key. See [Rate limits](/api/docs/limits#rate-limits).
Source: /api/docs/integrations/openai-api