Skip to content

Webhooks Guide

Webhooks

Webhooks allow your application to receive real-time HTTP notifications when specific events occur in your VEC.digital institution. Instead of polling the API, VEC.digital sends a POST request to your endpoint whenever a subscribed event fires.

How It Works

  1. You configure a webhook endpoint URL in your VEC.digital institution dashboard
  2. When a subscribed event occurs, VEC.digital sends an HTTP POST request to your URL
  3. Each delivery includes a JSON payload and headers for verification
  4. Your endpoint processes the event and responds with a 2xx status code

Delivery Headers

Every webhook delivery includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the raw request body, using your signing secret
X-Webhook-EventThe event name (e.g., certificate_application.created)
X-Webhook-IDThe webhook endpoint ID
X-Webhook-TimestampISO 8601 timestamp of when the delivery was sent
Content-Typeapplication/json

Verifying Signatures

Each webhook delivery is signed using HMAC-SHA256 with the signing secret that was shown when you created the webhook. You should always verify the signature before processing the payload.

Verification Logic

expected_signature = HMAC-SHA256(raw_request_body, your_signing_secret)
valid = timing_safe_compare(X-Webhook-Signature header, expected_signature)

Important: Always use a timing-safe comparison function to prevent timing attacks. Never use regular string equality.

Node.js Example

const crypto = require("crypto");
function verifySignature(rawBody, signatureHeader, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expectedSignature),
);
}
// In your Express handler:
app.post(
"/webhooks/vec",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-webhook-signature"];
const rawBody = req.body; // Raw body buffer
if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(rawBody.toString());
const event = req.headers["x-webhook-event"];
// Process the event
console.log(`Received event: ${event}`, payload);
res.status(200).send("OK");
},
);

PHP Example

function verifySignature(string $rawBody, string $signatureHeader, string $secret): bool {
$expectedSignature = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expectedSignature, $signatureHeader);
}
// In your handler:
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$event = $_SERVER['HTTP_X_WEBHOOK_EVENT'];
if (!verifySignature($rawBody, $signature, $webhookSecret)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$payload = json_decode($rawBody, true);
// Process the event

Payload Format

All webhook payloads follow a standardized structure to ensure consistency across different event types:

{
"event": "certificate_application.approved",
"webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-06-04T10:30:00Z",
"application_id": 42,
"uuid": "9e7860f0-05d1-4fd0-b3e0-e98e19f1cf15",
"certificate_title": "Certificate of Achievement",
"applicant_email": "john@example.com",
"source": "vault",
"trigger_context": "bulk_upload"
}

Common Payload Fields

FieldTypeDescription
eventstringThe event name that triggered the delivery
webhook_idstringUUID of the webhook endpoint
timestampstringISO 8601 timestamp of when the event occurred
application_idintegerInternal ID of the certificate application
uuidstringUUID of the certificate application
certificate_titlestringTitle of the certificate
applicant_emailstringEmail of the applicant
sourcestringOrigin of the application: vault (Institution-uploaded) or customer (User-submitted)
trigger_contextstringSpecific action context: single_upload, bulk_upload, or customer_submission

Test Event

When you use the “Test Webhook” button in the dashboard, a test payload is sent:

{
"event": "webhook.test",
"webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-06-04T10:30:00Z",
"data": {
"message": "This is a test webhook delivery."
}
}

Delivery & Retry Behavior

  • Webhooks are delivered asynchronously — your endpoint should respond quickly (2xx status)
  • Failed deliveries (non-2xx responses or network errors) are retried up to 3 times with 30-second backoff intervals
  • The last_delivery_status in the dashboard shows success, failed, or none
  • The consecutive_failures counter tracks repeated delivery failures and resets to 0 on a successful delivery
  • A disabled webhook (is_active: false) will not receive deliveries

Best Practices

  • Always verify signatures before processing webhook payloads
  • Respond quickly — return a 2xx status immediately, then process the event asynchronously
  • Handle duplicates — webhooks may occasionally be delivered more than once; make your handler idempotent
  • Use HTTPS — your endpoint must be publicly accessible over HTTPS
  • Log deliveries — keep a record of received payloads and their signatures for debugging

Limits

  • Each institution can create up to 5 webhook endpoints
  • Each webhook must subscribe to at least 1 event
  • Webhook URLs must be valid HTTPS URLs (maximum 2048 characters)

Required Permission

The manage:webhooks permission is required to create, update, delete, and test webhook endpoints in the institution dashboard.

Available Events

For a full list of available events and their specific payload details, see the Webhook Events Reference.