Attesto

Webhooks

Signed tenant webhooks

Attesto sends signed HTTP POST deliveries for important lifecycle events. Your endpoint must verify the signature, dedupe delivery IDs, and return a 2xx response within 10 seconds.

When to use webhooks

Webhooks notify your systems that Attesto evidence changed. Use them for workflow automation: start an internal review after a bundle is ready, notify a case system after a checkpoint closes, or pause reliance on a stream when fork evidence appears. Do not treat a webhook as the evidence itself. Fetch or verify the referenced receipt, checkpoint, fork evidence, or bundle before relying on it.

NeedUseWhy
Receive lifecycle notificationsTenant webhooksAttesto calls your endpoint after evidence changes.
Send source evidence into AttestoConnectors or SDK/APIYour system creates the event; Attesto receipts it.
Verify a received bundleOffline verifier or POST /v2/verifyVerification checks evidence integrity; notification does not.

Supported events

EventWhen it fires
batch.confirmedA Merkle batch of tenant events was confirmed on-chain.
batch.failedA batch permanently failed or exceeded the orphan window.
event.anchoredPer-event notification after the related batch is confirmed.
proofstream.checkpointedA Proofstream checkpoint closed and is available for verification.
proofstream.fork_detectedFork evidence was created for a stream history conflict.
proofstream.bundle_readyA verifier bundle is ready for the configured range.

Create a 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

Verify delivery signatures

Each delivery includes these headers:

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)

Minimal Node handler

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 and dedupe

Delivery is at least once. Any non-2xx response, timeout, or refused connection is retried. Dedupe by X-Attesto-Delivery-Id before side effects.

AttemptDelay
1Immediate
230 seconds
32 minutes
410 minutes
51 hour
66 hours
7Final attempt

Endpoint requirements

Use HTTPS, verify the raw body before parsing, store delivery IDs for dedupe, return quickly, and move expensive processing to your own queue after signature verification succeeds.