← Guides

Webhooks

Try it on a test key

curl -s "https://api.wellwardhealth.com/v1/webhooks" -H "Authorization: Bearer $WELLWARD_KEY"

Register an HTTPS endpoint (dashboard → Webhooks, or POST /v1/webhooks) and choose events. You get a signing secret once — store it.

Events

| Event | When | |---|---| | request.completed | A price request finished — outcome says whether a price was obtained, price_ids lists them | | request.expired | A price request passed its promised date by a week unresolved | | price.withdrawn | A price your account retrieved in the last 30 days was withdrawn | | price.updated | A price your account retrieved in the last 30 days changed | | billing.payment_failed | We could not collect an invoice | | usage.threshold_reached | Your month's charges crossed 50%, 80% or 100% of your spending cap |

price_request.completed and price.retired are the older names of request.completed and price.withdrawn; a subscription to either name still works.

An endpoint created with a test key (or as test in the dashboard) receives only test-mode events.

Payload

{ "id": "<delivery id>", "event": "request.completed", "created_at": "2026-09-23T15:04:05Z", "data": { … } }

Verify the signature

Every delivery is signed with your secret, twice, with the same MAC — verify either:

  • Wellward-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
  • X-Wellward-Signature: v=<unix seconds>,d=<hex HMAC-SHA256>

The MAC is over <timestamp>.<raw request body>. Reject a timestamp more than five minutes old.

Node

import crypto from "node:crypto";

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

Python

import hmac, hashlib, time, re

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]+)", header or "")
    if not m or abs(time.time() - int(m.group(1))) > 300:
        return False
    expected = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, m.group(2))

Delivery

At least once. Answer 2xx within 10 seconds; anything else is retried after 1 m, 5 m, 30 m, 2 h, 6 h and 15 h — about a day in all. Deliveries go out on a five-minute cycle, so the first attempt and each retry can land up to five minutes after that. Be idempotent on X-Wellward-Delivery (the delivery id). The dashboard shows each endpoint's delivery log, and Send test event delivers a signed webhook.test immediately (and retries it like any event if your endpoint fails).

A test endpoint receives only test-mode events, which in practice means request.completed (from test price requests) and webhook.test. Withdrawals, price updates and billing events happen only in live mode; to exercise withdrawal handling, use GET /v1/prices/changes?test_scenario=withdrawn_since. price.created can be subscribed to but is never sent.

API reference · Dashboard · Questions: hello@wellwardhealth.com