Building a Postman Collection for OpenAI-Compatible APIs
Guides

Building a Postman Collection for OpenAI-Compatible APIs

A reusable Postman or Insomnia collection for any OpenAI-compatible endpoint — environments, streaming, tool calls, saved examples, and sharing it without leaking keys.

Everyone building against a model API ends up with a scratch request somewhere — a curl line in shell history, a file called test.json, a tab in an HTTP client that nobody else can reproduce. A collection is the version of that which survives contact with a second person and a second provider.

The value is not the requests themselves; those are four lines of JSON. It is having one place where the base URL and the key are variables rather than literals, where a working tool-call request already exists when you need to debug one at 5pm, and where the response your provider actually returned last month is saved next to the request that produced it.

Environments are the whole design

Put nothing provider-specific in a request. The base URL, the API key and the model identifier are environment variables, referenced as {{baseUrl}}, {{apiKey}} and {{model}}. Everything else in the collection is then provider-agnostic, and switching providers is selecting a different environment from a dropdown.

That is not a tidiness argument. The reason OpenAI compatibility is worth anything is that the same client works against several endpoints, and a collection that hardcodes one host cannot demonstrate that. Create at least two environments on day one, even if the second is a local model server, because a collection with one environment always turns out to have a literal buried in it somewhere. The scope of what actually ports between providers is in the compatibility explainer.

Set the base URL to the root of the compatible surface, ending at the version segment, and put the operation path in each request. So the variable holds https://api.example.com/v1 and the request URL is {{baseUrl}}/chat/completions. Baking the operation into the variable is how people end up with duplicated path segments and a 404 they diagnose as an authentication failure for an hour.

Put the authorisation header on the collection rather than on each request. Both Postman and Insomnia support collection-level or folder-level auth that every child request inherits, which means adding a new request is one step and rotating a key is one edit.

The chat completions request

Start with the smallest thing that proves the connection works. A single user message, a low max_tokens, no streaming.

POST {{baseUrl}}/chat/completions
Authorization: Bearer {{apiKey}}
Content-Type: application/json

{
  "model": "{{model}}",
  "max_tokens": 64,
  "messages": [
    {"role": "user", "content": "Reply with the single word: ok"}
  ]
}

Add a second request for the model list route, usually {{baseUrl}}/models. It is the fastest way to find out what identifiers a provider actually publishes, and it is the answer to most of the errors that look like a broken model name. Keep it first in the collection so it is the first thing a new person runs.

From there, add variants rather than editing the original: one with a system prompt, one with a JSON response format, one with a deliberately oversized max_tokens so you can see how truncation reports itself. Each of these is a question you will eventually need answered about a new provider, and having the request ready turns a twenty-minute investigation into one click. Structured outputs and JSON mode covers what to expect from the response-format variants, which vary more between providers than the basic chat shape does.

Streaming, and where clients disappoint you

A streaming request is the same body with "stream": true. What differs is what the client does with the reply, and this is where HTTP clients built for REST show their age.

Recent Postman versions detect text/event-stream and render events as they arrive; Insomnia has an equivalent event stream view. Older builds buffer the whole response and show it at the end, which looks identical to a provider that does not stream at all. If you are testing whether streaming works end to end, verify your client can display it before concluding the endpoint is broken — and be aware that a corporate proxy in the path can also collapse a stream into one delivery.

Keep the streaming request in the collection anyway, even if the rendering is poor, because it is the fastest way to confirm the wire format. Each event is a line beginning with data: carrying a JSON chunk, and the stream ends with a sentinel line rather than valid JSON — the details are in the server-sent events walkthrough. Seeing the raw frames once makes every downstream client bug easier to read.

Tool calls need two requests, not one

The tool-calling request is the one most worth having saved, because it is fiddly to write from memory and it is what breaks when you switch providers. It also needs a second request to be a complete test, which is the part people leave out.

The first request sends a tools array of function definitions with JSON Schema parameters and asks something that should trigger one. The response comes back with a tool_calls array on the assistant message and a finish reason indicating a tool call rather than a completed answer.

{
  "model": "{{model}}",
  "messages": [{"role": "user", "content": "What is the weather in Oslo?"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Current weather for a city",
      "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"]
      }
    }
  }]
}

The second request is the continuation: the original user message, the assistant message containing the tool call exactly as returned, and a tool role message carrying a fabricated result keyed to the same call id. If that round trip works, tool calling works. If only the first half works, you have a model that emits calls and a provider that rejects the result format, which is a much more specific bug than it appears. The tool calling explainer covers the loop, and writing schemas for tool calls covers the parameter block that most often causes silent non-invocation.

