Skip to main content

Hooks API

Call your own API on gateway traffic. A hook sends selected request details to an HTTPS endpoint you run. Your endpoint can allow, flag or block the request before the model is called, check the response before it is returned, or receive every finished request in batches for your own logging, billing or analytics. You decide which fields are sent, and how the call and its reply look.

Hooks are created and changed only through the Management API. The console lists them read-only under Settings > AI Gateway > LLM Gateway > Management API > Configured hooks. Try every operation in the Management API playground.

StageWhen it runsWhat your endpoint can doRequests
pre_callAfter Quilr's request checks, before the model call. Also on response-cache hits.Allow, flag or block the request.Streaming and non-streaming
post_callAfter Quilr's response checks, before the response is returned or cached. Also on response-cache hits.Allow, flag or block the response.Non-streaming only (streams skip it)
on_completeAfter the request finishes.Receive a record of it, in batches. Never changes the request.Streaming and non-streaming

Where hooks run​

Client request
POST /openai_compatible/v1/chat/completions
{ "model": "gpt-4.1", "messages": [...] }
app: support-bot   user: dana@example.com
QuilrAI LLM Gateway
Admit
Quilr checks
Access, routing, limits
Request guardrails
pre_call hooks
Your API, in parallel
allow / flag / block
Blocked: 403, no tokens used
Call the model
Provider
Retries and failover
Hooks run once per request, not per attempt
Return
Quilr checks
Response guardrails
post_call hooks
Non-streaming only
allow / flag / block
Blocked: 403, tokens already used
After the response
on_complete hooks
One record per request
Batched POST to your API
Retried, never blocks
Hooks on one stage run in parallel · the strictest decision wins: block > flag > allow
Client response
The model's responseor 403 external_hook_blocked in the client's API format
QuilrAI

Hooks run on these client APIs, and you can narrow a hook to some of them with match.api_families:

Client APIapi_family
OpenAI Chat Completionsopenai.chat_completions
OpenAI Responsesopenai.responses
Anthropic Messagesanthropic.messages
Gemini generateContentgemini.generate_content
Bedrock Conversebedrock.converse
Bedrock InvokeModelbedrock.invoke

Other routes (embeddings, audio, Realtime, Assistants and provider passthrough routes) do not run hooks.

Before you start​

  • The Policy Engine must be on for the LLM Gateway. While it is off, every call answers 409 policy_not_enabled.
  • Scopes: hooks:write to create, change, delete and test hooks; read to list and read them, read the field catalog, preview a draft and read stats. See Scopes.
  • Writes follow the same rules as the other controls: content-idempotent PUT, If-Match ETags, If-None-Match: * to create only, ?dry_run=true, a JSON merge-patch PATCH, and one policy revision per write. See Writes, versions and idempotency.
  • Start in observe mode. A new pre_call or post_call hook in mode: "observe" logs its decisions without enforcing them. Switch to enforce once the decisions look right. Saving a new enforcing hook returns the warning hook_enforcing_without_observe.

The hook resource​

A hook is one resource under /controls/hooks/{id}. This hook checks every Chat Completions and Anthropic Messages request of support-bot with your policy service, in observe mode:

{
"description": "ACME prompt policy check",
"stage": "pre_call",
"mode": "observe",
"scope": {"apps": ["support-bot"]},
"match": {"api_families": ["openai.chat_completions", "anthropic.messages"]},
"secrets": {"api_key": "sk_live_example", "hmac_key": "whsec_example"},
"endpoint": {
"url": "https://guard.example.com/v1/check?app={{app.name}}",
"headers": [
{"name": "X-Api-Key", "value": "{{secret.api_key}}"},
{"name": "X-Agent", "value": "{{client.agent.name}}", "omit_if_empty": true}
],
"signing": {"secret": "hmac_key"},
"timeout_ms": 1500,
"on_failure": "allow"
},
"send": {"preset": "standard", "request_headers": ["user-agent", "x-team-id"]}
}
FieldValues
stagepre_call, post_call or on_complete. Required.
modeenforce (default) or observe. pre_call and post_call only.
stream_modepost_call only: skip (the default and only value). Streaming requests never run post_call hooks.
enabledtrue (default) or false. A disabled hook stays saved and never runs.
descriptionUp to 500 characters.
scopeapps, app_tags, smart_groups, users. Every non-empty list must match; empty lists do not restrict.
all_appsSet true to leave scope empty and apply the hook to every app (otherwise 422 hook_scope_required).
matchapi_families (see above) and requested_models. Empty lists match everything.
secretsNamed values used in headers, query values, auth and signing. Write-only. See Security.
endpointThe call: method, url, headers, auth, signing, body, response, timeout_ms, on_failure. pre_call and post_call only.
exportThe batch delivery: url, headers, auth, signing, record, batch_size, flush_ms, max_batch_bytes. on_complete only. See on_complete export.
sendWhat is sent: preset, include, exclude, body, response_body, request_headers, max_body_bytes, on_oversize. See Choose the fields.

Customize the call​

Everything about the HTTP call is yours to shape, so a hook can call an existing API without an adapter in between.

URL
Fixed https host
Fields in path and query
Headers
Fixed text, secrets
Fields, client headers
Auth and signing
Bearer, basic, API key
HMAC signature
Body
Quilr envelope
Your template, raw, none
Reply
Quilr decision JSON
or mapped from yours
QuilrAI

URL and method​

endpoint.method is POST (default), PUT or PATCH. endpoint.url is an https:// URL with a fixed host, up to 2048 characters:

  • Path segments can use {{app.name}}, {{request_id}}, {{tenant_id}} and {{api_family}}, for example https://guard.example.com/apps/{{app.name}}/check. Values are URL-encoded.
  • Query values can use any single-value field, {{secret.name}} and {{request_header.name}}. Query parameter names are fixed.
  • The host cannot contain placeholders or credentials, and the URL cannot have a #fragment.

on_complete export URLs and headers take fixed text and secrets only, because one batch carries many requests.

Headers​

endpoint.headers is a list of up to 50 {"name", "value", "omit_if_empty"} entries (4096 characters per value, 16 KB in total). A value mixes fixed text with placeholders:

PlaceholderInserts
{{field.path}}A single-value field from the field catalog, for example {{user.email}}. Lists and objects go in the body instead (422 list_field_not_allowed_here).
{{secret.name}}A value from secrets.
{{request_header.name}}A header the client sent, for example {{request_header.x-team-id}}. Credential and identity headers can never be read (422 request_header_not_allowed).

With omit_if_empty: true, a header whose placeholders render empty is left out. Hooks cannot set framing or hop-by-hop headers (Host, Content-Length, Transfer-Encoding, Connection, Content-Type and similar) or X-Quilr-* headers (422 header_not_allowed). A header set twice, or also set by auth or signing, is 422 header_conflict.

Every hook call also carries these headers from the gateway:

