Events

Subscribing

A subscription sends the events it names to a webhook, or through a notification adapter — Slack, Microsoft Teams, Discord, email or ntfy. Make one in the console under Events, with pando subscriptions create, with POST /api/v1/subscriptions, or with the pando_create_subscription tool.

A subscription is about one app, which needs app.view on it, or the whole installation, which needs install.events.manage. Every delivery is checked against its owner's access at the moment it is sent, so a subscription stops delivering when its owner loses sight of what it is about.

Choosing events

A subscription's events is a list of names (deploy.failed), prefixes (deploy.*), or * for everything. A name that matches no event is refused when the subscription is saved. An app event reaches subscriptions on its app and install-wide ones; an install event reaches install-wide subscriptions only.

Webhooks

Pando sends a POST with a JSON body to the subscription's URL:

{
"id": "evt_01J…",
"type": "deploy.failed",
"occurred_at": "2026-10-04T12:00:00Z",
"app": {
"id": "app_01HQ8…",
"name": "Billing",
"slug": "billing"
},
"actor": {
"kind": "system"
},
"data": {
"deployment_id": "dep_…",
"error_code": "BUILD_FAILED",
"message": "…"
}
}

app is absent for an install event. actor.kind is user, token, system or anonymous. data holds the event's fields and nothing else; no event carries a secret value.

HeaderMeaning
Pando-EventThe event's name.
Pando-Event-IdThe event's ID. A delivery can arrive more than once; drop one whose ID you have seen.
Pando-Delivery-IdThis delivery's ID, as the delivery log shows it.
Pando-TimestampWhen this attempt was signed, in Unix seconds.
Pando-Signaturev1= and the hex HMAC-SHA256 of the timestamp, a full stop, and the body, keyed by the subscription's signing key.

Checking a delivery came from Pando

The signing key is shown once, when the subscription is made or its key is rotated. To check a delivery, compute the HMAC over the raw body exactly as received and compare it in constant time; refuse a timestamp more than five minutes from your clock, so a recorded delivery cannot be replayed:

import hashlib, hmac, time
def from_pando(key: str, headers, body: bytes) -> bool:
ts = headers["Pando-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(key.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v1=" + mac, headers["Pando-Signature"])

Retries

A 2xx answer is a delivery. Anything else — another status, a redirect, a timeout after 15 seconds — is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marked failed. Every attempt is recorded and shown in the delivery log, and any delivery can be sent again from it. An endpoint that has failed every attempt for a day is turned off, and its owner is told. Turning it back on clears the record.

Delivery is at least once and survives a restart: events are written to an outbox in the same transaction as what they describe.

A webhook may not reach a private, loopback or link-local address — the local network, the host itself, a cloud metadata service — unless host policy's allow_private_webhooks is on. The check is made on the address actually connected to, and redirects are not followed.

Shaping the request

For a receiver that expects a particular request, a webhook may set method (POST, PUT or PATCH), content_type, headers of its own — for an API key the receiver needs; values are stored encrypted and never shown again — and payload_template, the body as a Go template. Pando's Pando-* headers are always sent and cannot be set, and the signature covers the body actually sent. A template is given .ID, .Type, .OccurredAt, .App (.ID, .Name, .Slug), .Actor, .Data, .Subject, .Body, .Fields, .Link and .Envelope, and the functions json (a value as JSON) and default. For a chat tool that takes a text field:

{"text": {{json .Subject}}, "url": {{json .Link}}}

A template is checked against a sample event when it is saved, and a JSON content type needs it to produce JSON. link points into the console when Pando's external_url is set.

Events

43 events. Audit-sourced events also carry target_kind and target_id in data when the action had a target.

adapter.configuredInstallation

An adapter was added or changed. It applies after a restart.

  • category — The adapter's category.
  • kind — The adapter's kind.
  • credentials_changed — Whether a credential changed.
adapter.recoveredInstallation

An adapter's health check passes again.

  • adapter_id — The adapter.
  • category — Its category.
adapter.unhealthyInstallation

An adapter's health check started failing.

  • adapter_id — The adapter.
  • category — Its category.
  • reason — Why, in words a person can act on.

An app was created.

  • name — The app's name.
  • slug — The app's slug.

An app was deleted. Its volumes are kept.

  • backup_id — The backup's ID, bkp_….
  • volumes_discarded — Whether its volumes were discarded rather than kept.
  • reason — idle when Pando deleted it because nobody used it.

Pando stopped an app that stayed over its disk limit. It stays stopped until somebody starts it.

  • used_bytes — What the app's containers and volumes hold.
  • limit_bytes — Its disk limit.

An app is near its disk limit, or over it and will be stopped if it still is at the next reading.

  • used_bytes — What the app's containers and volumes hold.
  • limit_bytes — Its disk limit.
  • over — true when it is over the limit, false when it is near it.