Save example responses

Both clients let you attach a saved response to a request. Use it. A stored example is documentation that cannot drift from the thing it documents, and it answers questions that are otherwise archaeology: what did the usage object look like on this provider, does it return a system fingerprint, how are refusals shaped, what exactly comes back on a rate limit.

Save the error cases especially. An authentication failure, a bad model name and a 429 from each provider you use, captured once, will save you more time than the successful examples — the messages differ enough between implementations that recognising them by shape is genuinely useful. The error code reference is the general map; your saved examples are the specific one for your providers.

Redact before saving. Response bodies rarely contain credentials, but request examples do if you have been careless, and an example is stored in the collection file that you are about to export.

Sharing it without shipping your key

The rule is simple: the collection is committed, the environment is not. Collections contain variable references; environments contain values. Export the collection into the repository, export a template environment with empty or placeholder values, and add the real environment file to .gitignore.

Both tools have a notion of an initial value versus a current value, and this is the trap. The initial value is what gets exported and synced; the current value stays local. Put the key in the current value only and leave the initial value blank, or you will sync your credential to a workspace and, eventually, to a repository. Postman also offers a secret variable type that is not exported — prefer it where available.

If your client syncs to a cloud workspace by default, decide deliberately whether that is acceptable for your keys rather than discovering it later. A workspace shared with a team is a workspace where every member can read every current value they have access to.

One provider-level mitigation helps regardless: use a separate key for exploratory work, so a leak from a collection is one revocation rather than an incident. If you are already running one credential across several tools — as many people sensibly do — make the collection the exception rather than the tenth consumer.

Scripts, and knowing when to stop

A small amount of scripting earns its place. A test script that asserts a 200 and a non-empty choices array turns the collection into something you can run as a smoke check against a new provider. A script that captures the tool call id from one response into a variable makes the two-request tool test a two-click operation instead of a copy-paste.

pm.test("has content", function () {
  const j = pm.response.json();
  pm.expect(j.choices[0].message).to.be.an("object");
  pm.environment.set("lastId", j.id);
});

Beyond that, stop. A collection that has grown a pre-request script computing signatures and a chain of dependent requests is a program, and it should be a program in a language with tests and a debugger. The collection is for exploring and reproducing, not for orchestrating. When you find yourself debugging the collection rather than the API, move the logic out — a curl script or a short SDK program is easier to reason about and easier to paste into a bug report.

The takeaway

Three environment variables and collection-level auth. A models request first, then chat, then streaming, then the two-request tool-call pair. Save successful and error responses as examples. Commit the collection, gitignore the environment, keep the key in the current value only, and use a throwaway key for exploration. When the collection starts needing its own debugging, that logic belongs in code instead.

Common questions

How do I share a collection without sharing my API key?

Commit the collection and never the environment. Keep the key in the current value rather than the initial value, because only the initial value is exported and synced. Export a placeholder environment as a template and gitignore the real one.

Streaming shows nothing until the response finishes. Is the endpoint broken?

Probably not. Older HTTP client builds buffer text/event-stream and render it only at the end, and a proxy in the path can do the same. Confirm your client version supports incremental event display before concluding the provider does not stream.

What is the minimum set of requests worth saving?

The model list route, a small chat completion, a streaming variant, and a tool-call pair — the call request plus the continuation that sends the tool result back. Add saved examples of an auth failure and a rate limit from each provider you use.

Similar articles

Aider Setup Guide: Any OpenAI-Compatible Endpoint
Guides
Guides·8 min read

Aider Setup Guide: Any OpenAI-Compatible Endpoint

Configure Aider against a custom base URL — the openai/ prefix, .aider.conf.yml, model metadata for unknown models, and picking the right edit format.

Read
Cline Setup Guide: Pointing It at a Custom Endpoint
Guides
Guides·8 min read

Cline Setup Guide: Pointing It at a Custom Endpoint

Configure Cline against any OpenAI-compatible base URL — the provider fields, the model configuration block that people skip, and separate Plan and Act models.

Read
Continue.dev Setup: config.yaml for a Custom Endpoint
Guides
Guides·8 min read

Continue.dev Setup: config.yaml for a Custom Endpoint

Point Continue at any OpenAI-compatible base URL using config.yaml — model roles, apiBase, request options, and why the autocomplete slot needs its own model.

Read