HeaderValue
X-Quilr-Request-IdThe gateway request id (not on export batches).
X-Quilr-Hook<hook id>; stage=<stage>; rev=<version>. rev changes whenever the hook's configuration changes.
X-Quilr-Hook-RequestLoop marker. See Security.
User-AgentQuilr-LLM-Gateway-Hooks/1
Idempotency-KeyExport batches only: the same value when the same batch is retried.

Client headers​

send.request_headers forwards headers the client sent: up to 50 names or prefixes ending in * (for example x-team-*). They appear under request_headers in the Quilr envelope. Credential and identity headers (Authorization, API key headers, cookies, identity headers) and X-Quilr-*, X-Amz-* and X-Forwarded-* headers are never sent.

Auth presets​

auth.typeSettingsSends
none (default)-Nothing.
bearersecretAuthorization: Bearer <secret>
basicusername_secret, password_secretAuthorization: Basic <base64>
api_keysecret, and exactly one of header or queryThe secret in that header or query parameter.

Every setting names a secret, never the value itself. A hook with no auth, no signing and no secret header saves with the warning hook_unauthenticated, because your endpoint cannot tell the call comes from Quilr.

HMAC signing​

signing signs the body with a key you supply in secrets:

FieldValues
secretThe secret holding the signing key. Required.
headerDefault X-Quilr-Signature.
formatquilr (default): t=<unix seconds>,v1=<signature of "<t>.<body>">. body: the signature of the body alone, or of <t>.<body> when timestamp_header is set.
algorithmsha256 (default) or sha512.
encodinghex (default) or base64.
prefixText put before the signature, for example sha256=.
timestamp_headerAlso send the timestamp in this header.

Verify the default signature on your endpoint before you trust the call, and reject old timestamps:

import hashlib, hmac, time

