Integration examples
Call the gateway from cURL, Node.js, Python and React — a working SSE client, a standalone detection call, and a server-side conversation.
- Version: 0.4
- Role: admin_user, normal_user
- Type: task
Build a working client against the gateway: authenticate, discover which models you can call, read the event stream correctly, scan text without a model, and hold a conversation on the server. Every snippet on this page runs against the real routes.
What do I need?
- Licence
- Any
- Role
admin_usernormal_user
Prerequisites
- The gateway is reachable and answers GET /health with status ok.
- First-boot setup is complete, and the installed licence is valid — otherwise every route below returns 423 or 402.
- A provider credential exists for your tenant or its tenant group, so at least one model is callable.
- You hold a gateway API key (Authorization: Bearer cid_key_…) for chat, models and detection, or local sign-in credentials for a user JWT if you intend to use sessions.
<gateway-host> below is the address the gateway listens on — port 3000 by default, or whatever
the reverse proxy in front of it publishes. Route prefixes are not uniform: chat is under /chat,
sessions under /sessions, models under /models, and detection is the only surface under
/api/v1.
Warning
POST /chat/completions and POST /sessions/:id/messages always answer with
text/event-stream. The gateway buffers the model's answer, filters it, and emits the whole
reply as a single filtered-response event — there are no token deltas and no choices[] body.
Calling response.json() on either route fails.
Confirm which models your credential can reach
GET /models accepts either a JWT or a cid_key_ bearer, and returns only the models whose
provider has an active credential for your tenant.
The response is an array of model objects. Each carries the model_name you pass as model
and the provider that offers it:
An empty array means no provider credential resolves for your tenant, and every chat call will fail until one does.
Send a chat request from the command line
Use -N so cURL does not buffer the stream.
The terminal pauses while the model generates, then prints the whole answer in one frame, then the terminator:
Send provider whenever the model name is ambiguous. gpt-4o, gpt-4o-mini and gpt-4-turbo
are registered by both openai and azure_openai; Azure's 3.5 model is gpt-35-turbo,
without the dot.
Read the stream from your own code
Buffer the decoded bytes and split on the blank line between frames — one read can deliver several frames or half of one.
Both clients return the complete answer, or raise on a policy rejection.
Handle the events that are not content
Between the request and the answer the gateway may emit control events. Ignore the ones you do
not use, but do not let an unknown type fall through into your content handler.
| Event | When it appears | What to do |
|---|---|---|
{"error":…,"type":"content_rejected"} | The prompt violated policy. Terminal, no provider call, no tokens spent | Show error to the user; do not retry the same text |
{"type":"user_locked",…} | The user is locked by an LLM review. Terminal | Stop, and surface the review_id |
{"type":"security_warning",…} | A review flagged the user. Not terminal | Log it and continue |
{"type":"documents_processed",…} | You sent documents | Optionally report the summaries |
{"type":"model_routed",…} / {"type":"routing_evaluated",…} | Model routing evaluated the request | Telemetry only |
{"type":"token_optimized",…} | Prompt compression ran | Telemetry only |
{"type":"token_usage",…} | Always, when the provider reported usage | Record the prompt and completion split |
{"type":"output_content_rejected",…} | The answer was blocked. Terminal | Show reason; the answer is discarded |
{"id":"filtered-response",…} | The answer | Render content |
{"type":"output_content_warning",…} | The answer was flagged but delivered | Show a warning next to the answer |
Note
user_message_processed — the event that reports how much of your prompt was masked — is
emitted internally on /chat/completions and never reaches the client. It does reach the
client on POST /sessions/:id/messages.
Scan text without calling a model
POST /api/v1/guardrails/detect runs the same detection pipeline with no provider involved. It
returns 200 and states the verdict in the body, so a block is not an HTTP error.
action is one of reject, mask, flag or allow. text must be 1–50,000 characters.
detectedEntities[].value is a placeholder or [REDACTED]: the endpoint never echoes the raw
PII it found, and normalization.originalText is deliberately blank.
Hold a conversation on the server with a session
Sessions keep the history server-side, so you send one message per turn. They are guarded by
JwtAuthGuard alone — a cid_key_ bearer is rejected, so sign in first.
The message call streams the same way chat does, with one difference worth coding for: a
session rejects a prompt with {"type":"content_rejected","reason":…} and no error
field, so a handler written for /chat/completions misses it.
Proxy the stream to a browser
Never put a gateway credential in front-end code. Terminate it in your own route and forward the frames unchanged, so the browser reads the same contract.
The assistant message appears in one step. A typewriter effect is not possible through the gateway, because output filtering needs the complete answer before it can release any of it.
Verify
curl -s https://<gateway-host>/healthreturns{"status":"ok",…}.curl -s https://<gateway-host>/models -H "Authorization: Bearer $CID222_API_KEY"returns a non-empty array, and themodel_nameyou intend to call is in it.- Your chat client returns non-empty text, and the raw stream contains exactly one frame whose
idisfiltered-response, followed bydata: [DONE]. detect("Email me at john@example.com")returnsactionofmaskand amaskedTextin which the address is replaced by a placeholder.- Send a prompt that your filter set rejects. The stream terminates with a
content_rejectedevent and nofiltered-response, and the attempt appears in the dashboard under All Detections with the actionrejected.
If it fails
- Every route answers
423withcode: "SETUP_REQUIRED"→ first-boot setup is unfinished; finish the wizard. - Every route answers
402withcode: "LICENSE_EXPIRED"→ 402 after setup GET /modelsreturns[]→ no provider credential resolves for your tenant. Add one under Credentials, or add the tenant to a group that has one.401on a route that worked yesterday → the JWT expired, or the API key was regenerated.401on/sessionswith a key that works on/chat/completions→ sessions are JWT-only.- The client hangs and then times out → it is waiting for token deltas that never come. Handle the
single
filtered-responseevent instead. See Error handling.
Next steps
- Error handling — the status codes, the three error body shapes, and which failures are worth retrying.
- Best practices — credential handling, connection reuse, and choosing between sessions and the stateless endpoint.
- Chat completions — the full request DTO and the complete event list.
- Content detection — every field of the detection request and response.
Last updated on