Somebody opened a shell in one of an app's workloads. The command is named; what was typed is not recorded.

  • workload — The workload.
  • command — The command the session started.

Pando stopped trying to bring an app back after repeated failures. It stays failed until somebody deploys it.

  • reason — Why, in words a person can act on.
  • failures — How many attempts failed.
  • window — Over how long.

Nobody has used an app for a while, and its owner was told Pando will stop or delete it.

  • action — stop or delete.
  • days — The idle setting in force.
  • last_activity — When the app was last used, deployed or started.
  • action_at — When Pando will act.

Pando stopped an app nobody had used. It stays stopped until somebody starts it.

  • days — The idle setting in force.
  • last_activity — When the app was last used, deployed or started.

Somebody restarted an app.

An app's data was restored from a backup.

  • volumes — The volumes restored.

An app's secret was set or changed. The value is never in an event.

  • key — The secret's name.

An app moved from one state to another: running, degraded, failed, stopped, deploying and the rest. A health change is a move between running and degraded.

  • from — The state it was in.
  • to — The state it is in now.

A backup was taken. A disaster-recovery bundle has no app, and reaches install-wide subscriptions only.

  • backup_id — The backup's ID, bkp_….
  • kind — rolling, on_delete or dr_bundle.
  • size_bytes — Its size.

A backup was not taken.

  • scheduled — True for the hourly backup nobody asked for.
  • message — What went wrong.
  • remedy — What to do about it.
  • reason — Why, in words a person can act on.

A backup was restored.

  • restored — What was restored.

Somebody approved a deploy.

  • approvals — Approvals so far.
  • approvals_required — How many it needs.

A deploy failed.

  • deployment_id — The deploy's ID, dep_….
  • spec_id — The revision that did not deploy.
  • trigger — What started it.
  • error_code — The stable error code.
  • message — What went wrong.

Somebody rejected a deploy.

  • reason — Why, in words a person can act on.

A deploy is waiting for approval.

  • spec_revision — The revision to deploy.
  • trigger — What started it.
  • reasons — Why it needs approval.
  • approvals_required — How many approvals it needs.

A deploy started.

  • spec_revision — The revision being deployed.
  • trigger — What started it.

A deploy finished and the app is running the new revision.

  • deployment_id — The deploy's ID, dep_….
  • spec_id — The revision deployed.
  • trigger — What started it.

Somebody was given access. On an app, this is a share; install-wide, a role.

  • plane — control or data.
  • role_id — The role granted, on the control plane.
  • principal_kind — user, group, token or anonymous.
  • principal_id — Who was given access.
  • passcode — Whether sharing with everyone needs a passcode.

Access was taken away.

  • scope — app or install.

A grant's role changed.

  • role_id — The new role.

An in-place upgrade did not finish.

  • from — The version running.
  • to — The version attempted.
  • reason — Why, in words a person can act on.
pando.upgradedInstallation

Pando upgraded itself in place.

  • from — The version before.
  • to — The version now.
policy.updatedInstallation

Host policy changed. The new policy is read from GET /api/v1/policy; the event says only that it changed.

An app's score is below the installation's minimum.

  • grace_hours — Hours before Pando acts.
  • action — What Pando does then.

An app's score is back at or above the minimum.

A scan did not finish.

  • reason — Why, in words a person can act on.

An app was scanned and has a security score.

  • score — The security score, 0 to 100.
  • critical — Critical findings.
  • high — High findings.
  • medium — Medium findings.
  • low — Low and unrated findings.
  • spec_id — The revision scanned.

Pando stopped an app that stayed below the installation's minimum score.

  • grace_hours — The grace period that passed.
  • insecure_since — When it fell below.

Pando turned a subscription off because its endpoint failed every delivery for a day. On an app subscription the event names the app; otherwise it is install-wide.

  • subscription_id — The subscription, sub_….
  • reason — Why, in words a person can act on.
subscription.testInstallation

A test delivery somebody asked for. Sent only to the subscription it tests, whatever its filter says.

  • subscription_id — The subscription being tested.
token.createdInstallation

An API token was created. The token itself is never in an event.

  • name — The token's name.
  • kind — delegated or account.
token.revokedInstallation

An API token was revoked.

user.createdInstallation

An account was created.

  • via — How it was created.
  • adapter_id — The identity provider.
user.deletedInstallation

An account was deleted.

  • username — Its username.

A sign-in was refused.

  • reason — Why, in words a person can act on.
  • adapter_id — The identity provider.
user.signed_inInstallation

Somebody signed in.

  • user_id — The account's ID.
  • via — How: password, or an identity provider.
  • adapter_id — The identity provider.