def verify(body: bytes, header: str, key: str, tolerance_s: int = 300) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp, signature = parts["t"], parts["v1"]
if abs(time.time() - int(timestamp)) > tolerance_s:
return False
expected = hmac.new(key.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

# verify(request.body, request.headers["X-Quilr-Signature"], HOOK_SIGNING_KEY)
const crypto = require('crypto');

function verify(body, header, key, toleranceS = 300) {
const parts = Object.fromEntries(header.split(',').map((item) => item.split('=', 2)));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceS) return false;
const expected = crypto.createHmac('sha256', key).update(`${parts.t}.`).update(body).digest('hex');
return expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Sign over the raw body bytes you received, before any JSON parsing. With a prefix, strip it from v1 first.

Body​

endpoint.body.format (and export.record.format for on_complete) chooses the body:

FormatBody
quilr (default)The Quilr envelope below.
templateYour own JSON shape (up to 16 KB, 16 levels deep). {"$value": "<field>"} inserts a field's JSON value, strings take {{field}} and {{request_header.name}} placeholders, {"$raw": "request_body"} inserts the client's request body and {"$raw": "response_body"} the response body (post_call and on_complete, with send.response_body: true). Secrets are never allowed in the body.
rawThe client's request body exactly as received. Call hooks only.
noneAn empty body. Call hooks only.

The Quilr envelope of a pre_call call:

{
"version": "1",
"stage": "pre_call",
"hook": "acme-guard",
"request_id": "req_8f2c",
"tenant_id": "tenant_sample",
"timestamp": "2026-11-04T10:15:02.114Z",
"api_family": "openai.chat_completions",
"endpoint": "/openai_compatible/v1/chat/completions",
"stream": false,
"body": {"model": "gpt-4.1", "messages": [{"role": "user", "content": "Hello"}]},
"body_omitted": null,
"request_headers": {"user-agent": "claude-cli/2.0.0", "x-team-id": "support"},
"metadata": {
"request_id": "req_8f2c",
"user": {"email": "dana@example.com", "verified": true, "groups": ["support"]},
"app": {"name": "support-bot", "tags": ["prod"]},
"model": {"requested": "gpt-4.1", "target": "gpt-4.1"}
}
}
  • body is the client's body exactly as it was received, before any Quilr redaction. A body that is not JSON (some Bedrock InvokeModel bodies) is base64 text with "body_encoding": "base64".
  • body_omitted is null when the body is included, "not_requested" with send.body: "none", or "too_large" when it exceeded send.max_body_bytes.
  • response_body (post_call and on_complete with send.response_body: true) is the response exactly as it is sent to the client. A response that is not JSON is base64 text with "response_body_encoding": "base64".
  • response_body_omitted is null when the response body is included, "not_requested", "too_large", or "stream" (streaming responses never carry a body).
  • request_headers is present only when send.request_headers is set.
  • metadata holds the selected fields, nested by their dotted path. A field with no value is null, never invented.

Choose the fields​

send decides what a hook receives:

SettingValues
presetminimal, standard (default), full_export or empty.
includeExtra fields: exact paths, or prefix.* for every non-sensitive field under a prefix (for example user.*).
excludeFields to drop from the preset, exact or prefix.*.
bodyas_received (default for pre_call and post_call) or none (default for on_complete).
response_bodySend the response the client receives. post_call (default true) and on_complete (default false); non-streaming requests only.
request_headersClient headers to forward (see Client headers).
max_body_bytesLargest body sent, 1 KB to 4 MB (default 1 MB). Applies to the request body and the response body separately.
on_oversizeWhen the body is larger: send_without_body (default; body_omitted: "too_large"), skip_hook (the hook does not run; call hooks) or apply_on_failure (the hook's on_failure applies; call hooks).
  • minimal sends request_id, api_family, stream, user.email, app.name, model.requested and model.target.
  • standard adds the identity, client agent, Smart Group, app, provider and request fields, Quilr's findings (quilr.status, quilr.categories, quilr.action and their response versions) and budgets, and the outcome, usage, cost and latency fields at the stages that have them.
  • full_export sends every non-sensitive field available at the stage, including client.headers and failover.
  • empty sends no fields, for a hook that needs only the body or its own template.

Sensitive fields (client.ip, identity.jwt_claims, quilr.entities, quilr.response_entities) are never part of a preset or a prefix.* pattern. Name them by exact path in include to send them. Fields used in the URL, headers or a body template are added to the selection automatically. GET /controls/hooks/field-catalog?stage= lists every field with its type, stages, presets and whether it is sensitive.

Field catalog​

FieldTypeStagesDescription
request_idstringallGateway request id (also X-Quilr-Request-Id).
tenant_idstringallYour organization's tenant id.
api_familystringallClient API, for example openai.chat_completions.
endpointstringallGateway path the client called.
streambooleanallWhether the client asked for a stream.
user.emailstringallUser the gateway attributed the request to (null when anonymous).
user.id, user.full_namestringallUser id and display name.
user.identity_sourcestringallWhere the identity came from: jwt_claims, token_header, self_service, admin_api_key, identity_header, body or none.
user.verifiedbooleanalltrue only for a validated token or a Quilr-issued key. Identity headers are client-supplied.
user.department, user.groupsstring, listallFrom your identity directory.
client.ipstringallClient IP address. Sensitive.
client.agent.name, .version, .confidencestringallDetected agent or SDK, for example claude-code, and how confident the detection is.
client.session_idsobjectallSession, conversation and trace ids the agent sent.
client.headersobjectallKnown client headers after masking (credentials never included).
conversation.idstringallConversation or session id the client sent.
smart_groups.names, smart_groups.alllistallEvery Smart Group the user belongs to, as names or {name, display_name}.
app.name, app.display_namestringallThe app's key name (what scopes use) and console name.
app.key_id, app.tagsstring, listallId of the credential presented, and the app's tags.
model.requested, model.targetstringallModel the client asked for, and the model selected by routing (at post_call and on_complete, the model that served it).
provider.name, provider.labelstringallProvider and provider label of the selected target.
request.metadataobjectallRequest metadata the client sent.
request.token_estimatenumberallThe gateway's estimate of input tokens.
policy.revisionnumberallPolicy revision serving the request.
key.self_serviceobjectallPersonal key id and name, when the request used one.
identity.jwt_claimsobjectallCaptured identity-token claims. Sensitive.
quilr.statusstringallWhether Quilr's request checks ran: ran, not_run, or not_run_cache_hit (a response-cache hit skips them).
quilr.categorieslistallCategory ids Quilr detected in the request, for example ["data_risk_category_pii"]. null when quilr.status is not ran.
quilr.actionstringallWhat Quilr did with the request: Allow, Block_Prompt, Anonymize_Prompt, or failed (findings in monitor-only categories). null when the checks did not run.
quilr.entitieslistallDetected values in the request, [{category, subcategory, value, offsets}] (subcategory and offsets when known), at most 200. Sensitive.
budgetslistallBudgets that matched at admission: [{id, name, metric, window, limit, used, remaining, unit}]. See below. null when no budget matched.
quilr.response_statusstringpost_call, on_completeWhether Quilr's response checks ran (same values as quilr.status).
quilr.response_categorieslistpost_call, on_completeCategory ids Quilr detected in the response.
quilr.response_actionstringpost_call, on_completeWhat Quilr did with the response (same values as quilr.action).
quilr.response_entitieslistpost_call, on_completeDetected values in the response (same shape as quilr.entities). Sensitive.
outcome.statusnumberpost_call, on_completeHTTP status returned to the client (at post_call, of the response about to be returned).
outcome.error_typestringon_completeError type, when the request failed or was blocked.
outcome.blocked_bystringon_completequilr, hook, gateway or provider; null when not blocked.
usageobjectpost_call, on_completeinput_tokens, output_tokens, source and a breakdown.
costobjecton_completePriced cost (null when unpriced).
latency_ms.total, latency_ms.first_responsenumberon_completeTotal gateway latency and time to the first response byte.
cache_hitbooleanpost_call, on_completeServed from the response cache.
failoverobjecton_completeFailover attempts and outcome.
hooksliston_completeResults of this request's pre_call and post_call hooks.

budgets: metric is spend, tokens or requests; spend is in USD with unit: "usd". used includes this request's own admission charge, and used and remaining are null when admission was deferred. At on_complete it is the snapshot taken at admission.

A field used at a stage that does not have it is 422 field_not_available_at_stage; an unknown path is 422 unknown_field.

What your endpoint returns​

The Quilr reply (default)​

Answer 2xx with JSON:

{
"decision": "block",
"reason": "Customer account numbers are not allowed in support prompts",
"user_message": "This request contains an account number. Remove it and try again.",
"labels": ["pii", "account-number"],
"metadata": {"policy": "support-v3"}
}
FieldMeaning
decisionRequired. allow, flag (continue, and record the flag) or block.
reasonRecorded with the request (up to 1000 characters). Never shown to the client.
user_messageShown to the client when the request is blocked. Without it the client sees "This request was blocked by an organization policy hook."
labelsUp to 20 strings of 64 characters, recorded with the request.
metadataUp to 4 KB of JSON, recorded with the request.

A non-2xx status, a body that is not JSON or a missing decision is a failure (see Timeouts and failures).

Map your own reply​

To call an API that already has its own reply shape, set endpoint.response.format to mapped:

{
"format": "mapped",
"status_map": {"403": "block", "429": "allow"},
"decision": {
"path": "$.result.action",
"block_values": ["deny", "reject"],
"flag_values": ["review"],
"case_insensitive": true,
"otherwise": "allow"
},
"reason_path": "$.result.explanation",
"user_message_path": "$.result.message",
"labels_path": "$.result.tags[*]",
"metadata_paths": {"score": "$.result.score"}
}
  1. A status listed in status_map decides directly.
  2. Otherwise a non-2xx status is a failure.
  3. Otherwise the value at decision.path decides: block if it equals a block_values entry, else flag if it equals a flag_values entry, else otherwise (allow, or failure to treat it as a failure). With no decision, a 2xx reply allows.

Paths start with $ and use dotted keys, [n] and at most one [*]. A mapped response needs status_map or decision.

Several hooks and observe mode​

  • Hooks on the same stage run in parallel. The strictest decision wins (block over flag over allow); when several hooks block, the message of the hook with the lowest id is shown.
  • In observe mode the decision is recorded but never enforced.
  • Each request's log records every hook that ran: hook id, stage, mode, status, decision, latency, the HTTP status, an error code on failure, and your reason, labels and metadata. On on_complete, the hooks field carries the same results.

What the client sees when a hook blocks​

The client gets 403 with code external_hook_blocked, the hook id and the request id, in its own API's error format, plus the x-quilr-request-id header. A pre_call block of a streaming request is returned before the stream starts.

Client APIBody
OpenAI Chat Completions, Responses{"error": {"message": "...", "type": "permission_error", "param": null, "code": "external_hook_blocked", "hook": "acme-guard", "request_id": "req_8f2c"}}
Anthropic Messages{"type": "error", "error": {"type": "permission_error", "message": "..."}, "request_id": "req_8f2c", "quilr": {"code": "external_hook_blocked", "hook": "acme-guard"}}
Gemini generateContent{"error": {"code": 403, "message": "...", "status": "PERMISSION_DENIED", "details": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "external_hook_blocked", "domain": "quilr.ai", "metadata": {"hook": "acme-guard", "request_id": "req_8f2c"}}]}}
Bedrock Converse, InvokeModel{"message": "...", "code": "external_hook_blocked", "hook": "acme-guard", "request_id": "req_8f2c"} with x-amzn-ErrorType: AccessDeniedException

message is your user_message. A pre_call block stops the request before the model call, so no tokens are used. A post_call block replaces a response the model already produced: those tokens are used and billed.

Timeouts and failures​

SettingValues
endpoint.timeout_ms100 to 10000, default 1500. The whole call, including connecting.
endpoint.on_failureallow (default) or block. What happens when the call fails.

