Controller API¶
The Controller API is the main integration point for LLM applications. It accepts message conversations and returns analysis results from the configured modules.
A live OpenAPI reference is available at api.link2.ai/docs.
Integration modes¶
As a separate service¶
Your application calls the LLM as usual and sends a copy of the messages to the LINK2AI.Trust Controller API in a separate request. Results are available synchronously or stored for later review. See the Analysis API.
As an OpenAI proxy¶
LINK2AI.Trust acts as a drop-in replacement for the OpenAI base URL. It forwards requests to OpenAI, analyzes both the input and output, and returns the LLM response alongside analysis results. Modules configured in blocking mode can reject requests before they ever reach the LLM. See the OpenAI Proxy API.
Authentication¶
Every request to the Controller API must include a valid API key. Keys are created in the Portal under Settings → API keys and are scoped to a project.
Two authentication methods are supported:
Bearer token (standard — used with the Analysis API):
Authorization: Bearer l2-<key_id>-<secret>
Custom header (used alongside OpenAI's Authorization header in proxy mode):
LINK2AI-API-KEY: l2-<key_id>-<secret>
Analysis API¶
Use the Analysis API when your application calls the LLM directly and you want to send the messages to LINK2AI.Trust separately.
POST /v1/analyzeInput¶
Analyzes the user-facing side of a conversation. Runs input-focused modules (malicious_intent, off_topic, guardrails, harmful_content, pii, secrets, usage).
Call this before sending the request to the LLM.
Headers
| Header | Value |
|---|---|
Authorization |
Bearer <api_key> |
Content-Type |
application/json |
Request body
{
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "How do I reset my password?" }
],
"model": "gpt-4o-mini",
"configuration": { ... },
"cache": {
"no_cache": false,
"no_store": false
}
}
| Field | Type | Required | Description |
|---|---|---|---|
messages |
array | Yes | Full conversation in OpenAI message format (system, user, assistant, tool roles). |
model |
string | Yes | The model identifier used for the LLM call (e.g. gpt-4o-mini). |
configuration |
object | No | Per-module configuration that replaces the project's saved configuration for this call; only the modules it lists run. See Overriding configuration per request. If omitted, the project's saved configuration is used. |
cache.no_cache |
bool | No | Skip reading from the analysis cache. Default: false. |
cache.no_store |
bool | No | Skip writing results to the analysis cache. Default: false. |
Response body
{
"results": {
"malicious_intent": {
"duration_ms": 45.2,
"status": "success",
"secure": true,
"result": {
"message_labels": [
{ "label": "SAFE", "score": 0.998 }
]
},
"cache_hit": null,
"error_message": null
},
"usage": {
"duration_ms": 1.1,
"status": "success",
"secure": true,
"result": {
"input_tokens": 24,
"output_tokens": null,
"messages": [ ... ],
"model": "gpt-4o-mini"
},
"cache_hit": null,
"error_message": null
}
},
"module_errors": false,
"secure": true
}
| Field | Type | Description |
|---|---|---|
results |
object | Map of module ID → ModuleResult. Only modules in Included in Response or Blocking mode appear here. Modules in Monitoring only mode run in the background and are never in this map. |
module_errors |
bool | true if any module returned a failure status. |
secure |
bool | false if any security module flagged the interaction. |
ModuleResult fields
| Field | Type | Description |
|---|---|---|
status |
"success" | "failure" |
Whether the module ran without error. |
duration_ms |
float | null | Module execution time in milliseconds. |
secure |
bool | null | false if this module flagged the interaction. Always true for non-security modules (e.g. usage). null if the module could not run. |
result |
object | null | Module-specific result payload. See Modules. null on failure. |
cache_hit |
object | null | Present if the result was served from cache. Contains event_id (string) and similarity (float | null). |
error_message |
string | null | Error details when status is "failure". |
units |
int | null | Number of priced operations the module performed, e.g. one per guardrail. Determines the credits charged. |
Status codes
| Code | Meaning |
|---|---|
200 |
Analysis completed. |
400 |
No modules are configured for this project. |
401 |
Missing or invalid API key. |
402 |
The organization has no credits left. The request is not analyzed. See Running out of credits. |
POST /v1/analyzeOutput¶
Analyzes the LLM response side of a conversation. Runs output-focused modules (adherence, guardrails, harmful_content, pii, secrets, usage).
Call this after receiving the LLM response. Include the assistant message in the messages array.
Request and response structure are identical to /v1/analyzeInput.
Python example¶
import os
import httpx
API_KEY = os.environ["LINK2AI_API_KEY"]
BASE_URL = "https://api.link2.ai"
headers = {"Authorization": f"Bearer {API_KEY}"}
messages = [
{"role": "system", "content": "You are a helpful assistant for a car rental company."},
{"role": "user", "content": "What cars do you have available?"},
]
# Analyze input before calling the LLM
input_analysis = httpx.post(
f"{BASE_URL}/v1/analyzeInput",
json={"messages": messages, "model": "gpt-4o-mini"},
headers=headers,
).json()
if not input_analysis["secure"]:
raise ValueError("Input blocked by security modules")
# ... call the LLM, get assistant_response ...
# Analyze output after receiving the LLM response
messages_with_response = messages + [{"role": "assistant", "content": assistant_response}]
output_analysis = httpx.post(
f"{BASE_URL}/v1/analyzeOutput",
json={"messages": messages_with_response, "model": "gpt-4o-mini"},
headers=headers,
).json()
OpenAI Proxy API¶
The proxy API is a drop-in replacement for OpenAI's API. It forwards your request to OpenAI, analyzes both the input and output, and returns the normal OpenAI response with input_analysis and output_analysis fields appended.
Modules configured in blocking mode will reject the request before it reaches OpenAI and return HTTP 422.
POST /openai/chat/completions¶
Proxies to the OpenAI Chat Completions API.
Headers
| Header | Value |
|---|---|
Authorization |
Bearer <openai_api_key> |
LINK2AI-API-KEY |
<link2ai_api_key> |
Content-Type |
application/json |
The request body is passed through to OpenAI unchanged. The response is the standard OpenAI ChatCompletion object with two additional fields:
{
"id": "chatcmpl-...",
"object": "chat.completion",
"choices": [ ... ],
"usage": { ... },
"input_analysis": {
"results": { ... },
"module_errors": false,
"secure": true
},
"output_analysis": {
"results": { ... },
"module_errors": false,
"secure": true
}
}
Streaming
Streaming requests ("stream": true) are supported. When streaming, only input_analysis is included (attached to the first SSE chunk). Output analysis is not available in streaming mode.
Status codes
| Code | Meaning |
|---|---|
200 |
LLM call succeeded and analysis is included. |
422 |
A blocking module flagged the input. The response body is the AnalyzeResponse for the blocked analysis. No LLM response is returned. |
401 |
Missing or invalid LINK2AI API key. |
402 |
The organization has no credits left. The request is not forwarded. See Running out of credits. |
POST /openai/responses¶
Proxies to the OpenAI Responses API. Same authentication, extension fields, and blocking behavior as /openai/chat/completions.
Python example (OpenAI SDK)¶
The OpenAI Python SDK works without any changes — just point base_url at the Controller and add the LINK2AI-API-KEY header.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.link2.ai/openai",
api_key=os.environ["OPENAI_API_KEY"],
default_headers={"LINK2AI-API-KEY": os.environ["LINK2AI_API_KEY"]},
)
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "Always speak like a pirate."},
{"role": "user", "content": "Hello!"},
],
model="gpt-4o-mini",
)
print(response.choices[0].message.content)
# Access analysis results
input_results = response.input_analysis["results"]
output_results = response.output_analysis["results"]
# Check if a guardrail was violated
if not response.output_analysis["secure"]:
print("Output blocked by a security module")
Running out of credits¶
When the organization has used up its credits, the analysis and proxy endpoints answer 402 Payment Required instead of analyzing the request partially:
{
"detail": {
"message": "included usage is exhausted",
"reason": "included usage is exhausted",
"code": "usage_exhausted"
}
}
code |
Meaning |
|---|---|
usage_exhausted |
The credits of the current period and any extra credits are used up. |
no_usage |
The organization's plan includes no credits. |
Requests are admitted again as soon as credits are available, for example in the next period or after an organization owner changes the plan.
Service endpoints¶
GET /status¶
Returns the health and availability of all configured modules. Useful for monitoring and debugging.
{
"modules": {
"malicious_intent": {
"module_id": "malicious_intent",
"available": true,
"response_time": 38.5
},
"adherence": {
"module_id": "adherence",
"available": false,
"response_time": null
}
},
"time": "2026-05-04T12:00:00Z"
}
response_time is the measured latency in milliseconds from a health-check probe, or null if the module could not be reached.
GET /info¶
Returns basic service metadata.
{
"controller_id": "abc123",
"loglevel": "INFO",
"version": "2.3.0"
}