Webhooks#
External systems can trigger Prism Agent by POSTing to a named webhook endpoint. Where the heartbeat polls on a timer and scheduled tasks run on a calendar, a webhook is push: Alertmanager fires an alert, PagerDuty opens an incident, GitHub merges a PR, and the agent reacts immediately.
Each webhook is a subscription: a named route with its own endpoint URL, an optional prompt template, a delivery target, and a way for the sender to prove itself. When an event arrives, the agent reasons over the payload — or, in deliver-only mode, the rendered template is posted directly with no LLM call — and the result is delivered to your connected channels.
Enable the receiver#
Webhooks are off by default. To enable them, open the web UI Webhooks page and turn on Webhook receiver. That opens the webhook port, with no restart. Turning it off closes the port and keeps your subscriptions. The setting survives restarts.
If enabling reports that the port is in use, set a free WEBHOOK_PORT, restart the runtime, and enable the receiver again.
Create a subscription#
To create a subscription, click New subscription on the Webhooks page and set its fields:
| Field | Meaning |
|---|---|
Name |
Lowercase slug ([a-z0-9][a-z0-9_-]*); becomes the endpoint path /agents/webhooks/<name>. Spaces are normalized to hyphens. |
Description |
What this webhook does (optional, shown in the list). |
Events |
Comma-separated event-type allowlist; empty accepts all. See event filtering. |
Deliver to |
Where results go: All channels (default), Chat, Slack, Microsoft Teams, or None. See delivery targets. |
Slack channels |
Shown when Deliver to is Slack: the linked Slack channels to post to. With none checked, the message goes to your DM or the main channel. See delivery targets. |
Deliver only |
Skip the agent: the rendered prompt template is posted directly as the message. Zero LLM cost — useful for plain notifications. |
Prompt |
Instructions for the agent when this webhook fires (optional), or the message in deliver-only mode. Supports payload substitution — see prompt templates. |
Scheme |
How the sender proves itself: Shared token in a header (default), HMAC signature of the body, or None. See Authentication. |
Each row in the subscriptions list shows the endpoint path with a button that copies its full URL, the authentication scheme (Unverified for None), an Active switch, an Edit action, and a delete action. Edit opens the same form; the name is the endpoint path and stays fixed, so create a new subscription for a new path. Changes apply immediately.
In the terminal UI, /webhooks pauses and resumes subscriptions and edits their description, events, delivery, Slack channels, and prompt.
The endpoint#
Senders POST to the webhook port, 3004, which is separate from the web UI and its API on 3003. WEBHOOK_PORT moves it to another port, which must differ from PORT. In Docker, publish it with -p 3004:3004.
Expose the webhook port through your ingress or tunnel. When the runtime knows its public webhook address, Copy endpoint URL copies it. Otherwise, combine the endpoint path with your public ingress or tunnel URL.
POST http://<PRISM-HOST>:3004/agents/webhooks/<NAME>
Content-Type: application/json
The body must be a JSON object. Responses (event is the resolved event type — see event filtering):
| Status | Meaning |
|---|---|
202 Accepted |
Agent run dispatched: {"status":"accepted","event":"<type>","runId":"..."}. The response is delivered to your channels when the run finishes. |
200 OK |
Deliver-only message posted: {"status":"delivered","event":"<type>"} — or the event was filtered out by the Events list: {"status":"ignored","event":"<type>"}. |
400 |
Body was not a JSON object. |
401 |
Check the subscription name, its Active switch, and the sender credentials. See Authentication. |
429 |
This subscription's run queue is full — retry later. |
Agent runs are asynchronous by design — webhook senders like Alertmanager time out quickly and retry on non-2xx, so the endpoint doesn't hold the connection open while the agent works. The agent's response arrives in the conversation (prefixed **Webhook: <name>**) on the subscription's delivery target. If the run fails outright, a brief failure notice is delivered there instead. Every accepted event is also listed on the Activity page with its result.
Runs are serialized per subscription: concurrent deliveries are accepted (202) and queued, then executed one at a time in arrival order — so rapid distinct events are never dropped, even from senders that don't retry (GitHub never redelivers automatically). The queue is capped at 10 pending; beyond that, deliveries answer 429 as a backstop against pathological floods. Deliver-only requests are not queued or limited — they cost no LLM tokens.
Authentication#
Expose the webhook port to your senders and keep port 3003 private: anyone who reaches 3003 controls the agent through the web UI and its API. Each subscription can require its sender to prove itself, so that a party who reaches the endpoint and guesses its name cannot start agent runs with a payload they control.
Verification is generic. You name the header your sender uses and choose how the runtime compares the credential, so any sender is a matter of configuration.
| Scheme | What must match |
|---|---|
Shared token in a header (default) |
The chosen header carries the secret verbatim, after an optional prefix. |
HMAC signature of the body |
The chosen header carries HMAC(secret, raw request body) in the chosen algorithm and encoding, after an optional prefix. |
None |
Any request is accepted. The list shows Unverified. |
The authentication fields appear once the scheme is not None:
| Field | Meaning |
|---|---|
Header |
Header that carries the credential, for example X-Hub-Signature-256 or Authorization. Matched case-insensitively. Required. |
Prefix |
Optional literal removed from the header value before the comparison, for example sha256=, v1=, or Bearer. A value that arrives without the prefix is accepted too. |
Secret |
The shared secret or HMAC key. Required. Write-only: the web UI shows whether a secret is set, never its value. |
Algorithm / Encoding |
HMAC only: sha256 (default), sha1, or sha512, encoded as hex (default) or base64. |
Signed URL |
HMAC only, optional: for a sender that signs the body followed by its webhook URL. Enter the exact URL configured in the sender. |
An HMAC header can carry several comma-separated signatures, and the request passes when any one matches. That keeps senders working while they rotate a secret. To rotate the secret on the Prism Agent side, click Edit on the subscription, enter the new secret, and click Save. An empty Secret field keeps the current one.
Recipes for common senders:
| Sender | Scheme | Header | Prefix | Notes |
|---|---|---|---|---|
| GitHub | HMAC | X-Hub-Signature-256 |
sha256= |
Put the same value in GitHub's webhook Secret field. |
| GitLab | Shared token | X-Gitlab-Token |
— | GitLab's Secret token field. GitLab sends the token as it is. |
| PagerDuty | HMAC | X-PagerDuty-Signature |
v1= |
Use the signing secret that PagerDuty shows when you create the v3 webhook subscription. |
| Alertmanager | Shared token | Authorization |
Bearer |
Configure http_config.authorization on the receiver; see Connect common senders. |
| Trello | HMAC | X-Trello-Webhook |
— | Algorithm sha1, Encoding base64, Signed URL set to the webhook's callbackURL, and the API secret from your Trello Power-Up admin page. Enable the receiver before you create the Trello webhook, because Trello checks the URL. Leave Events empty. |
| Custom scripts | Shared token | any header | — | The simplest option for internal senders. |
Delivery targets and storage#
Each subscription's Deliver to setting controls both where the message is sent and where it's recorded:
All channels(default) — broadcast, exactly like a heartbeat alert: the message appears in web chat and is sent to every linked channel integration. On Slack and Microsoft Teams that means your DM (personal mode) or the agent's main channel (team mode — the one flagged on the Channels page, else the oldest linked channel), not every linked channel.Chat— web chat only; nothing is sent to Slack or Teams.Slack— Slack only. The message goes to each channel checked inSlack channelsthat is still linked, or, with none checked, to your DM (personal mode) or the main channel (team mode). It's kept out of web chat: it's stored in each Slack destination's own conversation thread, which the web Recents list and CLI resume picker don't show. If none of those destinations is linked, the message falls back to the web thread (and the runtime logs a warning) so a triggered event is never silently lost.Microsoft Teams— Teams only, in the Teams personal chat or main channel, the same way asSlackwith no channels checked.None— the agent runs and posts no message, even when the run fails. Use it when the agent's work lands somewhere else, such as a review on GitHub.
The agent posts a reply only when it has something to report. Put the condition in the prompt, for example "Review the PR. Message me only if the review finds a blocking problem." A prompt that asks for a message on every event, or an empty prompt, gets one every time. A failed run is always posted, except with None.
Prompt templates#
An empty Prompt sends the agent the raw payload as pretty-printed JSON with an event and route lead-in — the agent reasons over the full event. Alertmanager and PagerDuty payloads work well this way with no template at all.
A non-empty Prompt is rendered against the payload first:
{field.path}— dot-notation access into the payload, for example{commonLabels.alertname}or{pull_request.title}. Unresolvable keys are left as-is.{__raw__}— the entire payload as indented JSON (truncated to about 4000 characters).
Example, for Alertmanager:
An alert group just fired ({status}):
{__raw__}
Triage it: check the affected pods, look for an obvious cause, and summarize
what you found. Only suggest a remediation if you're confident.
In deliver-only mode the rendered template is the message — for example Alert: {commonLabels.alertname} is {status} posts "Alert: HighMemory is firing" with no agent involvement.
Event filtering#
The Events field matches the incoming event's type, resolved in this order:
| Source | Where the type comes from | Example values |
|---|---|---|
| GitHub | X-GitHub-Event header |
push, pull_request, issues |
| GitLab | X-GitLab-Event header |
Merge Request Hook |
| Generic | top-level event_type field in the payload |
whatever the sender sets |
| Anything else | — | unknown |
A non-empty Events list accepts only matching types; everything else answers 200 {"status":"ignored"} without running the agent.
Note
Leave Events empty for Alertmanager and PagerDuty. Neither sends a GitHub-style event header or a top-level event_type field, so their deliveries resolve to unknown — an Events list like firing,resolved would silently filter out every event. Put that intent in the Prompt instead ("only act on firing alerts; ignore resolved notifications").
Connect common senders#
Alertmanager — add a webhook receiver to alertmanager.yml. This one matches a subscription with the scheme Shared token in a header, Header Authorization, and Prefix Bearer:
receivers:
- name: prism
webhook_configs:
- url: http://<PRISM-HOST>:3004/agents/webhooks/alertmanager
send_resolved: true
http_config:
authorization:
type: Bearer
credentials_file: /etc/alertmanager/prism-webhook-secret
route:
receiver: prism
Alertmanager groups alerts and sends one JSON payload per group (with status, commonLabels, and an alerts array) — the raw-payload default prompt handles it well. Alertmanager retries on non-2xx and re-notifies on repeat_interval; per-subscription run serialization keeps that from fanning out into concurrent agent runs, without dropping distinct alert groups.
PagerDuty — create a v3 webhook subscription (Integrations → Developer Tools → Webhooks) pointing at http://<PRISM-HOST>:3004/agents/webhooks/pagerduty. Events arrive as {"event": {"event_type": "incident.triggered", ...}}. Copy the signing secret that PagerDuty shows into the subscription's Secret field, with the scheme HMAC, Header X-PagerDuty-Signature, and Prefix v1=.
GitHub — add a repository webhook (Settings → Webhooks) with content type application/json pointing at http://<PRISM-HOST>:3004/agents/webhooks/github. Put the same string in GitHub's Secret field and in the subscription's Secret field, with the scheme HMAC, Header X-Hub-Signature-256, and Prefix sha256=. Use the Events field (for example pull_request,issues) to pick the event types you care about.
Anything else — any system that can POST JSON works. With a shared token (Header Authorization, Prefix Bearer):
curl -X POST http://<PRISM-HOST>:3004/agents/webhooks/my-hook \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PRISM_WEBHOOK_SECRET" \
-d '{"event_type": "deploy.finished", "service": "api", "version": "1.4.2"}'
With an HMAC signature (Header X-Hub-Signature-256, Prefix sha256=), sign the exact bytes you send:
BODY='{"event_type":"deploy.finished","service":"api"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$PRISM_WEBHOOK_SECRET" | awk '{print $2}')
curl -X POST http://<PRISM-HOST>:3004/agents/webhooks/my-hook \
-H 'Content-Type: application/json' \
-H "X-Hub-Signature-256: sha256=$SIG" \
-d "$BODY"