Skip to main content

Generic Webhook — Integration Guide

Audience: Solutions engineers and integration engineers setting up webhooks for clients.


Before you start

The Configuration API is internal only.

  • Config API Prod URL: https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/
  • Config API Dev URL: https://zi2btyam4ewqn4pb6kfz4bbkoq0yvwpg.lambda-url.ap-south-1.on.aws/
  • Authentication: appid + appkey headers (internal credentials) or JWT Bearer token

Setting up a webhook for an audit-portal client

Audit-portal automatically calls the generic webhook Config API whenever a client's webhook config is created or updated in audit-portal. You do not need to call the Config API manually for audit-portal clients.

When audit-portal creates/updates a config, it internally calls:

curl --request POST 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <INTERNAL_APPID>' \
--header 'appkey: <INTERNAL_APPKEY>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "<event-type>",
"isActive": true,
"webhookUrl": "<client-webhook-url>",
"product": "audit-portal",
"headers": {},
"updateSecret": true,
"worfklows": {
"<<workflowId1>>": ["url1"],
"<<workflowId2>>": ["url2"]
},
"additionalMetadataKeys": ["workflowId"]
}'

It also calls GET with shareSecret: true to retrieve the HMAC secret and return it to the client.

Audit-portal configs table (relevant columns)
ColumnWhat it means
appIdClient's app ID
webhookUrlDefault webhook URL
eventsArray of event types enabled for this client. Only listed event types are delivered.
newFlowtrue = use generic webhook (S3 → SQS → Lambda). false = old deprecated direct flow. Default true for all new clients — ensure all configs have newFlow: true.
headersCustom headers to pass through to delivery
additionalMetadataKeysSet to ["workflowId"] to include workflowId in S3 file metadata, enabling workflow routing
webhookPayloadBlacklistedKeysKeys stripped from the payload before S3 upload. Default: ["workflowId"] — prevents routing keys from leaking to clients

Audit portal webhook config

Audit-portal SQS consumer flow

Audit-portal consumes the dev-audit-portal-finish-transaction SQS queue. On each message:

  1. Calls updateTransactionDB to process the transaction
  2. If status === "kyc_in_progress" → triggers INTERMEDIATE_TRANSACTION_WEBHOOK
  3. Any other terminal status → triggers FINISH_TRANSACTION_WEBHOOK

To temporarily stop all audit-portal webhook delivery without code changes: set DISABLE_WEBHOOK_DELIVERY=yes env var.


Link-KYC does not auto-call the generic webhook Config API. You must register configs manually.

Link-KYC webhook setup

Step 1: Call the Config API to register the client's webhook config:

curl --request POST 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "START_TRANSACTION_WEBHOOK",
"isActive": true,
"webhookUrl": "https://client.example.com/webhook",
"product": "link-kyc",
"headers": { "Authorization": "Bearer client-token" },
"updateSecret": true
}'

Repeat for each event type (START_TRANSACTION_WEBHOOK, EXPIRED_LINK_USED).

Step 2: Share the HMAC secret with the client:

curl --request GET 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "START_TRANSACTION_WEBHOOK",
"shareSecret": true
}'

Link-KYC fetches its delivery config from Portal API (not from DynamoDB directly):

  • Polls POST {PORTAL_BASE_URL}/api/v2/internal/configs/list every 15 minutes
  • Fields fetched: enableTamperCheck, events, webhookPayloadBlacklistedKeys, additionalMetadataKeys
  • Defaults if fetch fails: startTransactionWebhookEnabled: true, webhookPayloadBlacklistedKeys: ['workflowId'], additionalMetadataKeys: []
Link-KYC vs audit-portal — key differences
AspectAudit PortalLink-KYC
S3 Bucketprod-generic-webhook-indhv-generic-webhook (env: GENERIC_WEBHOOK_S3_BUCKET)
S3 path prefixaudit-portal/link-kyc/
Auto-registers Config APIYesNo — manual registration required
Config sourceGeneric webhook DynamoDB (via Config API)Portal API polled every 15 min
Event typesFINISH_TRANSACTION_WEBHOOK, INTERMEDIATE_TRANSACTION_WEBHOOK, MANUAL_REVIEW_STATUS_UPDATE, APPLICATION_STATE_RESETSTART_TRANSACTION_WEBHOOK, EXPIRED_LINK_USED
Kill switchDISABLE_WEBHOOK_DELIVERY=yesNo equivalent

Setting up a webhook for a new product (not audit-portal or link-kyc)

Step 1: Get PUTObject access to the prod-generic-webhook-ind S3 bucket.

Step 2: Register the client's webhook config via Config API:

curl --request POST 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"isActive": true,
"webhookUrl": "https://client.example.com/webhook",
"product": "your-product-name",
"headers": { "Authorization": "Bearer client-token" },
"updateSecret": true
}'

Step 3: When events occur in your product, upload webhook.json to S3:

<product-name>/<appId>/<YYYY-MM-DD>/<eventId>/<eventType>/webhook.json

