# Events and webhooks

Find out when a deployment becomes ready, fails, is suspended or ends, and when your balance runs low, without polling each deployment. There are two ways to get the same events:

- **Poll one feed**: `GET /platform/events?after=<cursor>`. It works from anywhere, including an agent on a laptop that can't receive requests.
- **Webhooks**: we `POST` each event to your HTTPS endpoint as it happens.

## Event types

| `type` | When |
|---|---|
| `deployment.ready` | A rental or private model is running. After a resume too: `previous_state` is then `suspended`. |
| `deployment.failed` | It could not start, or stopped with an error; `error` says why. Nothing is billed for a start that failed. |
| `deployment.suspended` | It was paused: `suspended_reason` is `balance` (credit ran out) or `idle` (a private model nobody called). |
| `deployment.terminated` | It was stopped for good: deleted, or `max_runtime_minutes` reached. |
| `balance.low` | Your balance lasts under 24 hours at the current rate. Sent again under 2 hours. |
| `capacity.available` | GPUs you asked to hear about are free now (see [capacity alerts](https://www.green-compute.com/docs/gpu-rental#when-gpus-are-sold-out)). |

We also email you when a rental that was running stops by itself (a `deployment.failed` with `previous_state: "ready"`), once per batch, so you don't need anything set up to hear about it.

An event:

```json
{
  "id": 4182,
  "type": "deployment.suspended",
  "created_at": "2026-10-30T09:12:44.103000+00:00",
  "data": {
    "deployment_id": "d869d719-9d86-4131-9d25-41bdc9d8d5a8",
    "name": "rig-03",
    "workload_id": "6f0c…",
    "state": "suspended",
    "previous_state": "ready",
    "error": "Insufficient balance — top up your credits to resume",
    "suspended_reason": "balance"
  }
}
```

For `capacity.available`, `data` has `alert_id`, `gpu_model`, `gpu_count`, `count`, `available_gpus` and `max_gpus_per_deployment` (the most free on one machine).

For `balance.low`, `data` has `balance_cents`, `balance_usd`, `usd_per_hour`, `hours_left`, `runs_out_at`, `under_hours` (24 or 2) and `running` (what is being billed). Top up, or let [auto-resume](https://www.green-compute.com/docs/gpu-rental#running-out-of-balance) bring suspended rentals back.

## Poll the feed

```bash
AFTER=0
while true; do
  PAGE=$(curl -s "$GC_API/platform/events?after=$AFTER" -H "Authorization: Bearer $GC_KEY")
  echo "$PAGE" | jq -c '.events[]'
  AFTER=$(echo "$PAGE" | jq '.next_after')
  [ "$(echo "$PAGE" | jq '.has_more')" = true ] || sleep 15
done
```

- Keep `next_after` and pass it as `after` next time. `after=0` returns everything still kept; events are kept 30 days.
- `types=deployment.ready,deployment.failed` returns only those types. `limit` is up to 500 (default 100). `has_more: true` means call again right away.
- An event appears about 5 seconds after the change, so that the order you read is final.
- A key with just the `read` scope can poll the feed.

## Webhooks

Create one with a full-access API key:

```bash
curl -s -X POST "$GC_API/platform/webhooks" \
  -H "Authorization: Bearer $GC_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/green-compute", "events": ["deployment.ready", "deployment.failed", "deployment.suspended", "deployment.terminated"]}'
```

The reply has the webhook's `webhook_id` and its `secret`. The secret is shown only this once, so store it. Leave out `events` to get every type. `POST /platform/webhooks/{id}/test` sends a `webhook.test` event straight away and tells you whether your endpoint answered.

How delivery works:

- The URL must be `https://` and resolve to a public address. Redirects are not followed.
- Each event is the JSON body of one `POST`, with the headers `Green-Compute-Event` (the type), `Green-Compute-Event-Id` and `Green-Compute-Signature`.
- Answer with any `2xx` status within 10 seconds. Anything else counts as a failure. The event is retried after 10 s, 20 s, 40 s and so on, up to an hour apart, and later events wait, so they always arrive in order.
- Delivery is at least once: the same event can arrive twice. Use `id` to skip repeats.
- A webhook that keeps failing for 3 days is turned off (`active: false`, with the reason in `disabled_reason`). Fix the endpoint, then `PATCH /platform/webhooks/{id}` with `{"active": true}`, and it delivers what it missed.
- Up to 5 webhooks per account. A new webhook gets events from the moment it is created; the feed has the earlier ones.

### Check the signature

`Green-Compute-Signature` looks like `t=1793345564,v1=5f3a…`: `v1` is the hex HMAC-SHA256 of `<t>.<raw body>`, keyed with your webhook's secret. Check it against the raw body, before parsing the JSON:

```python
import hashlib, hmac, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) <= tolerance
    return fresh and hmac.compare_digest(expected, parts["v1"])
```

## Calls

| Call | Does |
|---|---|
| `GET /platform/events` | Events after `after`, oldest first. Optional `types` and `limit`. |
| `POST /platform/webhooks` | Creates a webhook: `url`, optional `events`. Returns its `secret` once. |
| `GET /platform/webhooks` | Your webhooks, with `active`, `failures`, `last_error` and `last_success_at`. |
| `PATCH /platform/webhooks/{id}` | Changes `url` or `events`, or turns it off and on with `active`. |
| `DELETE /platform/webhooks/{id}` | Deletes it. |
| `POST /platform/webhooks/{id}/test` | Sends a test event now: `{"delivered": true, "status_code": 200}`. |
