Webhooks
Signés, relancés avec délai progressif, protégés contre la relecture et testables avant que votre récepteur existe.
Inscrivez un point de terminaison avec POST /v1/webhooks (https seulement, un hôte public; les adresses privées et de bouclage sont refusées à l'inscription, puis de nouveau par résolution à la livraison). Le secret de signature (whsec_…) est renvoyé une seule fois. Les points de terminaison appartiennent à un environnement : ceux du bac à sable reçoivent les événements du bac à sable, ceux du réel, les événements réels.
| Événement | Quand |
|---|---|
calculation.completed | Un calcul est terminé |
connection.activated | Un client a connecté un profil à votre application |
connection.revoked | Une connexion a pris fin (par le client ou par vous) |
records.imported | Un lot de dossiers est arrivé dans un profil connecté |
key.revoked | Une clé d'API a été révoquée |
webhook.test | Une livraison de test que vous avez demandée |
Vérifier une livraison
Chaque livraison porte Taxpio-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, ainsi que Taxpio-Delivery-Id et Taxpio-Event. Recalculez le HMAC sur le corps brut avec votre secret, comparez en temps constant et refusez un t plus ancien que 300 secondes. Répondez par un code 2xx en moins de huit secondes; toute autre réponse est relancée.
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"));
}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)Nouvelles tentatives et collecteur
Une livraison échouée est relancée après 1m, 5m, 30m, 2h, 12h, puis marquée comme morte. Un point de terminaison qui échoue 25 livraisons de suite est désactivé; réactivez-le depuis la console. Les redirections ne sont jamais suivies. POST /v1/webhooks/{id}/test met en file une livraison webhook.test; GET /v1/webhooks/{id}/deliveries montre chaque tentative avec son statut, la dernière réponse du point de terminaison et la prochaine tentative.
Avant que votre récepteur existe, inscrivez un point de terminaison de bac à sable dont l'URL est https://api.taxpio.com/v1/sandbox/webhook-sink/pending : TaxPio la réécrit avec l'identifiant propre du point de terminaison, se livre à lui-même, vérifie sa propre signature avec le secret de votre point de terminaison et affiche le résultat dans la trace et la console.
{
"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" } }
}