Console & adminInference policy hooks

Inference policy hooks

⚠️
Preview — this feature is being built; the design below is the spec.

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

  1. A request arrives and passes authentication, rate limits and quota checks.
  2. Syntic sends a signed POST to your hook URL with the request’s metadata and content.
  3. Your service answers within the timeout with a verdict: allow, deny, or allow with redactions.
  4. On allow, the request continues to the model. On deny, the caller gets 403 policy_denied and nothing is billed.
  5. 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

FieldRequiredNotes
urlYesHTTPS URL of your service. Must resolve to a public address.
enabledYestrue or false.
timeout_msNo100 to 5000. Default 1500.
failure_modeNofail_closed (deny if the hook errors or times out; default) or fail_open (allow and record the failure).
include_contentNotrue (default) sends messages and tool definitions; false sends metadata only.
post_responseNoAlso send replies. Default false.
workspace_idsNoApply only to these workspaces. Default: all.
sample_rateNo0.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. code is your own label, recorded for audit.
  • redactions replace 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 code and the hook’s latency. Denials appear in audit events as inference.policy_denied.
  • The Console shows hook latency percentiles and failure counts for the last 24 hours.
  • Five minutes of continuous failures sends inference_hook.failing to your webhooks and emails Owners.

Limits

LimitValue
Hooks per organization1
Payload size sent to the hookUp to 2 MB; larger requests send metadata only with "content_truncated": true
Timeout100 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_response requires buffering; document clearly that it turns streams into a single final chunk.
Was this page helpful?