Wind Wind / Docs
Dashboard →

Protocol

Webhooks

Signed lifecycle events Wind POSTs to your endpoint.

Set an HTTPS endpoint under App → Webhooks and store the signing secret. Wind POSTs a JSON event with two headers:

  • Wind-Signature: t=<unix seconds>,v1=<hex> — HMAC-SHA256 of `${t}.${rawBody}` with the endpoint’s secret
  • Wind-Event-Id — the event’s evt_… id, the same across retries; use it to de-duplicate

Verify the signature before trusting the payload.

Events

EventWhenDo
connection.createda user finished the connect flowprovision the user
connection.exhaustedallocation hit zerostop calling, nudge the user to raise the cap
connection.revokeduser revoked the grantmark disconnected, prompt re-connect
connection.frozenWind’s risk layer paused the connection after abnormal spendstop calling; the user has been emailed and can resume from review_url
connection.resumedthe user resumed a paused connectioncalls work again
connection.risk_warningthe connection is spending fast enough to be flagged, not yet paused (at most every 10 min per connection)check for a runaway loop; consider telling the user
allocation_change.requestedan acr_ increase was filedsend the user to its approval_url
allocation_change.approveduser approved an acr_ increasethe new cap is live; an exhausted connection is active again
allocation_change.denieduser denied an acr_ increasethe cap is unchanged
app.spend_limit_reachedyour app hit its tier’s daily spend limit (at most once an hour)expect 429 app_spend_limited until spend ages out; consider applying for full live
request.settleda streamed call that ended with settlement: "pending" was settled — charged, or released unchargedrecord the final cost against that request
realtime_session.endeda voice session ended, for any reasonrecord the session’s total

allocation_change.approved and .denied carry { acr_id, connection_id, new_allocation_mc }. app.spend_limit_reached carries { app_id, tier, daily_limit_mc }.

request.settled carries { request_id, connection_id, model, feature, outcome, reason, ledger_id, input_tokens, output_tokens, provider_cost_mc, wind_fee_mc, dev_margin_mc, user_charge_mc }. request_id is the call’s Idempotency-Key. outcome is charged or not_charged; when not charged, reason is usage_missing (the provider never produced a usage record), connection_revoked or connection_frozen, and the token and money fields are null. It only fires for calls that were pending. A stream that ended with settlement: "settled" or "estimated" was already final, so it doesn’t fire.

connection.frozen carries { connection_id, kind, reason, review_url, frozen_at }. kind is chat or realtime (which kind of spending tripped it). reason is a human-readable sentence. review_url is the dashboard page where the user can resume the connection, allow it to spend faster, or revoke it. connection.risk_warning carries { connection_id, kind, reason, review_url }, and connection.resumed carries { connection_id, resumed_at }. See Paused connections.

realtime_session.ended carries { session_id, connection_id, model, end_reason, response_count, charged_mc, duration_s }. charged_mc is the session’s total across all its billed replies. For end_reason values see Voice sessions.

{
  "id": "evt_c41e",
  "type": "connection.frozen",
  "created_at": "2026-10-06T14:02:51Z",
  "data": {
    "connection_id": "conn_1a2b",
    "kind": "realtime",
    "reason": "Voice spending ran at 3.2× a normal conversation's pace.",
    "review_url": "https://dashboard.usewind.app/connections/conn_1a2b?review=paused",
    "frozen_at": "2026-10-06T14:02:51Z"
  }
}
{
  "id": "evt_d7a0",
  "type": "realtime_session.ended",
  "created_at": "2026-10-06T14:20:09Z",
  "data": {
    "session_id": "rts_4c1d",
    "connection_id": "conn_1a2b",
    "model": "gpt-realtime-2.1",
    "end_reason": "client_closed",
    "response_count": 14,
    "charged_mc": 41250000,
    "duration_s": 312
  }
}

Payload

{
  "id": "evt_9f2a",
  "type": "connection.exhausted",
  "created_at": "2026-09-06T18:22:04Z",
  "data": { "connection_id": "conn_1a2b", "app_id": "app_77" }
}

Verifying

The SDKs do this for you: wind.verify({ payload: rawBody, signature: req.headers['wind-signature'] }) in @usewind/node, wind.verify(payload=..., signature=...) in usewind. By hand:

import crypto from 'node:crypto'

function verify(rawBody, header, secret, toleranceSec = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]+)$/i.exec(header ?? '')
  if (!m) return false
  const [, t, sig] = m
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false // replay guard
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const provided = Buffer.from(sig, 'hex')
  return provided.length === expected.length && crypto.timingSafeEqual(provided, expected)
}

Use the raw request body. Re-serialized JSON won’t match.

Delivery

Respond 2xx within 5 seconds. Anything else, including a timeout, is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 15 h, then marked failed. Recent deliveries and their status are listed per endpoint in the console.