File format:

{
"metadata": {
"appId": "<client-appid>",
"transactionId": "<transactionId>",
"eventType": "<event-type>",
"eventId": "<unique-event-id>",
"workflowId": "(optional - only if using workflow routing)"
},
"payload": {
"(actual data to deliver to the client)"
}
}

Step 4: Done. The Delivery Service Lambda picks it up automatically via SQS.


Configuring workflow-based routing

Use this when a client wants different webhook URLs for different workflows (e.g. standard vs premium KYC flows).

Workflow routing setup

How to configure

Step 1: Enable workflowId in S3 metadata via the Audit Portal config API (uses the client's credentials):

curl --request PUT 'https://review-api.idv.hyperverge.co/api/v1/config' \
--header 'appId: <client-appid>' \
--header 'appKey: <client-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"additionalMetadataKeys": ["workflowId"]
}'

Step 2: Configure per-workflow URLs via the Config API (uses internal credentials):

curl --request PATCH 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"workflows": {
"standard-kyc": ["https://client.com/standard-webhook"],
"premium-kyc": ["https://client.com/premium-webhook1", "https://client.com/premium-webhook2"]
}
}'

How routing works at delivery time:

  1. If metadata.workflowId exists AND matches a key in workflows AND the URL list is non-empty → deliver to those URLs (in parallel if multiple)
  2. Otherwise → fall back to default webhookUrl

Notes:

  • Workflow routing is supported for FINISH_TRANSACTION_WEBHOOK and MANUAL_REVIEW_STATUS_UPDATE from audit-portal. The generic webhook system itself is event-type agnostic.
  • Multiple URLs per workflow are all delivered in parallel via Promise.all()
  • Each eventType has its own separate HMAC secret
  • The Config API is internal — do not share with clients

Updating a client's webhook config

To update a webhook URL, rotate a secret, toggle active state, or update headers:

curl --request PATCH 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"webhookUrl": "https://new-endpoint.example.com/webhook",
"updateSecret": true
}'

Only the fields you send are updated. appId and eventType are the lookup key and cannot be changed.

To rotate the HMAC secret, pass "updateSecret": true. The new secret is returned in the response.


Pausing webhook delivery for a client

Set isActive: false to pause delivery without deleting the config:

curl --request PATCH 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"isActive": false
}'

Messages for inactive configs are logged as "EVENT NOT ACTIVE CURRENTLY" and considered successfully processed — no SQS retry.


Retrieving a client's HMAC secret
curl --request GET 'https://a4l5wq5l6gjkjrromxx3riovu40lpzcx.lambda-url.ap-south-1.on.aws/' \
--header 'appid: <internal-appid>' \
--header 'appkey: <internal-appkey>' \
--header 'Content-Type: application/json' \
--data '{
"appId": "<client-appid>",
"eventType": "FINISH_TRANSACTION_WEBHOOK",
"shareSecret": true
}'

The secret is hidden from GET responses by default. Pass shareSecret: true to include it.


How clients verify the HMAC signature

Share this with clients:

const crypto = require('crypto');
const stringify = require('fast-json-stable-stringify');

function verifySignature(transactionId, payload, receivedSignature, secret) {
const signedPayload = `${transactionId}_${stringify(payload)}`;
const expected = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(receivedSignature), Buffer.from(expected));
}

// Usage:
// transactionId - from the webhook body (payload.transactionId)
// payload - the full webhook body object (as received, before any modification)
// receivedSignature - value of the x-hv-signature header
// secret - the shared secret obtained from Config API GET with shareSecret: true

Important notes for clients:

  • Use fast-json-stable-stringify (or any library that sorts keys alphabetically) — not JSON.stringify
  • Always use timingSafeEqual for comparison — never ===
  • The secret is unique per appId + eventType pair
  • If the signature doesn't match, reject the webhook

Troubleshooting

ProblemWhat to check
Webhook not delivered at allCheck DynamoDB config exists for appId + eventType. Check isActive is true. Check CloudWatch logs for Lambda errors.
Webhook delivered but client says it never arrivedCheck generic_webhook_failure Redshift table. Check client's server logs. Check if SSL errors (see APP_IDS_TO_IGNORE_SSL_VERLIFICATION).
Client can't verify signatureConfirm they are using fast-json-stable-stringify (keys alphabetically sorted). Confirm transactionId is from the payload, not metadata. Confirm they are using the correct secret (per eventType).
Wrong URL being calledCheck workflows config in DynamoDB. Check if workflowId is present in S3 metadata.
Webhooks stopped after config changeVerify isActive is still true. For audit-portal clients, verify newFlow: true.
Delivery succeeds but client gets wrong payloadCheck webhookPayloadBlacklistedKeys in audit-portal config — keys listed here are stripped before S3 upload. Check deleteAppId flag in DynamoDB.
Was this helpful?
Ask AI

Ask anything about the internal documentation

AI answers are based on internal documentation. Verify critical information.