Docs · Webhooks and billing

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": "160000",
                                   "billable_seconds": 710, "billed_seconds": 720 } } } }

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

Check every delivery

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.80 an hour, a 12-minute phone costs 16 cents: 720 billed seconds and 160,000 microdollars.