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:
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.
| Header | Meaning |
|---|---|
Pando-Event | The event's name. |
Pando-Event-Id | The event's ID. A delivery can arrive more than once; drop one whose ID you have seen. |
Pando-Delivery-Id | This delivery's ID, as the delivery log shows it. |
Pando-Timestamp | When this attempt was signed, in Unix seconds. |
Pando-Signature | v1= 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:
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:
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.configuredInstallationAn adapter was added or changed. It applies after a restart.
adapter.recoveredInstallationAn adapter's health check passes again.
adapter.unhealthyInstallationAn adapter's health check started failing.
app.createdAppAn app was created.
app.deletedAppAn app was deleted. Its volumes are kept.
Pando stopped an app that stayed over its disk limit. It stays stopped until somebody starts it.
An app is near its disk limit, or over it and will be stopped if it still is at the next reading.
app.execAppSomebody opened a shell in one of an app's workloads. The command is named; what was typed is not recorded.
app.failedAppPando stopped trying to bring an app back after repeated failures. It stays failed until somebody deploys it.
Nobody has used an app for a while, and its owner was told Pando will stop or delete it.
Pando stopped an app nobody had used. It stays stopped until somebody starts it.
Somebody restarted an app.
app.restoredAppAn app's data was restored from a backup.
An app's secret was set or changed. The value is never in an event.
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.
A backup was taken. A disaster-recovery bundle has no app, and reaches install-wide subscriptions only.
A backup was not taken.
A backup was restored.
Somebody approved a deploy.
A deploy failed.
Somebody rejected a deploy.
A deploy is waiting for approval.
A deploy started.
A deploy finished and the app is running the new revision.
Somebody was given access. On an app, this is a share; install-wide, a role.
Access was taken away.
A grant's role changed.
pando.upgrade_failedInstallationAn in-place upgrade did not finish.
pando.upgradedInstallationPando upgraded itself in place.
policy.updatedInstallationHost 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.
An app's score is back at or above the minimum.
A scan did not finish.
An app was scanned and has a security score.
Pando stopped an app that stayed below the installation's minimum score.
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.testInstallationA test delivery somebody asked for. Sent only to the subscription it tests, whatever its filter says.
token.createdInstallationAn API token was created. The token itself is never in an event.
token.revokedInstallationAn API token was revoked.
user.createdInstallationAn account was created.
user.deletedInstallationAn account was deleted.
user.sign_in_deniedInstallationA sign-in was refused.
user.signed_inInstallationSomebody signed in.