A call fails on a timeout, a connection error (connecting is retried once within the timeout), a non-2xx or unreadable reply, a reply over 64 KB or nested deeper than 32 levels, or a hook that cannot run (for example its secrets cannot be read). The failure is recorded with an error code and on_failure applies. When a failure is allowed through, the response is not stored in the response cache.

Circuit breaker. After 5 consecutive failures on a gateway server, the hook is skipped there for 30 seconds and its on_failure applies without a call. Then one call probes the endpoint; success closes the breaker. GET /controls/hooks/{id}/stats reports whether it is open on the server that answers.

on_complete export​

An on_complete hook delivers one record per finished request to export.url, in batches. Records cover requests that reached the hook stage (after access, routing, limits and Quilr's request checks), including requests a hook blocked and requests the provider failed.

{
"stage": "on_complete",
"description": "Usage export to our warehouse",
"all_apps": true,
"secrets": {"token": "warehouse_token_example"},
"export": {
"url": "https://ingest.example.com/quilr/requests",
"auth": {"type": "bearer", "secret": "token"},
"batch_size": 200,
"flush_ms": 5000
},
"send": {"preset": "standard"}
}

Your endpoint receives a POST with {"records": [...]}. Each record is the Quilr envelope (with stage: "on_complete") or your export.record.template:

{
"records": [
{
"version": "1",
"stage": "on_complete",
"hook": "warehouse-export",
"request_id": "req_8f2c",
"tenant_id": "tenant_sample",
"timestamp": "2026-11-04T10:15:03.020Z",
"api_family": "openai.chat_completions",
"endpoint": "/openai_compatible/v1/chat/completions",
"stream": false,
"body": null,
"body_omitted": "not_requested",
"metadata": {
"app": {"name": "support-bot"},
"model": {"requested": "gpt-4.1", "target": "gpt-4.1"},
"outcome": {"status": 200, "error_type": null, "blocked_by": null},
"usage": {"input_tokens": 12, "output_tokens": 40, "source": "provider"},
"cost": {"total_usd": 0.0004},
"latency_ms": {"total": 820, "first_response": 310}
}
}
]
}
SettingValues
export.batch_size1 to 1000 records, default 100.
export.flush_ms250 to 60000, default 5000. A partly filled batch is sent after this time.
export.max_batch_bytes64 KB to 8 MB, default 4 MB.
export.record{"format": "quilr"} (default) or {"format": "template", "template": {...}}.
  • Delivery: any 2xx within 30 seconds delivers the batch. Each batch is tried up to 3 times with the same Idempotency-Key, so deduplicate on it.
  • While your endpoint is down: after a batch fails all its tries, the hook is unhealthy. The gateway keeps only the latest 100 records for it and sends one probe batch every 30 seconds; when one succeeds, normal delivery resumes.
  • Retention: at most 50,000 records or 256 MB are queued per hook on each gateway server; the oldest are dropped first. Deleting a hook drops its queued records.
  • Bodies: set send.body: "as_received" to include the request body, and send.response_body: true to include the response body of non-streaming requests (response_body_omitted: "stream" for streams).
  • GET /controls/hooks/{id}/stats shows the queue on the server that answers: healthy, queued, queued_bytes, delivered, dropped, failed_batches, last_error and last_success.

Security​

  • Secrets are write-only. Up to 10 per hook, named [a-z][a-z0-9_]*, up to 4096 characters each. Reads return ********. Sending ******** back (or leaving secrets out of a PATCH) keeps the stored value. Secrets go only in headers, query values, auth and signing, never in the body (422 secret_not_allowed_here). A secret that is defined but never used saves with the warning hook_secret_unused.
  • Destinations. Only https:// URLs to public addresses. Quilr hosts, localhost and private or loopback addresses are refused at save (422 destination_not_allowed), and every connection resolves the host again and connects only to a public address. Dedicated and on-premises deployments can allow private networks through deployment configuration, never through the API.
  • Credentials never leave the gateway. The client's credential and identity headers can never be sent to a hook, in any setting.
  • Loop protection. Every hook call carries X-Quilr-Hook-Request. If your endpoint calls the LLM Gateway itself (for example to ask a model), forward that header unchanged: hooks do not run on that inner request, so a hook cannot call itself in a loop. The value is signed for your organization and that request, so a forged or copied value never skips hooks.
  • Hooks see the original request. The body is sent exactly as the client sent it, before any Quilr redaction. Use send.body: "none" or an exclude list when a hook does not need content.

Preview, test and stats​

  • Preview (POST /controls/hooks/preview, read) validates a draft and returns the exact call it would make for a sample request of the chosen API family: method, URL, headers and body, with secrets masked. Nothing is saved or sent.
  • Test (POST /controls/hooks/{id}/test, hooks:write) sends one sample call with the saved hook and returns the HTTP status, latency and the parsed decision. An on_complete test sends one batch with a sample record.
  • Stats (GET /controls/hooks/{id}/stats, read) returns the circuit breaker and export queue state on the server that answers.

Preview and test are limited to 10 calls a minute per organization (429 rate_limited with retry_after_seconds). Both use sample values (user@example.com, support-bot, req_sample) and a small sample body, or your own sample_body.

export API="$MANAGEMENT_URL/llmgateway/management/v1"
auth=(-H "Authorization: Bearer $MANAGEMENT_KEY" -H 'Content-Type: application/json')

# Preview a draft
curl -X POST "$API/controls/hooks/preview" "${auth[@]}" \
-d '{"api_family": "openai.chat_completions", "hook": {"stage": "pre_call", "mode": "observe", "all_apps": true, "endpoint": {"url": "https://guard.example.com/v1/check"}, "send": {"preset": "minimal"}}}'

# Create it (409/412 rules as for every control; add ?dry_run=true to validate only)
curl -X PUT "$API/controls/hooks/acme-guard" "${auth[@]}" -H 'If-None-Match: *' \
-d '{"stage": "pre_call", "mode": "observe", "all_apps": true, "endpoint": {"url": "https://guard.example.com/v1/check"}, "send": {"preset": "minimal"}}'

# Send a test call
curl -X POST "$API/controls/hooks/acme-guard/test" "${auth[@]}" -d '{"api_family": "anthropic.messages"}'

A test result:

{
"ok": true,
"stage": "pre_call",
"api_family": "anthropic.messages",
"http_status": 200,
"latency_ms": 84.2,
"error_code": null,
"decision": "allow",
"reason": null,
"user_message": null,
"labels": [],
"metadata": null
}

Errors​

StatusCodeMeaning
422hook_scope_requiredAn empty scope needs all_apps: true.
422hook_limitMore than 3 enabled enforcing hooks on one stage (pre_call and post_call count separately). Set extra ones to observe.
422too_many_resourcesMore than 10 hooks in the organization.
422unknown_fieldA field path is not in the field catalog.
422field_not_available_at_stageThe field is not available at the hook's stage (for example usage at pre_call).
422list_field_not_allowed_hereA list or object field was used in a header, the URL or a string. Use {"$value": ...} in a body template.
422field_not_allowed_hereThe field or client header cannot be used in that position (for example a URL path segment).
422secret_not_allowed_hereSecrets go only in headers, query values, auth and signing.
422unknown_secretA {{secret.name}}, auth or signing secret is not defined in secrets.
422header_not_allowedA framing, hop-by-hop or X-Quilr-* header.
422header_conflictA header is set twice, or also by auth or signing.
422request_header_not_allowedA credential, identity or X-Quilr-* client header can never be sent.
422destination_not_allowedNot https, or a Quilr, local or private host.
422api_family_unsupportedThe API family does not run hooks.
422validation_failed, unsupported_fieldsAny other invalid value, or a field the resource does not take.
409policy_not_enabledThe Policy Engine is off for the LLM Gateway.
404not_foundTest or stats for a hook that does not exist.
429rate_limitedMore than 10 previews and tests a minute. Retry after retry_after_seconds.
403external_hook_blockedReturned to gateway clients (not to the Management API) when a hook blocks a request or response.

Writes also return the usual control errors (412 version_conflict, 428 precondition_required and the others in Management API errors).

Limits​

LimitValue
Hooks per organization10
Enabled enforcing hooks3 for pre_call and 3 for post_call (observe-mode and disabled hooks do not count)
Call timeout100 to 10000 ms, default 1500
Secrets10 per hook, 4096 characters each
URL2048 characters
Headers50 per hook, 4096 characters each, 16 KB in total
Body template16 KB, 16 levels deep
Body sent to a hook1 KB to 4 MB (send.max_body_bytes, default 1 MB)
Reply read from a hook64 KB, 32 levels deep
Labels and metadata in a reply20 labels of 64 characters; 4 KB of metadata
Export batch1000 records, 8 MB
Export queue50,000 records or 256 MB per hook per server; 100 while unhealthy
Preview and test10 a minute per organization

Endpoints​

GET/controls/hooks

List hooks

Every managed hook with its ETag, then the console policy rules of the same kind as read-only rows (source console, policy, rule_index, view). Console rows cannot be written here (409 rule_not_api_managed). Paginated (limit, cursor bound to the revision) and filtered by source, app, requested_model, mode and references.

REQUIRESread
Path, query & header parameters 7
limit (query)integeroptional

Page size.

min: 1max: 500default: 100
cursor (query)stringoptional

next_cursor of the previous page; bound to the revision (409 collection_changed when the policy changed).

source (query)objectoptional

Only managed resources or only console rows.

"managed""console"
app (query)stringoptional

Rows that can apply to this app (rows scoped only by smart group, user or tag are kept). app_name is a deprecated alias.

requested_model (query)stringoptional

Rows that can apply to this requested model. model is a deprecated alias.

mode (query)stringoptional

Rows with this mode (routes: active, observe, disabled; limits: enforce, alert, paused).

references (query)stringoptional

<provider_label> or <provider_label>/<model>: rows that name it, following destination and failover-profile refs (adds references [paths]).

No request body.

curl --request GET \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks?limit=example' \
--header 'Authorization: Bearer <management-key>'

Authentication, errors & retry rules Try it in the playground

GET/controls/hooks/field-catalog

List the fields a hook can send

Every field a hook can request, with its type, the stages it is available at, whether it is sensitive, whether it fits a header or URL slot and the presets that include it, plus the fields of each preset per stage. stage narrows the list to one stage.

REQUIRESread

No request body.

curl --request GET \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/field-catalog' \
--header 'Authorization: Bearer <management-key>'

Authentication, errors & retry rules Try it in the playground

POST/controls/hooks/preview

Preview the call a hook would make

Validates a draft hook and renders the exact HTTP call it would make for a sample request of the chosen API family, with secrets masked. Nothing is saved or sent. Limited to 10 previews and tests per minute.

REQUIRESread
Full request body specification application/json

api_familystringoptional
hookobjectrequired

External hooks call your own HTTPS API on gateway traffic. A pre_call hook runs after Quilr's checks and before the model call and can allow, flag or block the request (observe mode logs without enforcing). A post_call hook runs on the final non-streaming response (after Quilr's response checks, before it is cached or returned) and can allow, flag or block it; streaming requests skip it. An on_complete hook exports finished requests in batches. You choose the fields sent, the method, URL, headers, auth and signing, the body shape and how the reply maps to a decision. Secrets are write-only.

