Webhooks
Webhooks tenant signés
Attesto envoie des signed HTTP POST deliveries pour les lifecycle events importants. Votre endpoint doit vérifier la signature, déduper les delivery IDs et retourner une réponse 2xx en moins de 10 secondes.
Quand utiliser les webhooks
Les webhooks notifient vos systèmes lorsque l'evidence Attesto change. Utilisez-les pour workflow automation: démarrer une revue interne lorsqu'un bundle est prêt, notifier un case system après la fermeture d'un checkpoint, ou suspendre la reliance sur un stream quand une fork evidence apparaît. Ne traitez pas un webhook comme l'evidence elle-même. Récupérez ou vérifiez le receipt, checkpoint, fork evidence ou bundle référencé avant de vous y fier.
| Need | Use | Why |
|---|---|---|
| Receive lifecycle notifications | Tenant webhooks | Attesto appelle votre endpoint après des changements d'evidence. |
| Send source evidence into Attesto | Connectors or SDK/API | Votre système crée l'event; Attesto le receipt. |
| Verify a received bundle | Offline verifier or POST /v2/verify | Verification contrôle l'evidence integrity; une notification ne le fait pas. |
Supported events
| Event | Quand il se déclenche |
|---|---|
batch.confirmed | Un Merkle batch d'events tenant a été confirmé on-chain. |
batch.failed | Un batch a échoué définitivement ou dépassé l'orphan window. |
event.anchored | Notification per-event après confirmation du batch lié. |
proofstream.checkpointed | Un Proofstream checkpoint s'est fermé et est disponible pour verification. |
proofstream.fork_detected | Fork evidence créée pour un conflit de stream history. |
proofstream.bundle_ready | Un verifier bundle est prêt pour la range configurée. |
Créer une subscription
: "${ATTESTO_WEBHOOK_URL:?set this to an HTTPS endpoint controlled by your organization}"
curl -X POST https://dashboard.attesto.eu/v1/webhooks \
-H "Authorization: Bearer $ATTESTO_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- <<JSON
{
"name": "audit-notifications",
"url": "$ATTESTO_WEBHOOK_URL",
"eventTypes": ["batch.confirmed", "batch.failed"],
"description": "Posts lifecycle notifications to the configured endpoint"
}
JSON
Vérifier les delivery signatures
Chaque delivery inclut ces headers:
X-Attesto-Event: event type.X-Attesto-Delivery-Id: ID unique de delivery attempt.X-Attesto-Timestamp: Unix seconds utilisés dans la signature.X-Attesto-Signature: HMAC-SHA256 surtimestamp + "." + raw_body.
Delivery body shape:
{
"id": "delivery_...",
"event": "proofstream.bundle_ready",
"tenant_id": "tenant_...",
"created_at": "2026-06-07T12:00:00Z",
"data": {
"stream_id": "str_...",
"bundle_id": "bundle_...",
"from_seq_no": 1,
"to_seq_no": 250,
"verification_url": "https://verify.attesto.eu/v2/verify"
}
}
import hashlib
import hmac
import time
def verify(signing_value: str, body: bytes, timestamp: str, signature: str) -> bool:
ts = int(timestamp)
if abs(int(time.time()) - ts) > 300:
return False
expected = hmac.new(
signing_value.encode("ascii"),
f"{ts}.".encode("ascii") + body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Handler Node minimal
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.raw({ type: "application/json" }));
function verifyDelivery(signingValue, body, timestamp, signature) {
const ts = Number(timestamp);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) {
return false;
}
const expected = crypto
.createHmac("sha256", signingValue)
.update(`${timestamp}.`)
.update(body)
.digest("hex");
const expectedBytes = Buffer.from(expected, "hex");
const receivedBytes = Buffer.from(signature || "", "hex");
return expectedBytes.length === receivedBytes.length
&& crypto.timingSafeEqual(expectedBytes, receivedBytes);
}
app.post("/attesto/webhook", async (req, res) => {
const ok = verifyDelivery(
process.env.ATTESTO_WEBHOOK_SIGNING_VALUE,
req.body,
req.header("X-Attesto-Timestamp"),
req.header("X-Attesto-Signature"),
);
if (!ok) return res.status(401).end();
const deliveryId = req.header("X-Attesto-Delivery-Id");
const payload = JSON.parse(req.body.toString("utf8"));
await recordDeliveryOnce(deliveryId, payload);
res.status(204).end();
});
Retry et dedupe
Delivery est at least once. Toute réponse non-2xx, timeout ou refused
connection est retry. Dedupe par
X-Attesto-Delivery-Id avant les side effects.
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | Final attempt |
Endpoint requirements
Utilisez HTTPS, vérifiez le raw body avant parsing, stockez les delivery IDs pour dedupe, répondez rapidement et déplacez le processing coûteux dans votre propre queue après succès de signature verification.
