One endpoint for language-model completions, with per-connection credentials, quotas and logging. Inference runs on our own hardware; nothing you send is passed to a third-party model provider.
You are issued a client secret for each connection. It looks like brg_… and is shown to your administrator once, at issuance — we store only a hash of it and cannot show it again. Keep it as you would a database password.
You do not send the client secret on API calls. You exchange it for a short-lived access token, and send the token:
curl -s https://bridgeapi.mynexuss.com/v1/token \
-H 'content-type: application/json' \
-d '{"client_secret":"brg_YOUR_SECRET"}'
{"access_token":"eyJ…","token_type":"Bearer","expires_in":900}Present it on every request as Authorization: Bearer <access_token>, and exchange again when it expires. Tokens are signed with ES256; the public keys are at /.well-known/jwks.json if you want to verify one yourself.
Everything the API does is POST https://bridgeapi.mynexuss.com/v1/chat/completions. It is wire-compatible with the OpenAI chat-completions shape, so most existing client libraries work by changing the base URL and the key.
curl -s https://bridgeapi.mynexuss.com/v1/chat/completions \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"model": "Qwen3.8-27B-FP8",
"messages": [
{"role":"system","content":"You answer concisely."},
{"role":"user","content":"Summarise the attached contract."}
],
"max_tokens": 800,
"temperature": 0
}'| Field | What it does |
|---|---|
model | Required. Must be a model your connection is permitted to call — GET /v1/models lists them. |
messages | Required. The whole conversation, oldest first. The API is stateless: we do not remember previous calls, so send the history you want considered. Roles: system, developer (treated exactly as system, for SDKs that send it), user, assistant and tool (see Tool calling). |
max_tokens | Ceiling on the reply. Optional: by default a connection has no per-request output cap, so a reply is bounded only by the model's context window minus your prompt — send max_tokens when you want less than that. If your administrator has set a cap on your connection, a larger max_tokens is clamped to it. |
temperature | 0 to 2. Use 0 for deterministic output; only 0 is eligible for caching. |
top_p | Nucleus sampling, above 0 and at most 1. |
stop | Up to four stop sequences. |
stream | true for server-sent events. Deltas arrive as data: lines; the last carries usage, then data: [DONE]. |
tools | Functions the model may ask you to run — the OpenAI shape, see Tool calling. |
tool_choice | "none", "auto" (the default when tools are sent), "required", or {"type":"function","function":{"name":"…"}} to force one. |
parallel_tool_calls | false to have the model make at most one call per turn. |
cache | false to bypass the response cache for this call. There is no opt-in — caching is enabled per connection by your administrator. |
One field is refused with an error rather than quietly ignored, because an SDK that sends it and is ignored produces wrong output in production:
response_format — structured output is not implemented yet. Accepting it and returning unconstrained prose would be worse than refusing it. Refused with 400 unsupported_response_format.You can give the model functions it may ask you to run. We run nothing: the
model decides it wants a function called and tells you which and with what arguments; you
run it on your side and send the result back as a tool message. This is the
OpenAI function-calling shape, so an SDK that already does tool calling works unchanged.
1. Send the tools with the request.
{
"model": "Qwen3.8-27B-FP8",
"messages": [{"role":"user","content":"What's the weather in Leeds?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}
2. The model asks you to run one. finish_reason is
tool_calls, content is null, and each call carries an
id you will need in a moment. arguments is a JSON string, as in the
OpenAI shape.
{"id":"resp_7cQ…","object":"chat.completion",
"choices":[{"index":0,"finish_reason":"tool_calls","message":{
"role":"assistant","content":null,
"tool_calls":[{"id":"call_…","type":"function",
"function":{"name":"get_weather","arguments":"{\"city\":\"Leeds\"}"}}]}}],
"usage":{…}}
3. Run it, and send the result back as a tool message whose
tool_call_id is that id. Using previous_response_id means
you send only the new message — the assistant's call is already on the thread:
{
"model": "Qwen3.8-27B-FP8",
"previous_response_id": "resp_7cQ…",
"messages": [{"role":"tool","tool_call_id":"call_…","content":"{\"temp_c\":14,\"sky\":\"overcast\"}"}],
"tools": [ …the same tools… ]
}
Without previous_response_id, send the whole exchange yourself: the user message,
then the assistant message with its tool_calls exactly as you received it, then
the tool message. Either way the answer comes back as ordinary text with
finish_reason: "stop".
Streaming works the same way: each call arrives as one
delta.tool_calls[] chunk carrying its index, id, function
name and complete arguments, and the final chunk carries
finish_reason: "tool_calls" and usage.
^[a-zA-Z0-9_-]{1,64}$ and are unique within a request; parameters, if given, is a JSON Schema object.tool_calls on one assistant message when replaying history.400 invalid_request and a message naming the field.Your request log shows how many calls the model made on a turn — a count. The function names and the arguments are content, and like every other piece of content they are not written down.
Small attachments can ride on the request itself as content parts:
{"role":"user","content":[
{"type":"text","text":"Summarise this."},
{"type":"input_file","filename":"contract.pdf","file_data":"<base64>"}
]}
Past your connection's inline limit — by count or by total size — the request is
refused with 413 inline_file_limit_exceeded. That is
deliberate: dropping the fourth attachment silently would give you an answer that looks
complete and is not. Upload the files first, then reference them.
curl -s https://bridgeapi.mynexuss.com/v1/files \
-H "authorization: Bearer $TOKEN" \
-F 'file=@contract.pdf'
{"id":"file_9tK...","object":"file","filename":"contract.pdf","bytes":184320,"expires_at":1760000000,"scanned":true}
{"role":"user","content":[
{"type":"text","text":"Summarise this."},
{"type":"input_file","file_id":"file_9tK..."}
]}
Every upload is virus-scanned before it is stored. Text-bearing formats (PDF, Word,
PowerPoint, Excel, CSV, RTF, ODT, HTML, plain text and Markdown) have their text
extracted and given to the model; images are sent as pictures.
File contents are never returned — GET /v1/files/{id} gives
you the details and nothing else. This is not a storage service.
The same rule as a conversation: one hour from the last time you used it, and never more than 48 hours in total. Referencing a file in a prompt counts as using it, so a document you keep coming back to during a long piece of work stays available the whole time.
A file being used by a live conversation is kept until that conversation finishes, even if the file's own hour has run out. That is what stops a document disappearing from under a discussion you are still having.
curl -s -X DELETE https://bridgeapi.mynexuss.com/v1/files/file_9tK... \
-H "authorization: Bearer $TOKEN"
You will get one of two answers, and the difference matters if you are acting on someone's request to erase their data:
{"id":"file_9tK...","deleted":true,"status":"deleted",
"detail":"The file's content has been deleted."}
The content is gone.
{"id":"file_9tK...","deleted":false,"status":"pending","removed_at":1760003600,
"detail":"The file is still being used by a conversation, ..."}
The file was still in use by a live conversation, so the content has not been
deleted yet. It can no longer be referenced by new requests, and it will be
removed when that conversation stops being active — removed_at tells you
when that is expected. We will not tell you something is deleted while it still
exists, so check deleted rather than assuming a 200 means gone. If
you have a webhook set up, we will tell you when it finally goes.
Every answer comes back with an id. Send it as
previous_response_id on your next request and the model picks up where you
left off — it has the earlier questions and its own earlier answers in front of it.
That is what makes a series of refinements a conversation rather than a set of
unrelated calls.
{"id":"resp_7cQ…","object":"chat.completion","choices":[…]}
{
"model": "Qwen3.8-27B-FP8",
"previous_response_id": "resp_7cQ…",
"messages": [{"role":"user","content":"Shorter, and drop the second clause."}]
}
You send only the NEW message. Do not resend the history — we already have it, and resending it would be charged to you twice.
One hour from the last time you used it, not from when it was created. So a back-and-forth you are actively working on stays alive as long as you keep going, and one you walk away from is deleted an hour later. There is also an outer limit — a thread is removed after 48 hours whatever happens.
When a thread has expired, or if you send an id we do not recognise, the
request still succeeds — it is simply answered as a fresh conversation. Check
the continues field on the response if you need to know which happened: it
carries the id you supplied when we picked the thread up, and is absent when we did
not. That way a timed-out thread degrades into a slightly worse answer rather than an
error in the middle of someone's workflow.
While a thread is live we hold the turns in it — your questions and our answers —
because that is what the next turn is built from. That is a separate thing from the
logging described below, and it is bounded: the thread is deleted when it expires, and
deleted immediately if your connection is cut off. Send "store": false on
a request to keep that particular exchange out of it entirely; you then get no id back
and cannot follow up on it.
If your administrator has turned follow-ups off for your connection, nothing is held between requests at all and every question is answered on its own.
| Limit | What happens |
|---|---|
| Daily token quota | 429 quota_exhausted with Retry-After. Resets at 00:00 UTC. It is a hard stop, not a slowdown. |
| Requests per minute | 429 rate_limited with Retry-After. |
| Prepaid token balance | 402 balance_exhausted. Your company's token balance has run out. It does not reset by itself: a bridgeAdmin adds tokens on the Tokens page of the Bridge module site (https://bridge.mynexuss.com), and the balance, usage and this connection's logs are all visible there. |
| Output tokens per request | None by default — a reply is bounded only by the model's context window minus your prompt (400 context_window_exceeded when the two cannot fit). If your administrator has set a per-request cap on your connection, your max_tokens is clamped to it. |
| Request timeout | 504 timeout. Set per connection by your administrator. |
| Address restriction | If your connection is restricted to particular addresses, a request from anywhere else is refused. Ask your administrator which addresses are registered. |
Every response carries an x-request-id. Quote it when asking about a specific call — it is how we find the record without you having to tell us what you sent.
{"error":{"message":"…","type":"invalid_request_error","code":"model_not_allowed","request_id":"…"}}
Branch on code, which is stable; message is for a human. The codes are: unauthenticated, connection_not_usable, source_address_not_allowed, operation_not_allowed, model_not_allowed, quota_exhausted, rate_limited, balance_exhausted, invalid_request, inline_file_limit_exceeded, file_not_found, file_refused, unsupported_response_format, context_window_exceeded, upstream_unavailable, timeout, internal_error.
Your administrator can give us an address to notify when something changes that your
software would otherwise have to guess at. We send a POST with a small JSON
body — never any of your content — for these:
| Event | Means |
|---|---|
conversation.closed | A conversation has finished. Its reference can no longer be followed up. |
file.deleted | A file's content has been removed — it expired, or the connection was closed. |
file.deletion_completed | A file you asked us to delete, which was still in use at the time, has now been removed. |
quota.exhausted | Your allowance for the day has run out. Sent once a day, not once per refused request. |
balance.low | Your company's prepaid token balance has fallen to its low-water mark. Sent once, until the next top-up. |
balance.exhausted | Your company's prepaid token balance has run out and requests are being refused with 402. Sent once, until the next top-up. |
connection.revoked | Your access has been withdrawn. This is the last thing you will hear from us. |
POST /your-endpoint
Bridge-Signature: t=1789240000,v1=4a7f...
content-type: application/json
{"id":"0193...","type":"conversation.closed","created_at":1789240000,
"connection_id":"22ff...","subject":"resp_7cQ...","reason":"expired"}
Every delivery is signed with a key your administrator gives you. Take the
t= value and the exact body you received, join them with a full stop, and
compute an HMAC-SHA256 using your key. It should equal the v1= value:
signed = t + "." + raw_body
expected = hex( hmac_sha256(your_signing_key, signed) )
Compare the two in constant time. Reject anything where t is more
than a few minutes old — the timestamp is part of what is signed precisely so
that an old delivery cannot be replayed at you later.
We retry six times with increasing gaps — roughly 30 seconds, then 1, 2, 4 and 8 minutes — and then stop and record the failure for your administrator to see. Nothing is queued indefinitely. Deliveries are sent in the background, so a slow or unavailable endpoint of yours never slows down your API requests.
We only send to https addresses on the public internet, and we do not follow redirects. If your address stops resolving publicly we stop delivering to it.
This is worth reading rather than assuming, because it is not what most APIs do.
We record that a request happened, how many input tokens it used, how many output tokens it produced, and how long it took — along with which connection made it, which model, and the outcome.
We do not store your prompts or the model's replies. Not truncated, not redacted, not for a short period: they are not written down. Upstream error messages are reduced to one of our own codes before anything is recorded, because a model's error can quote your prompt back.
There is one exception and it is deliberate. If you ask us to investigate a specific problem, an administrator can switch content capture on for your connection for a bounded period, with a stated reason and an audit record. It is off by default, it expires by itself, and it is visible to your administrator the whole time it is on.
The follow-up threads described above are the other place your words are held, and they are held for a different reason and for a bounded time — an hour from last use, with a 48-hour outer limit, deleted on expiry and deleted immediately if your connection is cut off. A thread can only ever be continued by the connection it was issued to.