Docs / Reference

Events

Every lifecycle event a stack emits, when it fires, its payload, and how webhooks deliver and sign it.

Everything that happens to a stack is recorded as an event. Events can be polled with GET /v0/stacks/{id}/events or agentserve events, and are pushed to the stack's webhook if the manifest sets notify.webhook. Claimed projects also show them on the dashboard's activity page.

Envelope#

Every event has the same shape, both from the API and in a webhook body:

{
  "id": 57,
  "stack_id": "stk_3f9a1c0b2d4e",
  "type": "service.crashed",
  "data": {"service": "api", "exit_code": 1, "crashes": 2},
  "created_at": 1791647012.4
}
FieldTypeMeaning
idintegerIncreasing across the whole server, not per stack. Use it as the after cursor when polling.
stack_idstringThe stack.
typestringOne of the types below.
dataobjectType-specific payload. {} when there is none.
created_atnumberUnix time in seconds.

Event types#

Deploys#

TypeFires whendata
stack.createdA stack is created, before its first build starts.anonymous (boolean): false when created with an API key.
stack.redeployingA redeploy was accepted and its build is starting.generation: the generation being built.
service.buildingEach service's install and build starts, in dependency order.service, generation
service.readyA service is serving: its process passed the health check, or the static site is in place. Fires on deploys, rollbacks and restores.service, url
stack.liveA deploy or redeploy finished and every service is up.generation, urls (service name to URL), expires_at (null if claimed)
stack.deploy_failedA redeploy failed to build or become healthy and the previous generation is serving again. The stack stays live with deploy_error set.generation (the failed one), error (last 1,000 characters), serving_generation
stack.failedThe stack stopped: the first deploy failed, a redeploy of a failed stack failed, a rollback couldn't bring the previous generation back, a service crashed too often, or a restore failed.error (last 1,000 characters)

Runtime#

TypeFires whendata
service.crashedThe supervisor found a service's process exited while the stack is live. Also fires when an automatic restart attempt itself fails, with exit_code: null.service, exit_code (integer or null), crashes (crashes in the last 10 minutes, including this one)
service.restartedA crashed service was restarted and is healthy, or a restart requested from the dashboard succeeded.service; requested: true for a restart from the dashboard

After more than 5 crashes of one service within 10 minutes, the stack becomes failed and stack.failed fires instead of a restart. See Limits.

Lifetime#

TypeFires whendata
stack.extendedThe TTL was extended.expires_at
stack.expiringAn unclaimed stack is 5 minutes from expiry (by default). Fires once; extending the TTL re-arms it.expires_at, message: "Ask your human to claim this stack, or extend the TTL."
stack.expiredAn unclaimed stack's TTL ran out and its processes were stopped.retained_until (when the snapshot is deleted), claim_url: the server's claim entry page (<base>/claim), where a human types the code
stack.claimedThe stack moved into an account, from the browser or POST /v0/claim.by: the account's email
stack.restoredA stack came back without a rebuild: after a claim of an expired stack, and for every live stack when the server restarts.urls
stack.agent_revokedThe owner revoked the manage token in the dashboard.none
stack.deletedThe stack was deleted: through the API, the CLI, the dashboard, or because an unclaimed snapshot reached the end of its retention.none

A typical sequence#

stack.created       {"anonymous": true}
service.building    {"service": "api", "generation": 1}
service.building    {"service": "web", "generation": 1}
service.ready       {"service": "api", "url": "https://api-todo-app-3f9a.agentserve.sh"}
service.ready       {"service": "web", "url": "https://web-todo-app-3f9a.agentserve.sh"}
stack.live          {"generation": 1, "urls": {…}, "expires_at": 1791649931.2}
stack.expiring      {"expires_at": 1791649931.2, "message": "Ask your human to claim this stack, or extend the TTL."}
stack.expired       {"retained_until": 1792254731.2, "claim_url": "https://agentserve.sh/claim"}
stack.claimed       {"by": "[email protected]"}
service.ready       {"service": "api", …}
service.ready       {"service": "web", …}
stack.restored      {"urls": {…}}

All services are built first, then all are started: every service.building comes before the first service.ready.

Polling#

GET /v0/stacks/{id}/events?after=<id> returns up to 200 events with an id greater than after, oldest first. Start at after=0 and pass the last id you received. See HTTP API.

Webhook delivery#

When the stack has a webhook URL, each event is POSTed to it as soon as it is recorded, on a background thread per event.

HeaderValue
Content-Typeapplication/json
X-AgentServe-EventThe event type, e.g. stack.live.
X-AgentServe-DeliveryThe event id. The same on every retry, so use it to deduplicate.
X-AgentServe-Signaturet=<unix>,v1=<hex>. See below.
  • The body is the event envelope.
  • Each attempt has a 5-second timeout.
  • A 2xx or 3xx response is a success. A 4xx response is a failure and is not retried.
  • Network errors and 5xx responses are retried after 1, 5 and 25 seconds: four attempts in total. Then the event is dropped from delivery; it can still be polled.
  • Deliveries run in parallel, so they can arrive out of order. Order by id.
  • Redirects are not followed.

Signature#

v1 is the hex HMAC-SHA256 of "<t>.<raw body>", keyed with the stack's webhook_secret (whsec_…). t is the Unix time of the attempt, so it changes between retries. The secret is returned once: in the create response when the manifest has notify.webhook, or in the redeploy response that first adds one.

Verify against the raw bytes of the body, before parsing it, and reject old timestamps:

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

More on receiving webhooks in Webhooks.

Stuck? Your agent can read /skill.md, or connect it over MCP.