Skip to content

Alert webhook reference

What Vaicoa sends to an alert target. To set one up, see Send alerts where you work.

Targets are set up in the portal under Alerts → Delivery targets, by an admin. Vaicoa Pro delivers to one target, Vaicoa Team to up to five.

FieldTargetNotes
Namewebhook, PagerDutyup to 60 characters, unique in the team. Shown on the page, in alerts about the target and in the audit log; empty, the address is shown instead
Send alerts to this URLwebhookhttps:// only, up to 500 characters, no user name or password. Stored encrypted and never shown again: the portal shows only the scheme and host, because for many receivers the path is the credential. To change it, edit the target and paste the full URL again
Which alertswebhook, PagerDutyWarnings and critical alerts or Critical alerts only, per target. Push notifications are not affected
What we sendwebhookStandard payload or Coding-agent brief (see below)
Authentication header (optional), Header valuewebhooka header your endpoint expects, for example Authorization. The value is stored encrypted and never shown again. When editing, the field is Replace the header value: empty keeps the stored value. Clearing the name deletes the value. Send a test event sends the header too
Service regionPagerDutyUnited States or European Union
Integration routing keyPagerDutyfrom the Events API v2 integration you create in PagerDuty. Stored encrypted and never shown again. Vaicoa cannot issue a new one, because PagerDuty does. When editing, the field is Replace the routing key: paste a new key, or leave it empty to keep the stored one

Different targets can receive different alerts: for example PagerDuty for the on-call rotation, all warnings to an engineering channel and critical alerts only to an agent endpoint. Remove stops a target; adding it again later does not resend earlier alerts. A plan change never deletes a target: if the new plan allows fewer, the oldest keep delivering and the others are kept, paused, until the plan allows them again or you remove an older one.

A webhook target receives one POST request each time an alert starts or is resolved, and when you use Send a test event.

HeaderContent
Content-Typeapplication/json
User-AgentCloudViewer-Webhook/1
X-CloudViewer-Eventthe event value, so you can route without parsing the body
X-CloudViewer-Deliveryan ID that stays the same across retries; store it if you need idempotency. It equals the body’s id
X-CloudViewer-Signaturesha256=<hex>, an HMAC-SHA256 of the raw body
your own headeronly if you set Authentication header (optional) on the target

Your own header cannot be one Vaicoa sets: Content-Type, Content-Length, User-Agent, Host, Transfer-Encoding or any name starting with X-CloudViewer-.

The body is UTF-8 JSON, sent compact (no whitespace) with its keys sorted. The example below is formatted for reading.

Every event and every receiver gets the same JSON structure:

{
"version": 1,
"event": "alert.firing",
"id": "9f2c…",
"sent_at": "2026-08-16T09:12:33Z",
"team": { "id": 42, "name": "Ops", "slug": "ops" },
"alert": {
"fingerprint": "a1b2c3…",
"rule": "disk_space_critical",
"severity": "critical",
"status": "firing",
"summary": "Disk almost full",
"description": "Disk 96% full on web-1 (/var)",
"started_at": "2026-08-16T09:11:02Z",
"resolved_at": null
},
"resource": { "id": "srv_1", "project_id": 10, "server_id": 7 },
"links": {
"app": "cloudviewer://alert/a1b2c3…?resource=srv_1&project=10&team=42",
"portal": "https://account.vaicoa.com/a/ops/alerts/"
}
}
FieldMeaning
versionthe schema version, 1. Added fields do not change it; it changes only for a change a receiver could break on
eventalert.firing, alert.resolved or test
idthe delivery ID, the same as X-CloudViewer-Delivery
sent_atwhen this request was sent, UTC. It is inside the signed body
teamthe team’s numeric ID, name and slug
alert.fingerprintthe alert’s identity. With started_at it identifies one alert episode
alert.rulethe rule key, for example disk_space_critical. See Alert rules
alert.severitywarning or critical
alert.statusfiring or resolved
alert.summary, alert.descriptionthe rule’s text for this alert
alert.started_atwhen the alert started, UTC
alert.resolved_atnull unless the event is alert.resolved
resource.idthe resource the alert is on, or an empty string
resource.project_id, resource.server_idthe connected project and monitored server, or null
links.appthe same deep link the push notification uses. Opening it on a phone shows the alert in the app, where you can mute it
links.portalthe team’s Alerts page in the portal

An alert that starts and resolves before it was delivered is sent once, as alert.resolved, with both timestamps.

