# Webhooks: events pushed to your URL

Leave a URL once; the platform POSTs a signed JSON event there whenever something happens to the pod. No polling for `running`, no polling for expiry.

## Set it

On create: add `"webhookUrl": "https://ops.example.com/hooks"` to `POST /api/agents`. The reply carries `webhookSecret` beside `agent`.

Later, or to rotate the secret:

```bash
curl -s -X PUT https://agentspodium.com/api/agents/$ID/webhook -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"url":"https://ops.example.com/hooks"}'
# {"agent":{…,"webhookUrl":"https://ops.example.com/hooks"},"webhook":{"url":"…","secret":"whsec_…","events":[…]}}
```

The secret is shown **once**, by the call that set it. `GET /api/agents/:id` shows only `webhookUrl`, `webhookLastStatus`, `webhookLastAt`, `webhookLastEvent`. `DELETE /api/agents/:id/webhook` removes the hook.

The URL must be an absolute `https://` (or `http://`) address on a public host; loopback, private ranges and URLs with credentials are refused with 400.

## Prove it works

```bash
curl -s -X POST https://agentspodium.com/api/agents/$ID/webhook/test -H "Authorization: Bearer $TOKEN"
# {"delivery":{"ok":true,"status":200,"attempts":1}}
```

This sends a `test` event with all retries and waits for the outcome, so a dead receiver takes about a minute to report `ok: false`.

## What arrives

`POST <your url>`, `Content-Type: application/json`, headers:

| header | value |
|---|---|
| `X-AgentsPodium-Event` | the event type |
| `X-AgentsPodium-Delivery` | the event id; retries reuse it, so it is your idempotency key |
| `X-AgentsPodium-Signature` | `sha256=` + hex HMAC-SHA256 of the raw request body, keyed with your secret |
| `User-Agent` | `AgentsPodium-Webhooks/1.0` |

Body:

```json
{ "id": "evt_9f3c2a1b7d4e6f80", "type": "agent.running", "createdAt": "2026-09-09T10:00:00.000Z",
  "agent": { "id": "agt_…", "name": "My Agent", "engine": "hermes", "tier": "small", "status": "running", "endpointUrl": "https://…" },
  "data": { "from": "provisioning", "to": "running" } }
```

| type | when | `data` |
|---|---|---|
| `agent.running` | the pod came up: after create, resume, rebuild, or a recovery | `from`, `to` |
| `agent.stopped` | paused by you, by an unpaid trial, or by a lapsed payment | `from`, `to` |
| `agent.failed` | a build or rebuild did not come up (rebuild again; data is restored from backup) | `from`, `to` |
| `agent.deleted` | the pod and its data are gone | `from`, `to` |
| `payment.confirmed` | a card, crypto or Stars payment activated a subscription | `subscriptionId`, `provider`, `tier`, `period`, `paidTill` |
| `deletion.warning` | 3, 2 and 1 days before an unpaid pod is stopped, and once when it stops | `daysLeft` (null once stopped), `keptDays` |
| `test` | you asked for it | `note` |

Verify the signature before acting; reply with any 2xx within 10 seconds. Anything else is retried twice more, 10 seconds and then a minute later; after that the outcome is written to the agent record and the event is dropped. Order between events is not guaranteed; use `createdAt`.

Verification in Node:

```js
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET).update(rawBody).digest("hex");
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-agentspodium-signature"] ?? ""));
```
