Build a custom webhook
Last validated:
Aperture can send real-time event data to external services through webhooks to feed LLM usage data into your own audit logs, cost dashboards, security tools, or policy engines. For each request that matches a grant (an access rule that controls who can use which models), Aperture POSTs a JSON payload containing the metadata and data types you select.
Prerequisites
Before you begin, ensure you have the following:
- An Aperture instance with at least one provider configured.
- Admin access to the Aperture configuration.
- An HTTP or HTTPS endpoint that accepts POST requests with JSON payloads.
Define a hook endpoint
Open the Settings page in the Aperture dashboard and add a hooks section to your configuration. Each hook has a unique key and specifies the endpoint URL:
"hooks": {
"my-webhook": {
"url": "https://example.com/aperture-events",
"apikey": "<api-key>"
}
}
If your endpoint uses a non-Bearer authentication scheme, set the authorization field:
"hooks": {
"my-webhook": {
"url": "https://example.com/aperture-events",
"apikey": "<api-key>",
"authorization": "x-api-key",
"timeout": "10s"
}
}
The authorization field supports bearer (default), x-api-key, and x-goog-api-key. The timeout field accepts Go duration strings such as 5s, 30s, or 1m and defaults to 5s.
A hook defined in the hooks section has no effect until a grant references it.
Trigger the hook from a grant
Add a send_hooks entry to a capability (a set of permissions such as allowed models) in your grants section. This controls which requests trigger the hook and what data Aperture includes in the payload:
"grants": [
{
"src": ["*"],
"app": {
"tailscale.com/cap/aperture": [
{
"models": "**",
"send_hooks": [
{
"name": "my-webhook",
"events": ["entire_request"],
"send": ["estimated_cost"]
}
]
}
]
}
}
]
The grant examples on this page use Aperture configuration syntax, where the dst field is not required because the destination is the Aperture device itself. If you define grants in the tailnet policy file instead, you must include a dst key specifying the Aperture device (for example, "dst": ["tag:aperture"]). Omitting dst in a tailnet policy file grant causes the grant to silently have no effect.
Select hook events
The events array specifies when Aperture calls the hook:
| Event | Description |
|---|---|
pre_request | Fires before the provider call. Synchronous: Aperture waits for the hook response. The hook can allow, block, or modify the request. |
entire_request | Fires for every completed request. |
tool_call_entire_request | Fires once after the response completes if any message in the response contained tool calls. |
The pre_request event is a synchronous guardrail. Unlike entire_request and tool_call_entire_request, which fire asynchronously after a request completes, pre_request blocks until the hook responds and can allow, block, or modify the request. For details, refer to guardrails.
Select data types
The send array specifies which data to include in the POST payload:
| Type | Description |
|---|---|
tools | Array of tool calls extracted from the response. |
request_body | The original request body sent to the LLM. |
user_message | The user's message from the request. |
response_body | The reconstructed response body JSON. |
raw_responses | Array of raw SSE messages (for streaming) or single response object. |
estimated_cost | Dollar cost estimate, pricing basis, and token usage breakdown. |
grants | Non-Aperture app capabilities from the user's grants. |
quotas | Current state of all quota buckets that applied to this request. |
Review the payload format
Every hook call includes a metadata object with request context, regardless of what you specify in send:
{
"metadata": {
"login_name": "alice@example.com",
"user_agent": "curl/8.0",
"url": "/v1/chat/completions",
"model": "gpt-5.5",
"provider": "openai",
"tailnet_name": "example.com",
"stable_node_id": "n12345",
"request_id": "abc123",
"session_id": "oacc_1a2b3c4d5e6f7890"
}
}
When you include data types in the send array, Aperture adds them to the payload alongside metadata. For example, with "send": ["tools", "estimated_cost"]:
{
"metadata": {
"login_name": "alice@example.com",
"...": "..."
},
"estimated_cost": {
"dollars": 0.0342,
"cost_basis": "anthropic/claude-sonnet-4-6",
"usage": {
"input_tokens": 1500,
"output_tokens": 800,
"cached_tokens": 200,
"reasoning_tokens": 0
}
},
"tool_calls": [...]
}
Verify the webhook
After saving the configuration, send a test LLM request through Aperture and check that your endpoint receives the POST payload. If the webhook does not fire:
- Confirm the hook name in
send_hooksmatches the key in thehookssection. - Confirm the grant's
srcandmodelspatterns match your test request. - Check the Aperture server logs for timeout or connection errors to the hook URL.
To temporarily disable a hook without removing it from the configuration, set "disabled": true in the hook definition.
Next steps
- Export usage data to S3 for long-term storage of session logs.
- Integrate Cerbos with Aperture, Integrate Highflame with Aperture, or Integrate Oso with Aperture for policy engine integrations that use hooks.
- Refer to the hooks configuration reference for the complete field reference and additional examples.