Object fields
all_appsbooleanoptional

Required (true) to leave scope empty.

descriptionstringoptional
max length: 500
enabledbooleanoptional
default: true
endpointobjectoptional

pre_call and post_call.

Object fields
authobjectoptional
Object fields
headerstringoptional
password_secretstringoptional
querystringoptional
secretstringoptional
typeobjectoptional
"none""bearer""basic""api_key"
username_secretstringoptional
bodyobjectoptional
Object fields
formatobjectoptional
"quilr""template""raw""none"
templateobjectoptional

JSON in your own schema.

headersarray<object>optional
Array item specification
namestringrequired
omit_if_emptybooleanoptional
valuestringrequired

Fixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.

Unknown fields are rejected in this object.

methodobjectoptional
"POST""PUT""PATCH"
on_failureobjectoptional
default: "allow"
"allow""block"
responseobjectoptional
Object fields
decisionobjectoptional
Object fields
block_valuesarray<object>optional
Array item specification
object
case_insensitivebooleanoptional
flag_valuesarray<object>optional
Array item specification
object
otherwiseobjectoptional
"allow""failure"
pathstringoptional
formatobjectoptional
"quilr""mapped"
labels_pathstringoptional
metadata_pathsobjectoptional
reason_pathstringoptional
status_mapobjectoptional
user_message_pathstringoptional
signingobjectoptional
Object fields
algorithmobjectoptional
"sha256""sha512"
encodingobjectoptional
"hex""base64"
formatobjectoptional

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

"quilr""body"
headerstringoptional
default: "X-Quilr-Signature"
prefixstringoptional
secretstringrequired
timestamp_headerstringoptional
timeout_msintegeroptional
min: 100max: 10000default: 1500
urlstringoptional
exportobjectoptional

on_complete only.

Object fields
authobjectoptional
Object fields
headerstringoptional
password_secretstringoptional
querystringoptional
secretstringoptional
typeobjectoptional
"none""bearer""basic""api_key"
username_secretstringoptional
batch_sizeintegeroptional
min: 1max: 1000default: 100
flush_msintegeroptional
min: 250max: 60000default: 5000
headersarray<object>optional
Array item specification
namestringrequired
omit_if_emptybooleanoptional
valuestringrequired

Fixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.

Unknown fields are rejected in this object.

max_batch_bytesintegeroptional
default: 4194304
recordobjectoptional
Object fields
formatobjectoptional
"quilr""template"
templateobjectoptional
signingobjectoptional
Object fields
algorithmobjectoptional
"sha256""sha512"
encodingobjectoptional
"hex""base64"
formatobjectoptional

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

