Webhooks
Receive real-time HTTP notifications when content is signed, batches are anchored on blockchain, or content is verified. Webhooks let you build event-driven workflows without polling.
Creating a Webhook
Register a webhook endpoint via the REST API. You'll receive a secret for HMAC signature verification.
curl -X POST https://tectra.vision/api/v1/webhooks \
-H "Authorization: Bearer iai_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/tectra",
"events": ["content.signed", "batch.anchored", "content.verified"]
}'{
"id": "wh_abc123",
"url": "https://example.com/webhooks/tectra",
"events": ["content.signed", "batch.anchored", "content.verified"],
"secret": "whsec_k7x9m2...",
"created_at": "2025-06-14T09:32:00Z"
}Save the secret - it is only returned once and is required to verify webhook signatures.
Event Types
content.signedFired when a piece of content is signed and registered. Includes the record ID, hashes, signer info, and blockchain status.
batch.anchoredFired when a Merkle batch is successfully anchored on the Polygon blockchain. Includes the batch ID, Merkle root, leaf count, and transaction hash.
content.verifiedFired when content is verified through the API. Includes the verification result, confidence score, and individual check results.
Payload Format
content.signed
{
"event": "content.signed",
"timestamp": "2025-06-14T09:32:00Z",
"data": {
"record_id": "uuid",
"sha256": "abc123...",
"perceptual_hash": "def456...",
"signature_hex": "ghi789...",
"original_filename": "photo.jpg",
"origin_type": "camera_image",
"signer": {
"org_name": "Reuters Media",
"org_type": "news_agency",
"key_name": "production"
},
"blockchain_status": "pending"
}
}batch.anchored
{
"event": "batch.anchored",
"timestamp": "2025-06-14T09:33:12Z",
"data": {
"batch_id": "batch-uuid",
"merkle_root": "0xabc123...",
"leaf_count": 247,
"tx_hash": "0x4a7f...",
"block_number": 58203947,
"network": "polygon",
"record_ids": ["uuid1", "uuid2", "..."]
}
}content.verified
{
"event": "content.verified",
"timestamp": "2025-06-14T09:34:00Z",
"data": {
"authentic": true,
"confidence": 0.75,
"record_id": "uuid",
"checks": {
"watermark": { "passed": true, "details": "Payload matched" },
"sha256": { "passed": true, "details": "Exact match" },
"perceptual_hash": { "passed": true, "details": "Distance 2" },
"blockchain": { "passed": false, "details": "Pending" }
},
"signer": {
"org_name": "Reuters Media",
"org_type": "news_agency"
}
}
}Signature Verification
Always verify signatures
Every webhook request includes an X-Tectra-Signature header. This is the HMAC-SHA256 of the raw request body, using your webhook secret as the key. Always verify this signature before processing the event.
Headers included with every webhook delivery:
| Header | Description |
|---|---|
| X-Tectra-Signature | HMAC-SHA256 hex digest of the request body |
| X-Tectra-Event | Event type (e.g. content.signed) |
| X-Tectra-Delivery | Unique delivery ID for deduplication |
| X-Tectra-Timestamp | ISO 8601 timestamp of the event |
Receiving Webhooks (Node.js)
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.raw({ type: "application/json" }));
const WEBHOOK_SECRET = process.env.TECTRA_WEBHOOK_SECRET!;
function verifySignature(payload: Buffer, signature: string): boolean {
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
app.post("/webhooks/tectra", (req, res) => {
const signature = req.headers["x-tectra-signature"] as string;
if (!signature || !verifySignature(req.body, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString());
switch (event.event) {
case "content.signed":
console.log("Content signed:", event.data.record_id);
break;
case "batch.anchored":
console.log("Batch anchored:", event.data.tx_hash);
break;
case "content.verified":
console.log("Verified:", event.data.authentic);
break;
}
res.status(200).json({ received: true });
});
app.listen(3000);Receiving Webhooks (Python)
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["TECTRA_WEBHOOK_SECRET"]
def verify_signature(payload: bytes, signature: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/tectra", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Tectra-Signature", "")
if not verify_signature(request.data, signature):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
if event["event"] == "content.signed":
print(f"Content signed: {event['data']['record_id']}")
elif event["event"] == "batch.anchored":
print(f"Batch anchored: {event['data']['tx_hash']}")
elif event["event"] == "content.verified":
print(f"Verified: {event['data']['authentic']}")
return jsonify({"received": True}), 200Retry Policy
3 attempts with exponential backoff
If your endpoint returns a non-2xx status code or times out (30 seconds), Tectra retries the delivery up to 3 times with exponential backoff.
1st attemptImmediate
2nd attemptAfter 1 minute
3rd attemptAfter 5 minutes
After 3 failed attempts, the delivery is marked as failed. You can view failed deliveries and manually retry them from the dashboard or via the API. Use the X-Tectra-Delivery header for idempotency.
Next: Verification Guide
Understanding confidence scores and the 4-check verification system.