Webhooks
Webhooks allow your systems to receive real-time notifications when events occur in Concord eVault, eliminating the need to poll the API for status changes.
Overview
When a subscribed event occurs, Concord eVault sends an HTTP POST request to your configured endpoint URL with a signed JSON payload describing the event. Your server acknowledges receipt by returning any 2xx status code.
How it works
- An Organization Administrator creates a webhook in the portal, providing a URL and selecting which events to subscribe to.
- A signing secret is returned once at creation time — store it securely.
- When a subscribed event occurs, eVault dispatches a signed POST request to your URL.
- Your server validates the signature and processes the payload.
Setting up a webhook
Step 1: Create the webhook in the portal
Webhooks are created by Organization Administrators through the Concord eVault portal.
- Log into the Concord eVault portal.
- Navigate to Organization › Vaults and open the vault you want to configure.
- Select the Webhooks tab.
- Click Create Webhook.
- Configure the webhook: URL — the HTTPS endpoint on your server that will receive events; Events — select the event types to subscribe to.
- Click Create.
Step 2: Store the secret
After creation, the portal displays the webhook's signing secret once. This is the only time it is shown — copy it to your secrets manager immediately. If it is lost, a new secret can be generated from the portal. The secret is a 64-character lowercase hex string, for example:
a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
Step 3: Implement your endpoint
Your endpoint must accept POST requests with Content-Type: application/json, verify the X-VeraVault-Signature header (see Verifying Payloads), and return a 2xx response quickly — process the event asynchronously if needed.
Event types
| Event | Description |
|---|---|
certified_print.completed | A certified print has been successfully generated |
package_release.approved | A package release request was approved |
package_release.declined | A package release request was declined |
package_release.export_ready | An approved release export is ready to download |
certified_print.completed
Fired when a certified print finishes generating and is ready to download.
| Field | Type | Description |
|---|---|---|
certifiedPrintId | guid | ID of the completed certified print |
certificateNumber | string | Certificate number (e.g., CP-20240115-A7B3C9D1) |
vaultPackageId | guid | ID of the package the print belongs to |
packageName | string | Display name of the package |
fileSizeBytes | integer | Size of the generated PDF in bytes |
completedAt | ISO 8601 datetime | When the certified print was completed |
package_release.approved
Fired when the operations team approves a package release request. Export generation begins; wait for package_release.export_ready before downloading.
| Field | Type | Description |
|---|---|---|
releaseRequestId | guid | ID of the approved release request |
vaultPackageId | guid | ID of the package being released |
packageName | string | Display name of the package |
approvedAt | ISO 8601 datetime | When the request was approved |
package_release.declined
Fired when the operations team declines a package release request.
| Field | Type | Description |
|---|---|---|
releaseRequestId | guid | ID of the declined release request |
vaultPackageId | guid | ID of the package |
packageName | string | Display name of the package |
reason | string | The documented decline reason |
declinedAt | ISO 8601 datetime | When the request was declined |
package_release.export_ready
Fired when an approved release export finishes generating and can be downloaded via the Package Releases API.
| Field | Type | Description |
|---|---|---|
releaseRequestId | guid | ID of the release request |
vaultPackageId | guid | ID of the package being released |
packageName | string | Display name of the package |
certificateNumber | string | Export certificate (e.g., PO-20260115-A7B3C9D1) |
fileSizeBytes | integer | Size of the export ZIP in bytes |
checksum | string | SHA-256 checksum of the export ZIP |
expiresAt | ISO 8601 datetime | When the export can no longer be downloaded |
Payload format
Every webhook delivery uses the same envelope structure, with event-specific details in the data field.
Envelope
{
"event": "certified_print.completed",
"webhookId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deliveryId": "01944b7a-1234-7abc-8def-9876543210ab",
"timestamp": "2024-03-01T14:32:10.123Z",
"vaultId": "550e8400-e29b-41d4-a716-446655440000",
"organizationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"data": {}
}
| Field | Type | Description |
|---|---|---|
event | string | The event type that triggered this delivery |
webhookId | guid | ID of the webhook subscription |
deliveryId | guid | Unique ID for this delivery attempt |
timestamp | ISO 8601 datetime | UTC time when the delivery was dispatched |
vaultId | guid | The vault this event belongs to |
organizationId | guid | The organization that owns the vault |
data | object | Event-specific payload (see Event Types) |
Full example: certified_print.completed
{
"event": "certified_print.completed",
"webhookId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"deliveryId": "01944b7a-1234-7abc-8def-9876543210ab",
"timestamp": "2024-03-01T14:32:10.123Z",
"vaultId": "550e8400-e29b-41d4-a716-446655440000",
"organizationId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"data": {
"certifiedPrintId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"certificateNumber": "CP-20240301-F4A2D8B1",
"vaultPackageId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"packageName": "Loan Agreement - Smith, John",
"fileSizeBytes": 524288,
"completedAt": "2024-03-01T14:32:09.876Z"
}
}
Request headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-VeraVault-Event | The event type (e.g., certified_print.completed) |
X-VeraVault-Delivery | The unique delivery ID (matches deliveryId in payload) |
X-VeraVault-Signature | HMAC-SHA256 signature of the request body (see below) |
Verifying payloads
Always verify the signature before processing a webhook payload. This confirms the request originated from Concord eVault and the payload has not been tampered with.
How the signature is computed
- The raw request body bytes are signed using HMAC-SHA256 with the webhook secret as the key — use the secret as a UTF-8 string (do not hex-decode it).
- The result is hex-encoded (lowercase).
- The header value is
sha256=followed by the hex digest.
X-VeraVault-Signature: sha256=3b5a7c2d1f8e4a09b6c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1
Verification steps
- Read the raw request body before parsing it as JSON.
- Compute HMAC-SHA256 of the raw body bytes using your stored secret as the key.
- Hex-encode the result (lowercase).
- Compare your computed value to the value after
sha256=in theX-VeraVault-Signatureheader. - Use a constant-time comparison to prevent timing attacks.
- Reject the request if the values do not match.
Code examples
Node.js
import crypto from "crypto";
function verifySignature(secret, rawBody, signatureHeader) {
const prefix = "sha256=";
if (typeof signatureHeader !== "string" || !signatureHeader.startsWith(prefix)) {
return false;
}
const receivedSignatureHex = signatureHeader.slice(prefix.length);
if (!/^[0-9a-fA-F]+$/.test(receivedSignatureHex)) {
return false;
}
const expectedSignatureHex = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
if (expectedSignatureHex.length !== receivedSignatureHex.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expectedSignatureHex, "hex"),
Buffer.from(receivedSignatureHex, "hex"),
);
}
C#
using System;
using System.Security.Cryptography;
using System.Text;
bool VerifySignature(string secret, byte[] rawBody, string signatureHeader)
{
if (string.IsNullOrEmpty(signatureHeader))
return false;
const string Prefix = "sha256=";
if (!signatureHeader.StartsWith(Prefix, StringComparison.Ordinal))
return false;
var signatureHex = signatureHeader.Substring(Prefix.Length);
var keyBytes = Encoding.UTF8.GetBytes(secret);
var expectedBytes = HMACSHA256.HashData(keyBytes, rawBody);
try
{
var receivedBytes = Convert.FromHexString(signatureHex);
return CryptographicOperations.FixedTimeEquals(expectedBytes, receivedBytes);
}
catch (FormatException)
{
return false;
}
}
Important: Always use the raw request body bytes for signature verification. Parsing the JSON and re-serializing it before verification will produce a different byte sequence and cause verification to fail.
Managing deliveries
Viewing delivery history
The portal records every delivery attempt for each webhook. To view delivery history, navigate to Organization › Vaults and open the vault, select the Webhooks tab and click on a webhook, then select the Deliveries tab. Each delivery shows the event type, whether it succeeded, the HTTP status code returned by your endpoint, the full payload that was sent, and the response body. This is useful for debugging when your endpoint is not receiving or processing events as expected.
Replaying deliveries
If your endpoint was unavailable or returned an error, you can re-send a previous delivery from the portal without needing to trigger the original event again. A replay re-sends the same event data but with a new delivery ID and current timestamp, and creates a new delivery record with the outcome. To replay a delivery, navigate to the webhook's Deliveries tab (see above), find the delivery you want to re-send, and click Replay. Note: The webhook must be active to replay a delivery.
Rotating the signing secret
If your secret is compromised, or as part of routine security hygiene, you can generate a new signing secret from the portal. The new secret takes effect immediately. To rotate a webhook secret, navigate to Organization › Vaults and open the vault, select the Webhooks tab and click on the webhook, click Rotate Secret, and copy the new secret immediately — it is only shown once.