"quilr""body"
headerstringoptional
default: "X-Quilr-Signature"
prefixstringoptional
secretstringrequired
timestamp_headerstringoptional
urlstringoptional
matchobjectoptional
Object fields
api_familiesarray<object>optional
Array item specification
object

Values: "openai.chat_completions""openai.responses""anthropic.messages""gemini.generate_content""bedrock.converse""bedrock.invoke"

requested_modelsarray<string>optional
Array item specification
string
modeobjectoptional

pre_call and post_call only. observe logs decisions and never blocks.

"enforce""observe"
scopeobjectoptional
Object fields
app_tagsarray<string>optional
Array item specification
string
appsarray<string>optional
Array item specification
string
smart_groupsarray<string>optional
Array item specification
string
usersarray<string>optional
Array item specification
string
secretsmap<string, string>optional

Write-only. Reads return ******** ; sending ******** keeps the stored value.

Map value specification
string
sendobjectoptional
Object fields
bodyobjectoptional
"as_received""none"
excludearray<string>optional
Array item specification
string
includearray<string>optional
Array item specification
string
max_body_bytesintegeroptional
min: 1024max: 4194304
on_oversizeobjectoptional
"send_without_body""skip_hook""apply_on_failure"
presetobjectoptional
"minimal""standard""full_export""empty"
request_headersarray<string>optional
Array item specification
string
response_bodybooleanoptional

post_call (default true) and on_complete (default false) only: send the response the client receives, as delivered (quilr envelope key response_body, or {"$raw": "response_body"} in a template). Non-streaming requests only; streams carry response_body_omitted: "stream".

stageobjectrequired
"pre_call""post_call""on_complete"
stream_modeobjectoptional

post_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).

default: "skip"
"skip"

Unknown fields are rejected in this object.

sample_bodyobjectoptional

Client body to use instead of the family's sample.

sample_response_bodyobjectoptional

Response body to use instead of the family's sample (post_call / on_complete with send.response_body).

curl --request POST \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/preview' \
--header 'Authorization: Bearer <management-key>' \
--header 'Content-Type: application/json' \
--data '{
"api_family": "openai.chat_completions",
"hook": {
"description": "ACME prompt policy check",
"endpoint": {
"headers": [
{
"name": "X-Api-Key",
"value": "{{secret.api_key}}"
},
{
"name": "X-Agent",
"omit_if_empty": true,
"value": "{{client.agent.name}}"
}
],
"on_failure": "allow",
"signing": {
"secret": "hmac_key"
},
"timeout_ms": 1500,
"url": "https://guard.example.com/v1/check?app={{app.name}}"
},
"match": {
"api_families": [
"openai.chat_completions",
"anthropic.messages"
]
},
"mode": "observe",
"scope": {
"apps": [
"support-bot"
]
},
"secrets": {
"api_key": "sk_live_example",
"hmac_key": "whsec_example"
},
"send": {
"preset": "standard",
"request_headers": [
"user-agent",
"x-team-id"
]
},
"stage": "pre_call"
}
}'

Authentication, errors & retry rules Try it in the playground

DELETE/controls/hooks/{resource_id}

Delete a hook

Removes the hook; refused while other controls reference it. If-Match is required.

REQUIREShooks:write
Path, query & header parameters 4
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

dry_run (query)booleanoptional

Preview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.

default: false
If-Match (header)stringrequired

Resource ETag "r<revision>-<id>-<hash8>" from the latest read, or the collection ETag "r<revision>". Required to change or delete an existing resource (428 when missing, 412 when stale); an unrelated publish does not invalidate it.

Idempotency-Key (header)stringoptional

Unique per logical write. A retry with the same key and body returns the original result, including after a crash once the revision was published. Optional on PUT, PATCH and DELETE.

min length: 1max length: 200
Full request body specification application/json

Unknown fields are rejected in this object.

curl --request DELETE \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool' \
--header 'Authorization: Bearer <management-key>' \
--header 'If-Match: "r12-eu-pool-1a2b3c4d"'

Authentication, errors & retry rules Try it in the playground

GET/controls/hooks/{resource_id}

Read a hook

One hook and its ETag.

REQUIRESread
Path, query & header parameters 1
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

No request body.

curl --request GET \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool' \
--header 'Authorization: Bearer <management-key>'

Authentication, errors & retry rules Try it in the playground

PATCH/controls/hooks/{resource_id}

Update a hook

JSON merge patch of the hook (null removes a field); the result is validated as a whole. If-Match is required. Writes publish one policy revision. A PUT whose content equals the stored resource returns changed false and publishes nothing.

REQUIREShooks:write
Path, query & header parameters 4
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

dry_run (query)booleanoptional

Preview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.

default: false
If-Match (header)stringrequired

Resource ETag "r<revision>-<id>-<hash8>" from the latest read, or the collection ETag "r<revision>". Required to change or delete an existing resource (428 when missing, 412 when stale); an unrelated publish does not invalidate it.

Idempotency-Key (header)stringoptional

Unique per logical write. A retry with the same key and body returns the original result, including after a crash once the revision was published. Optional on PUT, PATCH and DELETE.

min length: 1max length: 200
Full request body specification application/json

JSON merge patch of ControlsHooks: send only the fields to change; null removes a field (nested objects merge, lists are replaced). The merged resource must be a valid ControlsHooks.

all_appsboolean | nulloptional
Variant 1 boolean
boolean

Required (true) to leave scope empty.

Also accepts null.

descriptionstring | nulloptional
Variant 1 string
string
max length: 500

Also accepts null.

enabledboolean | nulloptional
Variant 1 boolean
boolean
default: true

Also accepts null.

endpointobject | nulloptional
Variant 1 object
authobject | nulloptional
Variant 1 object
headerstring | nulloptional
Variant 1 string
string

Also accepts null.

password_secretstring | nulloptional
Variant 1 string
string

Also accepts null.

querystring | nulloptional
Variant 1 string
string

Also accepts null.

secretstring | nulloptional
Variant 1 string
string

Also accepts null.

typeobject | nulloptional
Variant 1 object
object

Values: "none""bearer""basic""api_key"

Also accepts null.

username_secretstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

bodyobject | nulloptional
Variant 1 object
formatobject | nulloptional
Variant 1 object
object

Values: "quilr""template""raw""none"

Also accepts null.

templateobject | nulloptional
Variant 1 object
object

JSON in your own schema.

Also accepts null.

Also accepts null.

headersarray<object> | nulloptional
Variant 1 array<object>
array<object>

Also accepts null.

methodobject | nulloptional
Variant 1 object
object

Values: "POST""PUT""PATCH"

Also accepts null.

on_failureobject | nulloptional
Variant 1 object
object
default: "allow"

Values: "allow""block"

Also accepts null.

responseobject | nulloptional
Variant 1 object
decisionobject | nulloptional
Variant 1 object
block_valuesarray<object> | nulloptional
Variant 1 array<object>
array<object>

Also accepts null.

