Generic Webhook — Flow
How a webhook gets delivered - the full flow
Understanding the end-to-end sequence
1. Admin registers client config via Configuration API
(webhook URL, headers, HMAC secret, workflow routing)
↓ stored in DynamoDB
2. Event happens (transaction completes, link started, etc.)
↓
3. Producer (audit-portal or link-kyc) writes webhook.json to S3
audit-portal → bucket: prod-generic-webhook-ind
link-kyc → bucket: hv-generic-webhook
↓
4. S3 notifies SQS queue
↓
5. SQS triggers Delivery Service Lambda (in batches)
↓
6. For each SQS message, Lambda:
a. Parses message → extracts S3 bucket + key
b. Fetches webhook.json from S3
c. Looks up client config in DynamoDB (appId + eventType)
d. Checks isActive - skips if false
e. Removes appId from payload if deleteAppId = true
f. Generates HMAC-SHA256 signature
g. Resolves target URL(s) via workflow routing
h. Logs pre_delivery to Redshift
i. POSTs payload to client's webhook URL
j. Logs post_delivery (or failure) to Redshift
↓
7. Failed messages are returned to SQS for retry
(only the failed ones - successful ones are not re-processed)

The two important event producers right now
| Producer | Repo | What it sends |
|---|---|---|
| Audit Portal | gitlab.com/hvlabs/kyc/audit-portal | KYC transaction results — final status, intermediate state, manual review decisions, CPR resets |
| Link-KYC | gitlab.com/hvlabs/kyc/global-dkyc | Link-level events — when a user starts a link-kyc flow, or tries to use an expired link |
The event types
Audit Portal
| Event Type | When it fires |
|---|---|
FINISH_TRANSACTION_WEBHOOK | Transaction is fully complete — all KYC checks synced, final status reached (approved/declined) |
INTERMEDIATE_TRANSACTION_WEBHOOK | Transaction in kyc_in_progress state — not all checks done yet |
MANUAL_REVIEW_STATUS_UPDATE | A reviewer manually approves or declines in the portal |
APPLICATION_STATE_RESET | CPR (Cross-Platform Resume) state reset — subtypes: COMPLETE_STATE_RESET, RESET_FROM_SPECIFIC_STEP |
A webhook only fires if that event type is listed in the client's events array in audit-portal's configs table.
Link-KYC
| Event Type | When it fires | Can be disabled? |
|---|---|---|
START_TRANSACTION_WEBHOOK | User opens a link and starts the KYC flow (POST /v1/link-kyc/start) | Yes — startTransactionWebhookEnabled in portal config |
EXPIRED_LINK_USED | User attempts to use a link that has already expired | No — always fires |
What gets saved to S3
Every webhook event is saved as webhook.json to S3 before delivery. The file has two sections:
{
"metadata": {
"appId": "55a3a6",
"transactionId": "txn-abc-123",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"eventId": "evt-456",
"workflowId": "premium-kyc"
},
"payload": {
"appId": "55a3a6",
"transactionId": "txn-abc-123",
"applicationStatus": "auto_approved",
"eventId": "evt-456",
"eventVersion": "1.0.0",
"eventTime": "2024-12-04T10:00:00.000Z",
"eventType": "FINISH_TRANSACTION_WEBHOOK"
}
}
| Section | Used for | Sent to client? |
|---|---|---|
metadata | Internal routing — DynamoDB lookup, HMAC generation, Redshift logging | No |
payload | The actual webhook body | Yes |

S3 paths:
| Producer | Bucket | Path |
|---|---|---|
| Audit Portal | prod-generic-webhook-ind | audit-portal/<appId>/<YYYY-MM-DD>/<eventId>/<eventType>/webhook.json |
| Link-KYC | hv-generic-webhook | link-kyc/<appId>/<YYYY-MM-DD>/<eventId>/<eventType>/webhook.json |
What the client receives
POST https://client.example.com/webhook
Content-Type: application/json
Authorization: Bearer client-token ← from their DynamoDB config headers
x-hv-signature: a1b2c3d4e5f6... ← HMAC-SHA256 signature
{
"appId": "55a3a6",
"transactionId": "txn-abc-123",
"applicationStatus": "auto_approved",
"eventId": "evt-456",
"eventVersion": "1.0.0",
"eventTime": "2024-12-04T10:00:00.000Z",
"eventType": "FINISH_TRANSACTION_WEBHOOK"
}
Only the payload object is sent. metadata is never exposed to the client.
HMAC signature
Every webhook is signed so the client can verify it genuinely came from HyperVerge.
How we generate it
signedPayload = "{transactionId}_{fast-json-stable-stringify(payload)}"
signature = HMAC-SHA256(signedPayload, secret)
header = x-hv-signature: {signature}
fast-json-stable-stringify sorts JSON keys alphabetically before stringifying — this ensures the same payload always produces the same signature regardless of key insertion order.

Workflow-based routing
Clients can route webhooks to different URLs depending on which workflow the transaction used.
Example: A client has standard-kyc and premium-kyc workflows and wants premium transactions delivered to a separate endpoint.
DynamoDB config
{
"appId": "55a3a6",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"webhookUrl": "https://client.com/default-webhook",
"workflows": {
"standard-kyc": ["https://client.com/standard-webhook"],
"premium-kyc": ["https://client.com/premium-webhook1", "https://client.com/premium-webhook2"]
}
}
Routing logic:
- If
metadata.workflowIdexists AND matches a key inworkflowsAND the URL list is non-empty → use those URLs (in parallel) - Otherwise → use default
webhookUrl