Skip to content

Webhooks

Webhooks push events to your HTTPS endpoint as they happen — no polling required. Available on Growth, Scale, and Enterprise plans.

All webhook management endpoints require a JWT session token (Authorization: Bearer <session-token>).


EventFired when
record.createdA provenance record is signed and stored
verify.attemptedAny verification is run against your records
verify.tamper_detectedA verification returns tampered: true
quota.warningSigning usage crosses 80% of your plan limit
quota.limitFree tier limit hit — signing blocked (402)
generator.revokedOne of your generators is revoked

Every webhook POST has this body:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "record.created",
"timestamp": "2026-06-11T14:23:01Z",
"org_id": "org_abc123",
"data": { }
}

id is the delivery ID — use it for idempotency.


Every request includes an X-Certivu-Signature header:

X-Certivu-Signature: t=1749654181,v1=abc123...

Verify it on your end:

import crypto from "node:crypto";
function verifyCertivuSignature(rawBody, header, secret) {
const [tPart, v1Part] = header.split(",");
const ts = tPart.split("=")[1];
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
if (expected !== v1Part.split("=")[1]) return false; // invalid signature
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay attack
return true;
}

The 5-minute tolerance (300s) protects against replay attacks. Reject requests outside that window.


List all registered endpoints for your org, plus plan eligibility.

GET /v1/webhooks
Authorization: Bearer <session-token>
{
"endpoints": [
{
"webhook_id": "uuid",
"url": "https://example.com/certivu-hook",
"events": ["record.created", "verify.tamper_detected"],
"status": "active",
"failure_count": 0,
"created_at": "2026-06-11T10:00:00Z"
}
],
"plan": "growth",
"webhooks_enabled": true
}

webhooks_enabled is false for Free and Starter plans. The endpoint always returns 200 — check this field rather than looking for a 402.


Create a new endpoint. Growth+ only. Maximum 10 endpoints per org.

POST /v1/webhooks
Authorization: Bearer <session-token>
Content-Type: application/json
{
"url": "https://example.com/certivu-hook",
"events": ["record.created", "verify.tamper_detected", "quota.warning"]
}

Response (201):

{
"webhook_id": "uuid",
"url": "https://example.com/certivu-hook",
"events": ["record.created", "verify.tamper_detected", "quota.warning"],
"status": "active",
"failure_count": 0,
"created_at": "2026-06-11T10:00:00Z",
"secret": "a3f2c8b1e7d4..."
}

The secret is returned once and cannot be retrieved again. Store it immediately.


Update URL, events, or status. Re-enabling (status: "active") resets the failure counter.

{ "status": "active" }

Permanently delete an endpoint and its delivery log.


Last 50 deliveries, sorted newest first. Payload is excluded from list responses.

{
"deliveries": [
{
"delivery_id": "uuid",
"webhook_id": "uuid",
"event": "record.created",
"status": "delivered",
"status_code": 200,
"response_body": "ok",
"attempts": 1,
"created_at": "2026-06-11T14:23:01Z"
}
]
}

Delivery records are auto-deleted after 30 days via MongoDB TTL.


POST /v1/webhooks/:id/deliveries/:deliveryId/retry

Section titled “POST /v1/webhooks/:id/deliveries/:deliveryId/retry”

Synchronously re-attempt a failed delivery. Growth+ only. The endpoint must be active.

{ "success": true }

Returns 409 if the delivery already succeeded or the endpoint is disabled.


After 5 consecutive delivery failures, Certivu automatically sets the endpoint status to disabled. Re-enable it via PATCH /v1/webhooks/:id — this also resets the failure counter to 0.

A delivery fails if:

  • The HTTP response is not 2xx
  • The request times out (10 second limit)
  • A network error occurs