case_insensitiveboolean | nulloptional
Variant 1 boolean
boolean

Also accepts null.

flag_valuesarray<object> | nulloptional
Variant 1 array<object>
array<object>

Also accepts null.

otherwiseobject | nulloptional
Variant 1 object
object

Values: "allow""failure"

Also accepts null.

pathstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

formatobject | nulloptional
Variant 1 object
object

Values: "quilr""mapped"

Also accepts null.

labels_pathstring | nulloptional
Variant 1 string
string

Also accepts null.

metadata_pathsobject | nulloptional
Variant 1 object
object

Also accepts null.

reason_pathstring | nulloptional
Variant 1 string
string

Also accepts null.

status_mapobject | nulloptional
Variant 1 object
object

Also accepts null.

user_message_pathstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

signingobject | nulloptional
Variant 1 object
algorithmobject | nulloptional
Variant 1 object
object

Values: "sha256""sha512"

Also accepts null.

encodingobject | nulloptional
Variant 1 object
object

Values: "hex""base64"

Also accepts null.

formatobject | nulloptional
Variant 1 object
object

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

Values: "quilr""body"

Also accepts null.

headerstring | nulloptional
Variant 1 string
string
default: "X-Quilr-Signature"

Also accepts null.

prefixstring | nulloptional
Variant 1 string
string

Also accepts null.

secretstring | nulloptional
Variant 1 string
string

Also accepts null.

timestamp_headerstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

timeout_msinteger | nulloptional
Variant 1 integer
integer
min: 100max: 10000default: 1500

Also accepts null.

urlstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

exportobject | nulloptional
Variant 1 object
authobject | nulloptional
Variant 1 object
headerstring | nulloptional
Variant 1 string
string

Also accepts null.

password_secretstring | nulloptional
Variant 1 string
string

Also accepts null.

querystring | nulloptional
Variant 1 string
string

Also accepts null.

secretstring | nulloptional
Variant 1 string
string

Also accepts null.

typeobject | nulloptional
Variant 1 object
object

Values: "none""bearer""basic""api_key"

Also accepts null.

username_secretstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

batch_sizeinteger | nulloptional
Variant 1 integer
integer
min: 1max: 1000default: 100

Also accepts null.

flush_msinteger | nulloptional
Variant 1 integer
integer
min: 250max: 60000default: 5000

Also accepts null.

headersarray<object> | nulloptional
Variant 1 array<object>
array<object>

Also accepts null.

max_batch_bytesinteger | nulloptional
Variant 1 integer
integer
default: 4194304

Also accepts null.

recordobject | nulloptional
Variant 1 object
formatobject | nulloptional
Variant 1 object
object

Values: "quilr""template"

Also accepts null.

templateobject | nulloptional
Variant 1 object
object

Also accepts null.

Also accepts null.

signingobject | nulloptional
Variant 1 object
algorithmobject | nulloptional
Variant 1 object
object

Values: "sha256""sha512"

Also accepts null.

encodingobject | nulloptional
Variant 1 object
object

Values: "hex""base64"

Also accepts null.

formatobject | nulloptional
Variant 1 object
object

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

Values: "quilr""body"

Also accepts null.

headerstring | nulloptional
Variant 1 string
string
default: "X-Quilr-Signature"

Also accepts null.

prefixstring | nulloptional
Variant 1 string
string

Also accepts null.

secretstring | nulloptional
Variant 1 string
string

Also accepts null.

timestamp_headerstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

urlstring | nulloptional
Variant 1 string
string

Also accepts null.

Also accepts null.

matchobject | nulloptional
Variant 1 object
api_familiesarray<object> | nulloptional
Variant 1 array<object>
array<object>

Also accepts null.

requested_modelsarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

Also accepts null.

modeobject | nulloptional
Variant 1 object
object

pre_call and post_call only. observe logs decisions and never blocks.

Values: "enforce""observe"

Also accepts null.

scopeobject | nulloptional
Variant 1 object
app_tagsarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

appsarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

smart_groupsarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

usersarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

Also accepts null.

secretsmap<string, string> | nulloptional
Variant 1 map<string, string>
map<string, string>

Write-only. Reads return ******** ; sending ******** keeps the stored value.

Also accepts null.

sendobject | nulloptional
Variant 1 object
bodyobject | nulloptional
Variant 1 object
object

Values: "as_received""none"

Also accepts null.

excludearray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

includearray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

max_body_bytesinteger | nulloptional
Variant 1 integer
integer
min: 1024max: 4194304

Also accepts null.

on_oversizeobject | nulloptional
Variant 1 object
object

Values: "send_without_body""skip_hook""apply_on_failure"

Also accepts null.

presetobject | nulloptional
Variant 1 object
object

Values: "minimal""standard""full_export""empty"

Also accepts null.

request_headersarray<string> | nulloptional
Variant 1 array<string>
array<string>

Also accepts null.

response_bodyboolean | nulloptional
Variant 1 boolean
boolean

post_call (default true) and on_complete (default false) only: send the response the client receives, as delivered (quilr envelope key response_body, or {"$raw": "response_body"} in a template). Non-streaming requests only; streams carry response_body_omitted: "stream".

Also accepts null.

Also accepts null.

stageobject | nulloptional
Variant 1 object
object

Values: "pre_call""post_call""on_complete"

Also accepts null.

stream_modeobject | nulloptional
Variant 1 object
object

post_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).

default: "skip"

Values: "skip"

Also accepts null.

Unknown fields are rejected in this object.

curl --request PATCH \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool' \
--header 'Authorization: Bearer <management-key>' \
--header 'If-Match: "r12-eu-pool-1a2b3c4d"'

Authentication, errors & retry rules Try it in the playground

PUT/controls/hooks/{resource_id}

Create or replace a hook

Creates (201) or replaces (200) the hook. Changing an existing one needs If-Match; If-None-Match: * creates only. Writes publish one policy revision. A PUT whose content equals the stored resource returns changed false and publishes nothing.

REQUIREShooks:write
Path, query & header parameters 5
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

dry_run (query)booleanoptional

Preview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.

default: false
If-Match (header)stringoptional

Resource ETag "r<revision>-<id>-<hash8>" from the latest read, or the collection ETag "r<revision>". Required to change or delete an existing resource (428 when missing, 412 when stale); an unrelated publish does not invalidate it.

Idempotency-Key (header)stringoptional

Unique per logical write. A retry with the same key and body returns the original result, including after a crash once the revision was published. Optional on PUT, PATCH and DELETE.

min length: 1max length: 200
If-None-Match (header)"*"optional

Create only: 412 when the resource exists with different content.

Full request body specification application/json

External hooks call your own HTTPS API on gateway traffic. A pre_call hook runs after Quilr's checks and before the model call and can allow, flag or block the request (observe mode logs without enforcing). A post_call hook runs on the final non-streaming response (after Quilr's response checks, before it is cached or returned) and can allow, flag or block it; streaming requests skip it. An on_complete hook exports finished requests in batches. You choose the fields sent, the method, URL, headers, auth and signing, the body shape and how the reply maps to a decision. Secrets are write-only.

