# Webhooks and billing

NoMac tells your backend when each phone is ready and when it ends, with your user's label and what the phone cost, so you can charge your users your own way.

## Make a webhook

TypeScript

```
const hook = await nomac.webhooks.create({ url: "https://platform.example/nomac", events: ["phone.ready", "phone.ended"] });
// Keep hook.secret: it is shown once.
```

An https URL at a public address, up to 5 per account. Each event is a JSON POST:

### phone.ended

```
{ "id": "whd_...", "type": "phone.ended", "created_at": "2026-10-24T10:00:00.000Z",
  "data": { "reason": "idle",
            "session": { "id": "ses_...", "state": "stopped", "device": "iphone", "label": "user-42",
                         "ready_at": "...", "shutdown_at": "...", "failure_code": null,
                         "cost": { "currency": "USD", "accrued_microunits": "180000",
                                   "billable_seconds": 710, "billed_seconds": 720 } } } }
```

`reason` is `stopped`, `idle`, `time_limit`, `spend_cap`, `credit`, `crashed`, `no_capacity` or `start_failed`.

## Check every delivery

TypeScript
Python

```
import { verifyWebhook } from "@nomac/simulator";

app.post("/nomac", express.raw({ type: "application/json" }), async (req, res) => {
  const event = await verifyWebhook(req.body, req.get("nomac-signature"), process.env.NOMAC_WEBHOOK_SECRET);
  if (event.type === "phone.ended") await charge(event.data.session.label, event.data.session.cost);
  res.sendStatus(204);
});
```

Pass the raw body as received, not parsed JSON. The `nomac-signature` header is `t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" under the secret>`, and a signature older than 5 minutes is refused.

- A delivery not answered 2xx within 5 seconds is sent again 1, 2, 4 and up to 60 minutes later, 12 times in all. Redirects are not followed.
- A delivery can arrive more than once: key on its `id`, also in the `nomac-delivery` header.
- `webhooks.deliveries(id)` lists the last 30 days' deliveries, so you can read back what your endpoint missed while it was down.

## Bill your users

Each `phone.ended` carries what you need: the `label` (your user), `billed_seconds` (whole minutes, at least one) and `accrued_microunits`, what NoMac charged your account in millionths of a dollar. Charge your user from those, with your own price on top.

To reconcile, list a user's phones with their cost:

TypeScript

```
const phones = await nomac.phones.list({ label: "user-42" });
const spent = phones.reduce((sum, p) => sum + Number(p.session.cost?.accrued_microunits ?? 0), 0) / 1e6; // dollars
```

At $0.90 an hour, a 12-minute phone costs 18¢: 720 billed seconds and 180,000 microdollars.
