API DOCUMENTATION

Webhooks

Signed, retried with backoff, replay-protected, and testable before your receiver exists.

Register an endpoint with POST /v1/webhooks (https only, a public host; private and loopback addresses are refused at registration and again by resolution at delivery). The signing secret (whsec_…) is returned once. Endpoints belong to one environment: sandbox endpoints receive sandbox events, live endpoints live events.

EventWhen
calculation.completedA calculation finished
connection.activatedA client connected a profile to your application
connection.revokedA connection ended (by the client or by you)
records.importedA batch of records landed in a connected profile
key.revokedAn API key was revoked
webhook.testA test delivery you asked for

Verify a delivery

Every delivery carries Taxpio-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, plus Taxpio-Delivery-Id and Taxpio-Event. Recompute the HMAC over the raw body with your secret, compare in constant time, and refuse a t older than 300 seconds. Answer any 2xx within eight seconds; anything else is retried.

Node
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTaxpio(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length / 2 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}
Python
import hmac, hashlib, time

def verify_taxpio(secret: str, header: str, raw_body: bytes, now: int | None = None) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs((now or int(time.time())) - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts.get("v1", ""), expected)

Retries and the sink

A failed delivery is retried after 1m, 5m, 30m, 2h, 12h, then marked dead. An endpoint that fails 25 deliveries in a row is disabled; enable it again from the console. Redirects are never followed. POST /v1/webhooks/{id}/test queues a webhook.test delivery; GET /v1/webhooks/{id}/deliveries shows every attempt with its status, the endpoint's last answer and the next attempt.

Before your receiver exists, register a sandbox endpoint whose URL is https://api.taxpio.com/v1/sandbox/webhook-sink/pending: TaxPio rewrites it to the endpoint's own id, delivers to itself, verifies its own signature with your endpoint's secret, and shows the outcome in the trace and the console.

Event body
{
  "id": "evt_…", "object": "event", "type": "calculation.completed", "created": "2026-09-06T08:19:32.797Z",
  "environment": "sandbox", "workspace_id": "…", "attempt": 1,
  "data": { "calculation": { "id": "calc_…", "jurisdiction": "CA", "program": "T1", "tax_year": 2026, "scope": "IN_SCOPE", "rule_package": "CA-T1-PREP-2026.1" } }
}