all_appsbooleanoptional

Required (true) to leave scope empty.

descriptionstringoptional
max length: 500
enabledbooleanoptional
default: true
endpointobjectoptional

pre_call and post_call.

Object fields
authobjectoptional
Object fields
headerstringoptional
password_secretstringoptional
querystringoptional
secretstringoptional
typeobjectoptional
"none""bearer""basic""api_key"
username_secretstringoptional
bodyobjectoptional
Object fields
formatobjectoptional
"quilr""template""raw""none"
templateobjectoptional

JSON in your own schema.

headersarray<object>optional
Array item specification
namestringrequired
omit_if_emptybooleanoptional
valuestringrequired

Fixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.

Unknown fields are rejected in this object.

methodobjectoptional
"POST""PUT""PATCH"
on_failureobjectoptional
default: "allow"
"allow""block"
responseobjectoptional
Object fields
decisionobjectoptional
Object fields
block_valuesarray<object>optional
Array item specification
object
case_insensitivebooleanoptional
flag_valuesarray<object>optional
Array item specification
object
otherwiseobjectoptional
"allow""failure"
pathstringoptional
formatobjectoptional
"quilr""mapped"
labels_pathstringoptional
metadata_pathsobjectoptional
reason_pathstringoptional
status_mapobjectoptional
user_message_pathstringoptional
signingobjectoptional
Object fields
algorithmobjectoptional
"sha256""sha512"
encodingobjectoptional
"hex""base64"
formatobjectoptional

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

"quilr""body"
headerstringoptional
default: "X-Quilr-Signature"
prefixstringoptional
secretstringrequired
timestamp_headerstringoptional
timeout_msintegeroptional
min: 100max: 10000default: 1500
urlstringoptional
exportobjectoptional

on_complete only.

Object fields
authobjectoptional
Object fields
headerstringoptional
password_secretstringoptional
querystringoptional
secretstringoptional
typeobjectoptional
"none""bearer""basic""api_key"
username_secretstringoptional
batch_sizeintegeroptional
min: 1max: 1000default: 100
flush_msintegeroptional
min: 250max: 60000default: 5000
headersarray<object>optional
Array item specification
namestringrequired
omit_if_emptybooleanoptional
valuestringrequired

Fixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.

Unknown fields are rejected in this object.

max_batch_bytesintegeroptional
default: 4194304
recordobjectoptional
Object fields
formatobjectoptional
"quilr""template"
templateobjectoptional
signingobjectoptional
Object fields
algorithmobjectoptional
"sha256""sha512"
encodingobjectoptional
"hex""base64"
formatobjectoptional

quilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).

"quilr""body"
headerstringoptional
default: "X-Quilr-Signature"
prefixstringoptional
secretstringrequired
timestamp_headerstringoptional
urlstringoptional
matchobjectoptional
Object fields
api_familiesarray<object>optional
Array item specification
object

Values: "openai.chat_completions""openai.responses""anthropic.messages""gemini.generate_content""bedrock.converse""bedrock.invoke"

requested_modelsarray<string>optional
Array item specification
string
modeobjectoptional

pre_call and post_call only. observe logs decisions and never blocks.

"enforce""observe"
scopeobjectoptional
Object fields
app_tagsarray<string>optional
Array item specification
string
appsarray<string>optional
Array item specification
string
smart_groupsarray<string>optional
Array item specification
string
usersarray<string>optional
Array item specification
string
secretsmap<string, string>optional

Write-only. Reads return ******** ; sending ******** keeps the stored value.

Map value specification
string
sendobjectoptional
Object fields
bodyobjectoptional
"as_received""none"
excludearray<string>optional
Array item specification
string
includearray<string>optional
Array item specification
string
max_body_bytesintegeroptional
min: 1024max: 4194304
on_oversizeobjectoptional
"send_without_body""skip_hook""apply_on_failure"
presetobjectoptional
"minimal""standard""full_export""empty"
request_headersarray<string>optional
Array item specification
string
response_bodybooleanoptional

post_call (default true) and on_complete (default false) only: send the response the client receives, as delivered (quilr envelope key response_body, or {"$raw": "response_body"} in a template). Non-streaming requests only; streams carry response_body_omitted: "stream".

stageobjectrequired
"pre_call""post_call""on_complete"
stream_modeobjectoptional

post_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).

default: "skip"
"skip"

Unknown fields are rejected in this object.

curl --request PUT \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool' \
--header 'Authorization: Bearer <management-key>' \
--header 'If-Match: "r12-eu-pool-1a2b3c4d"' \
--header 'Content-Type: application/json' \
--data '{
"description": "ACME prompt policy check",
"endpoint": {
"headers": [
{
"name": "X-Api-Key",
"value": "{{secret.api_key}}"
},
{
"name": "X-Agent",
"omit_if_empty": true,
"value": "{{client.agent.name}}"
}
],
"on_failure": "allow",
"signing": {
"secret": "hmac_key"
},
"timeout_ms": 1500,
"url": "https://guard.example.com/v1/check?app={{app.name}}"
},
"match": {
"api_families": [
"openai.chat_completions",
"anthropic.messages"
]
},
"mode": "observe",
"scope": {
"apps": [
"support-bot"
]
},
"secrets": {
"api_key": "sk_live_example",
"hmac_key": "whsec_example"
},
"send": {
"preset": "standard",
"request_headers": [
"user-agent",
"x-team-id"
]
},
"stage": "pre_call"
}'

Authentication, errors & retry rules Try it in the playground

GET/controls/hooks/{resource_id}/stats

Read hook delivery stats

This server's view of one hook: whether its circuit breaker (shared by the server's processes) is open and, for on_complete hooks, the export queue (queued records and bytes, delivered, dropped, failed batches, last error and last success).

REQUIRESread
Path, query & header parameters 1
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

No request body.

curl --request GET \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool/stats' \
--header 'Authorization: Bearer <management-key>'

Authentication, errors & retry rules Try it in the playground

POST/controls/hooks/{resource_id}/test

Send a test call with a saved hook

Sends one sample call with the saved hook to your endpoint and returns the HTTP status, latency and the parsed decision (pre_call, post_call) or whether the batch was accepted (on_complete). post_call and on_complete samples use sample_response_body (or the family's sample). Limited to 10 previews and tests per minute.

REQUIREShooks:write
Path, query & header parameters 1
resource_id (path)stringrequired

Client-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).

Full request body specification application/json

api_familystringoptional
sample_bodyobjectoptional
sample_response_bodyobjectoptional
curl --request POST \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/controls/hooks/eu-pool/test' \
--header 'Authorization: Bearer <management-key>' \
--header 'Content-Type: application/json' \
--data '{
"api_family": "openai.chat_completions"
}'

Authentication, errors & retry rules Try it in the playground