The test event has the same shape: alert.rule is webhook_test, alert.severity is warning, alert.summary is a fixed test text, resource.id and links.app are empty, and project_id and server_id are null. Its id is based on the time, so it never collides with a real alert.

  • One request per target per event, as soon as the alert changes: no grouping and no delay. Each target gets its own copy, so one target being down does not affect the others.
  • Warnings and critical alerts only. Informational alerts stay in the app. A target set to Critical alerts only receives no warnings. Muted alerts are never sent.
  • Accepted means any 2xx response. The response body is read up to 2 KiB and discarded.
  • Timeout: 5 seconds for the connection and the response.
  • Redirects are not followed. A 3xx response counts as a failure.
  • Retry: once a minute. A failed delivery is tried again on the next minute, with the same delivery ID, until it succeeds or the target is switched off. A change older than six hours is no longer sent. After a failure, the remaining events for that target wait for the next minute.
  • Switched off after 20 failed attempts in a row, about twenty minutes. A successful delivery resets the count. Switch it back on on the Alerts page, or saving the target, turns it on again.
  • At most 20 events per team per minute. The rest follow in the next minute.
  • Public https addresses only. A URL must start with https:// and must not contain a user name or password. A URL that resolves to a private or internal address is refused when you save it and again before every request.
  • A new target does not receive older alerts. Delivery starts with the alerts that change after you add it.

The secret is shown once: when the target is created, and each time you use Issue a new signing secret. Editing a target keeps its secret. Compute the HMAC over the raw request body, before parsing the JSON, and compare in constant time:

import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

sent_at is part of the signed body, so you can also reject requests older than, for example, five minutes.

A webhook target can send the coding-agent brief instead of the standard payload: choose Coding-agent brief under What we send. The request is the same object with the same signature and version, plus one agent object:

"agent": {
"preamble": "You are being handed a production alert from …",
"brief_format": "markdown",
"brief": "# Incident brief — disk_space_critical on srv_1\n\n…"
}
FieldMeaning
preambleyour team’s Notes for alert hand-offs from Agent Hand-off Notes in the portal, or Vaicoa’s default text when the team has none
brief_formatmarkdown
briefa short incident brief in Markdown: a title, an Alert section (rule, severity, status, times, summary, description cut to 500 characters), a Resource section and a Links section. A section with nothing in it is left out, not filled with “none”

The brief is written only from what the standard object contains (the alert, the resource, the links), so it discloses nothing extra. It is shorter than the brief the app produces: recent changes, metric trends and cost information are only available in the app, and the webhook brief leaves them out. The test event carries the agent object too, so you can see what your agent will receive. For a chat bridge, use the standard payload. More on briefs: Hand off to a coding agent.

The payload is generic JSON. It is not in Slack’s, Discord’s or Teams’ message format. If you point an incoming webhook URL from one of these services directly at Vaicoa, the message will not display properly, because each service expects its own format.

Put a small bridge in between: a Cloudflare Worker, a Lambda function, an n8n or Make step, or a few lines of code in something you already run. The bridge receives the JSON object, verifies the signature and posts the format your chat service expects.

If you send to your own endpoint (an incident tool, a script, a queue), no bridge is needed. The object above is the complete format.

A PagerDuty target (Vaicoa Team) sends PagerDuty Events API v2 events to the region you chose:

RegionEndpoint
United Stateshttps://events.pagerduty.com/v2/enqueue
European Unionhttps://events.eu.pagerduty.com/v2/enqueue

The routing key is in the body, so there is no signature and no X-CloudViewer-* header. The timeout, retry and switch-off rules are the same as for a webhook. Any 2xx counts as accepted.

VaicoaPagerDuty event
alert.firingevent_action: "trigger"
alert.resolvedevent_action: "resolve", with only routing_key, event_action and dedup_key
one alert episodeone dedup_key: cv- and 32 hex characters, a hash of the team, the target, the alert’s fingerprint and its start time. Trigger and resolve carry the same key, so the resolve closes the incident the trigger opened
alert.summarypayload.summary, cut to 1,024 characters (the rule key when there is no summary)
alert.severitypayload.severity: critical or warning
resource.idpayload.source; server <id> when there is no resource, the team slug when there is neither
alert.started_atpayload.timestamp, the alert’s own start, not the send time
links.applinks: [{"href": …, "text": …}], one link that opens the alert in the app
links.portalclient_url, with client set to the service’s name
rule, fingerprint, IDs, descriptionpayload.custom_details: rule, fingerprint, project_id, server_id, resource_id, description

An alert that flaps eleven times creates eleven incidents. To merge them, use PagerDuty’s own suppression rules. Two PagerDuty targets open separate incidents.

Send a test event sends a trigger with a fixed summary that says no alert is firing, severity warning and a dedup key starting with cv-test-, then a resolve for it. It cannot resolve a real incident.