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_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.

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

    Bash
    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_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 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.

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

    JSON
    {"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.
    • Too many requests. The limit is 60 requests a minute per key. See Rate limits.