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.
Where hooks run
POST /openai_compatible/v1/chat/completions
{ "model": "gpt-4.1", "messages": [...] }
app: support-bot user: dana@example.comHooks run on these client APIs, and you can narrow a hook to some of them with match.api_families:
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:writeto create, change, delete and test hooks;readto 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-MatchETags,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_callorpost_callhook inmode: "observe"logs its decisions without enforcing them. Switch toenforceonce the decisions look right. Saving a new enforcing hook returns the warninghook_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"]}
}
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 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 examplehttps://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:
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:
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
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:
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:
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"}
}
}
bodyis 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_omittedisnullwhen the body is included,"not_requested"withsend.body: "none", or"too_large"when it exceededsend.max_body_bytes.response_body(post_callandon_completewithsend.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_omittedisnullwhen the response body is included,"not_requested","too_large", or"stream"(streaming responses never carry a body).request_headersis present only whensend.request_headersis set.metadataholds the selected fields, nested by their dotted path. A field with no value isnull, never invented.
Choose the fields
send decides what a hook receives:
minimalsendsrequest_id,api_family,stream,user.email,app.name,model.requestedandmodel.target.standardadds the identity, client agent, Smart Group, app, provider and request fields, Quilr's findings (quilr.status,quilr.categories,quilr.actionand their response versions) andbudgets, and the outcome, usage, cost and latency fields at the stages that have them.full_exportsends every non-sensitive field available at the stage, includingclient.headersandfailover.emptysends 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
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"}
}
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"}
}
- A status listed in
status_mapdecides directly. - Otherwise a non-
2xxstatus is a failure. - Otherwise the value at
decision.pathdecides:blockif it equals ablock_valuesentry, elseflagif it equals aflag_valuesentry, elseotherwise(allow, orfailureto treat it as a failure). With nodecision, a2xxreply 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 (
blockoverflagoverallow); when several hooks block, the message of the hook with the lowest id is shown. - In
observemode 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,labelsandmetadata. Onon_complete, thehooksfield 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.
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
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}
}
}
]
}
- Delivery: any
2xxwithin 30 seconds delivers the batch. Each batch is tried up to 3 times with the sameIdempotency-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, andsend.response_body: trueto include the response body of non-streaming requests (response_body_omitted: "stream"for streams). GET /controls/hooks/{id}/statsshows the queue on the server that answers:healthy,queued,queued_bytes,delivered,dropped,failed_batches,last_errorandlast_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 leavingsecretsout 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 warninghook_secret_unused. - Destinations. Only
https://URLs to public addresses. Quilr hosts,localhostand 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 anexcludelist 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. Anon_completetest 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
Writes also return the usual control errors (412 version_conflict, 428 precondition_required and the others in Management API errors).
Limits
Endpoints
/controls/hooksList 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.
readPath, query & header parameters 7
limit (query)integeroptionalPage size.
cursor (query)stringoptionalnext_cursor of the previous page; bound to the revision (409 collection_changed when the policy changed).
source (query)objectoptionalOnly managed resources or only console rows.
"managed""console"app (query)stringoptionalRows 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)stringoptionalRows that can apply to this requested model. model is a deprecated alias.
mode (query)stringoptionalRows 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
/controls/hooks/field-catalogList 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.
readNo 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
/controls/hooks/previewPreview 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.
readFull request body specification application/json
api_familystringoptionalhookobjectrequiredExternal 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_appsbooleanoptionalRequired (true) to leave scope empty.
descriptionstringoptionalenabledbooleanoptionalendpointobjectoptionalpre_call and post_call.
Object fields
authobjectoptionalObject fields
headerstringoptionalpassword_secretstringoptionalquerystringoptionalsecretstringoptionaltypeobjectoptional"none""bearer""basic""api_key"username_secretstringoptionalbodyobjectoptionalObject fields
formatobjectoptional"quilr""template""raw""none"templateobjectoptionalJSON in your own schema.
headersarray<object>optionalArray item specification
namestringrequiredomit_if_emptybooleanoptionalvaluestringrequiredFixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.
Unknown fields are rejected in this object.
methodobjectoptional"POST""PUT""PATCH"on_failureobjectoptional"allow""block"responseobjectoptionalObject fields
decisionobjectoptionalObject fields
block_valuesarray<object>optionalArray item specification
objectcase_insensitivebooleanoptionalflag_valuesarray<object>optionalArray item specification
objectotherwiseobjectoptional"allow""failure"pathstringoptionalformatobjectoptional"quilr""mapped"labels_pathstringoptionalmetadata_pathsobjectoptionalreason_pathstringoptionalstatus_mapobjectoptionaluser_message_pathstringoptionalsigningobjectoptionalObject fields
algorithmobjectoptional"sha256""sha512"encodingobjectoptional"hex""base64"formatobjectoptionalquilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).
"quilr""body"headerstringoptionalprefixstringoptionalsecretstringrequiredtimestamp_headerstringoptionaltimeout_msintegeroptionalurlstringoptionalexportobjectoptionalon_complete only.
Object fields
authobjectoptionalObject fields
headerstringoptionalpassword_secretstringoptionalquerystringoptionalsecretstringoptionaltypeobjectoptional"none""bearer""basic""api_key"username_secretstringoptionalbatch_sizeintegeroptionalflush_msintegeroptionalheadersarray<object>optionalArray item specification
namestringrequiredomit_if_emptybooleanoptionalvaluestringrequiredFixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.
Unknown fields are rejected in this object.
max_batch_bytesintegeroptionalrecordobjectoptionalObject fields
formatobjectoptional"quilr""template"templateobjectoptionalsigningobjectoptionalObject fields
algorithmobjectoptional"sha256""sha512"encodingobjectoptional"hex""base64"formatobjectoptionalquilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).
"quilr""body"headerstringoptionalprefixstringoptionalsecretstringrequiredtimestamp_headerstringoptionalurlstringoptionalmatchobjectoptionalObject fields
api_familiesarray<object>optionalArray item specification
objectValues: "openai.chat_completions""openai.responses""anthropic.messages""gemini.generate_content""bedrock.converse""bedrock.invoke"
requested_modelsarray<string>optionalArray item specification
stringmodeobjectoptionalpre_call and post_call only. observe logs decisions and never blocks.
"enforce""observe"scopeobjectoptionalObject fields
app_tagsarray<string>optionalArray item specification
stringappsarray<string>optionalArray item specification
stringsmart_groupsarray<string>optionalArray item specification
stringusersarray<string>optionalArray item specification
stringsecretsmap<string, string>optionalWrite-only. Reads return ******** ; sending ******** keeps the stored value.
Map value specification
stringsendobjectoptionalObject fields
bodyobjectoptional"as_received""none"excludearray<string>optionalArray item specification
stringincludearray<string>optionalArray item specification
stringmax_body_bytesintegeroptionalon_oversizeobjectoptional"send_without_body""skip_hook""apply_on_failure"presetobjectoptional"minimal""standard""full_export""empty"request_headersarray<string>optionalArray item specification
stringresponse_bodybooleanoptionalpost_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_modeobjectoptionalpost_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).
"skip"Unknown fields are rejected in this object.
sample_bodyobjectoptionalClient body to use instead of the family's sample.
sample_response_bodyobjectoptionalResponse 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
/controls/hooks/{resource_id}Delete a hook
Removes the hook; refused while other controls reference it. If-Match is required.
hooks:writePath, query & header parameters 4
resource_id (path)stringrequiredClient-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).
dry_run (query)booleanoptionalPreview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.
If-Match (header)stringrequiredResource 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)stringoptionalUnique 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.
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
/controls/hooks/{resource_id}Read a hook
One hook and its ETag.
readPath, query & header parameters 1
resource_id (path)stringrequiredClient-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
/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.
hooks:writePath, query & header parameters 4
resource_id (path)stringrequiredClient-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).
dry_run (query)booleanoptionalPreview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.
If-Match (header)stringrequiredResource 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)stringoptionalUnique 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.
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 | nulloptionalVariant 1 boolean
booleanRequired (true) to leave scope empty.
Also accepts null.
descriptionstring | nulloptionalVariant 1 string
stringAlso accepts null.
enabledboolean | nulloptionalVariant 1 boolean
booleanAlso accepts null.
endpointobject | nulloptionalVariant 1 object
authobject | nulloptionalVariant 1 object
headerstring | nulloptionalVariant 1 string
stringAlso accepts null.
password_secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
querystring | nulloptionalVariant 1 string
stringAlso accepts null.
secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
typeobject | nulloptionalVariant 1 object
objectValues: "none""bearer""basic""api_key"
Also accepts null.
username_secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
bodyobject | nulloptionalVariant 1 object
formatobject | nulloptionalVariant 1 object
objectValues: "quilr""template""raw""none"
Also accepts null.
templateobject | nulloptionalVariant 1 object
objectJSON in your own schema.
Also accepts null.
Also accepts null.
headersarray<object> | nulloptionalVariant 1 array<object>
array<object>Also accepts null.
methodobject | nulloptionalVariant 1 object
objectValues: "POST""PUT""PATCH"
Also accepts null.
on_failureobject | nulloptionalVariant 1 object
objectValues: "allow""block"
Also accepts null.
responseobject | nulloptionalVariant 1 object
decisionobject | nulloptionalVariant 1 object
block_valuesarray<object> | nulloptionalVariant 1 array<object>
array<object>Also accepts null.
case_insensitiveboolean | nulloptionalVariant 1 boolean
booleanAlso accepts null.
flag_valuesarray<object> | nulloptionalVariant 1 array<object>
array<object>Also accepts null.
otherwiseobject | nulloptionalVariant 1 object
objectValues: "allow""failure"
Also accepts null.
pathstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
formatobject | nulloptionalVariant 1 object
objectValues: "quilr""mapped"
Also accepts null.
labels_pathstring | nulloptionalVariant 1 string
stringAlso accepts null.
metadata_pathsobject | nulloptionalVariant 1 object
objectAlso accepts null.
reason_pathstring | nulloptionalVariant 1 string
stringAlso accepts null.
status_mapobject | nulloptionalVariant 1 object
objectAlso accepts null.
user_message_pathstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
signingobject | nulloptionalVariant 1 object
algorithmobject | nulloptionalVariant 1 object
objectValues: "sha256""sha512"
Also accepts null.
encodingobject | nulloptionalVariant 1 object
objectValues: "hex""base64"
Also accepts null.
formatobject | nulloptionalVariant 1 object
objectquilr: 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 | nulloptionalVariant 1 string
stringAlso accepts null.
prefixstring | nulloptionalVariant 1 string
stringAlso accepts null.
secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
timestamp_headerstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
timeout_msinteger | nulloptionalVariant 1 integer
integerAlso accepts null.
urlstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
exportobject | nulloptionalVariant 1 object
authobject | nulloptionalVariant 1 object
headerstring | nulloptionalVariant 1 string
stringAlso accepts null.
password_secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
querystring | nulloptionalVariant 1 string
stringAlso accepts null.
secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
typeobject | nulloptionalVariant 1 object
objectValues: "none""bearer""basic""api_key"
Also accepts null.
username_secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
batch_sizeinteger | nulloptionalVariant 1 integer
integerAlso accepts null.
flush_msinteger | nulloptionalVariant 1 integer
integerAlso accepts null.
headersarray<object> | nulloptionalVariant 1 array<object>
array<object>Also accepts null.
max_batch_bytesinteger | nulloptionalVariant 1 integer
integerAlso accepts null.
recordobject | nulloptionalVariant 1 object
formatobject | nulloptionalVariant 1 object
objectValues: "quilr""template"
Also accepts null.
templateobject | nulloptionalVariant 1 object
objectAlso accepts null.
Also accepts null.
signingobject | nulloptionalVariant 1 object
algorithmobject | nulloptionalVariant 1 object
objectValues: "sha256""sha512"
Also accepts null.
encodingobject | nulloptionalVariant 1 object
objectValues: "hex""base64"
Also accepts null.
formatobject | nulloptionalVariant 1 object
objectquilr: 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 | nulloptionalVariant 1 string
stringAlso accepts null.
prefixstring | nulloptionalVariant 1 string
stringAlso accepts null.
secretstring | nulloptionalVariant 1 string
stringAlso accepts null.
timestamp_headerstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
urlstring | nulloptionalVariant 1 string
stringAlso accepts null.
Also accepts null.
matchobject | nulloptionalVariant 1 object
api_familiesarray<object> | nulloptionalVariant 1 array<object>
array<object>Also accepts null.
requested_modelsarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
Also accepts null.
modeobject | nulloptionalVariant 1 object
objectpre_call and post_call only. observe logs decisions and never blocks.
Values: "enforce""observe"
Also accepts null.
scopeobject | nulloptionalVariant 1 object
app_tagsarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
appsarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
smart_groupsarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
usersarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
Also accepts null.
secretsmap<string, string> | nulloptionalVariant 1 map<string, string>
map<string, string>Write-only. Reads return ******** ; sending ******** keeps the stored value.
Also accepts null.
sendobject | nulloptionalVariant 1 object
bodyobject | nulloptionalVariant 1 object
objectValues: "as_received""none"
Also accepts null.
excludearray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
includearray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
max_body_bytesinteger | nulloptionalVariant 1 integer
integerAlso accepts null.
on_oversizeobject | nulloptionalVariant 1 object
objectValues: "send_without_body""skip_hook""apply_on_failure"
Also accepts null.
presetobject | nulloptionalVariant 1 object
objectValues: "minimal""standard""full_export""empty"
Also accepts null.
request_headersarray<string> | nulloptionalVariant 1 array<string>
array<string>Also accepts null.
response_bodyboolean | nulloptionalVariant 1 boolean
booleanpost_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 | nulloptionalVariant 1 object
objectValues: "pre_call""post_call""on_complete"
Also accepts null.
stream_modeobject | nulloptionalVariant 1 object
objectpost_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).
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
/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.
hooks:writePath, query & header parameters 5
resource_id (path)stringrequiredClient-chosen control ID, ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ (console rules read as console:<policy>:<rule_index>).
dry_run (query)booleanoptionalPreview: spec and policy diff, diagnostics, warnings, would_publish and impact; nothing is saved.
If-Match (header)stringoptionalResource 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)stringoptionalUnique 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.
If-None-Match (header)"*"optionalCreate 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_appsbooleanoptionalRequired (true) to leave scope empty.
descriptionstringoptionalenabledbooleanoptionalendpointobjectoptionalpre_call and post_call.
Object fields
authobjectoptionalObject fields
headerstringoptionalpassword_secretstringoptionalquerystringoptionalsecretstringoptionaltypeobjectoptional"none""bearer""basic""api_key"username_secretstringoptionalbodyobjectoptionalObject fields
formatobjectoptional"quilr""template""raw""none"templateobjectoptionalJSON in your own schema.
headersarray<object>optionalArray item specification
namestringrequiredomit_if_emptybooleanoptionalvaluestringrequiredFixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.
Unknown fields are rejected in this object.
methodobjectoptional"POST""PUT""PATCH"on_failureobjectoptional"allow""block"responseobjectoptionalObject fields
decisionobjectoptionalObject fields
block_valuesarray<object>optionalArray item specification
objectcase_insensitivebooleanoptionalflag_valuesarray<object>optionalArray item specification
objectotherwiseobjectoptional"allow""failure"pathstringoptionalformatobjectoptional"quilr""mapped"labels_pathstringoptionalmetadata_pathsobjectoptionalreason_pathstringoptionalstatus_mapobjectoptionaluser_message_pathstringoptionalsigningobjectoptionalObject fields
algorithmobjectoptional"sha256""sha512"encodingobjectoptional"hex""base64"formatobjectoptionalquilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).
"quilr""body"headerstringoptionalprefixstringoptionalsecretstringrequiredtimestamp_headerstringoptionaltimeout_msintegeroptionalurlstringoptionalexportobjectoptionalon_complete only.
Object fields
authobjectoptionalObject fields
headerstringoptionalpassword_secretstringoptionalquerystringoptionalsecretstringoptionaltypeobjectoptional"none""bearer""basic""api_key"username_secretstringoptionalbatch_sizeintegeroptionalflush_msintegeroptionalheadersarray<object>optionalArray item specification
namestringrequiredomit_if_emptybooleanoptionalvaluestringrequiredFixed text mixed with {{field}}, {{secret.name}} or {{request_header.name}} placeholders.
Unknown fields are rejected in this object.
max_batch_bytesintegeroptionalrecordobjectoptionalObject fields
formatobjectoptional"quilr""template"templateobjectoptionalsigningobjectoptionalObject fields
algorithmobjectoptional"sha256""sha512"encodingobjectoptional"hex""base64"formatobjectoptionalquilr: t=<unix>,v1=<hmac of "t.body">; body: hmac of the body (or "t.body" with timestamp_header).
"quilr""body"headerstringoptionalprefixstringoptionalsecretstringrequiredtimestamp_headerstringoptionalurlstringoptionalmatchobjectoptionalObject fields
api_familiesarray<object>optionalArray item specification
objectValues: "openai.chat_completions""openai.responses""anthropic.messages""gemini.generate_content""bedrock.converse""bedrock.invoke"
requested_modelsarray<string>optionalArray item specification
stringmodeobjectoptionalpre_call and post_call only. observe logs decisions and never blocks.
"enforce""observe"scopeobjectoptionalObject fields
app_tagsarray<string>optionalArray item specification
stringappsarray<string>optionalArray item specification
stringsmart_groupsarray<string>optionalArray item specification
stringusersarray<string>optionalArray item specification
stringsecretsmap<string, string>optionalWrite-only. Reads return ******** ; sending ******** keeps the stored value.
Map value specification
stringsendobjectoptionalObject fields
bodyobjectoptional"as_received""none"excludearray<string>optionalArray item specification
stringincludearray<string>optionalArray item specification
stringmax_body_bytesintegeroptionalon_oversizeobjectoptional"send_without_body""skip_hook""apply_on_failure"presetobjectoptional"minimal""standard""full_export""empty"request_headersarray<string>optionalArray item specification
stringresponse_bodybooleanoptionalpost_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_modeobjectoptionalpost_call only. Streaming requests never run post_call hooks (buffering a stream is a later phase).
"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
/controls/hooks/{resource_id}/statsRead 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).
readPath, query & header parameters 1
resource_id (path)stringrequiredClient-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
/controls/hooks/{resource_id}/testSend 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.
hooks:writePath, query & header parameters 1
resource_id (path)stringrequiredClient-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_familystringoptionalsample_bodyobjectoptionalsample_response_bodyobjectoptionalcurl --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
Related
- Management API - authentication, scopes, errors and retry rules.
- Routing and failover API - the write, ETag and dry-run rules shared by every control.
- Security guardrails - Quilr's own request and response checks, which run before your hooks.
- Management API playground - list, preview, create and test hooks from the browser.