Inference policy hooks
An inference policy hook sends each request to a security service you run, ahead of the model, and lets that service allow or block it. Enterprises use hooks to apply their own data-loss prevention rules, block prompts containing customer identifiers, enforce which teams may use which models, or keep a copy of every prompt in their own compliance archive.
The hook is an organization setting: once it is on, it applies to every request from every key in the organization (or the workspaces you choose), without any change to application code.
When to use one
- Your security team must inspect AI traffic with its own tools, not only Syntic’s built-in safety checks.
- Regulations require that certain data never leaves your control, and your service can recognise it.
- You need an independent, customer-held record of every prompt.
Hooks add latency to every request. If you only need visibility after the fact, the usage report and audit events are cheaper.
How it works
- A request arrives and passes authentication, rate limits and quota checks.
- Syntic sends a signed
POSTto your hook URL with the request’s metadata and content. - Your service answers within the timeout with a verdict:
allow,deny, orallowwith redactions. - On
allow, the request continues to the model. Ondeny, the caller gets403 policy_deniedand nothing is billed. - Optionally (
post_response: true), the model’s complete reply is sent to the hook as well, for logging or a second check before it is returned. This disables streaming for affected requests until the reply is checked.
Configure the hook
Owners configure hooks in Settings → Security → Inference hooks, or with the Admin API:
PUT /v1/organizations/inference_hook
| Field | Required | Notes |
|---|---|---|
url | Yes | HTTPS URL of your service. Must resolve to a public address. |
enabled | Yes | true or false. |
timeout_ms | No | 100 to 5000. Default 1500. |
failure_mode | No | fail_closed (deny if the hook errors or times out; default) or fail_open (allow and record the failure). |
include_content | No | true (default) sends messages and tool definitions; false sends metadata only. |
post_response | No | Also send replies. Default false. |
workspace_ids | No | Apply only to these workspaces. Default: all. |
sample_rate | No | 0.01 to 1.0. Below 1.0, only that share of requests is sent, and unsampled requests are allowed. Default 1.0. |
The response includes a signing_secret the first time a hook is created (and after POST /v1/organizations/inference_hook/rotate_secret).
curl -sS -X PUT https://app.syntic.ai/v1/organizations/inference_hook \
-H "Authorization: Bearer $SYNTIC_ADMIN_KEY" \
-H "Content-Type: application/json" \
-H "syntic-beta: admin-2026-11-01" \
-d '{"url": "https://ai-gateway.example.com/syntic-hook", "enabled": true,
"timeout_ms": 1000, "failure_mode": "fail_closed"}'What your service receives
POST /syntic-hook HTTP/1.1
Content-Type: application/json
syntic-signature: t=1762300800,v1=8c3f1e...{
"type": "inference.request",
"request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
"organization_id": "org_01J9...",
"workspace_id": "wrkspc_01JC0A2C4E6G8J0L2N4Q",
"actor": { "kind": "api_key", "id": "apikey_01JB...", "name": "billing-worker-prod" },
"endpoint": "/v1/chat/completions",
"model": "Amara S1.2",
"request": {
"messages": [{ "role": "user", "content": "Summarise ticket 4821 for the customer Jane Roe." }],
"tools": []
},
"client": { "ip": "203.0.113.7", "user": "end-user-42" }
}Verify syntic-signature exactly as for webhooks. Reject requests whose timestamp is older than 5 minutes.
What your service returns
200 OK with:
{ "verdict": "allow" }{ "verdict": "deny", "reason": "Contains a customer account number.", "code": "dlp.account_number" }{
"verdict": "allow",
"redactions": [{ "path": "messages[0].content", "find": "Jane Roe", "replace": "[NAME]" }]
}reason(up to 200 characters) is shown to the caller in the error message; keep it free of sensitive detail.codeis your own label, recorded for audit.redactionsreplace exact text in the named string fields before the model sees them. Up to 50 per request.- Any other status, an unreadable body or a timeout counts as a hook failure and follows
failure_mode.
What the caller sees
A denied request returns:
{
"error": {
"type": "permission",
"code": "policy_denied",
"message": "Your organization's policy blocked this request: Contains a customer account number.",
"param": null
},
"request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d"
}A hook failure under fail_closed returns 503 overloaded, code policy_hook_unavailable, with retry-after.
Audit and monitoring
- Every verdict is recorded with the request ID, verdict, your
codeand the hook’s latency. Denials appear in audit events asinference.policy_denied. - The Console shows hook latency percentiles and failure counts for the last 24 hours.
- Five minutes of continuous failures sends
inference_hook.failingto your webhooks and emails Owners.
Limits
| Limit | Value |
|---|---|
| Hooks per organization | 1 |
| Payload size sent to the hook | Up to 2 MB; larger requests send metadata only with "content_truncated": true |
| Timeout | 100 to 5000 ms |
Available on Enterprise plans. Hook calls are not billed; the requests they allow are billed as usual.
Build notes for the platform team
- Run the hook after auth, rate limit and quota checks, and before the request is sent upstream; reuse the internal guardrails pipeline slot.
- Fetch the hook URL through the existing URL validator (no private or metadata addresses) and pin the resolved IP per call.
post_responserequires buffering; document clearly that it turns streams into a single final chunk.