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
- You configure a webhook endpoint URL in your VEC.digital institution dashboard
- When a subscribed event occurs, VEC.digital sends an HTTP POST request to your URL
- Each delivery includes a JSON payload and headers for verification
- Your endpoint processes the event and responds with a
2xxstatus code
Delivery Headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the raw request body, using your signing secret |
X-Webhook-Event | The event name (e.g., certificate_application.created) |
X-Webhook-ID | The webhook endpoint ID |
X-Webhook-Timestamp | ISO 8601 timestamp of when the delivery was sent |
Content-Type | application/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 eventPayload 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
| Field | Type | Description |
|---|---|---|
event | string | The event name that triggered the delivery |
webhook_id | string | UUID of the webhook endpoint |
timestamp | string | ISO 8601 timestamp of when the event occurred |
application_id | integer | Internal ID of the certificate application |
uuid | string | UUID of the certificate application |
certificate_title | string | Title of the certificate |
applicant_email | string | Email of the applicant |
source | string | Origin of the application: vault (Institution-uploaded) or customer (User-submitted) |
trigger_context | string | Specific 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 (
2xxstatus) - Failed deliveries (non-2xx responses or network errors) are retried up to 3 times with 30-second backoff intervals
- The
last_delivery_statusin the dashboard showssuccess,failed, ornone - The
consecutive_failurescounter 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
2xxstatus 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.