Activity Webhook
Post KYT, KYA, or KYE activity records to the gateway from async workers that don't go through
the REST path. Each post inserts one row into user_activity_history and appears immediately
in the user's dashboard.
Endpoint
POST /api/webhook/activity
Authentication
Send the shared secret in an X-Activity-Webhook-Secret header. Requests without a matching
header return 401 Unauthorized.
X-Activity-Webhook-Secret: $ACTIVITY_WEBHOOK_SECRET
Configure the secret via the ACTIVITY_WEBHOOK_SECRET environment variable. Generate one with:
openssl rand -hex 32
Request body
{
"activityType": "kyt",
"userId": "user_32T5kyEywX9x8X3P3XGxcyptIbn",
"requestId": "a9b2-...-c1d3",
"responseId": "f5a0-...-9e1b",
"statusCode": 200,
"blockchain": "bitcoin",
"transactionHash": "3a1bfd9d...",
"address": null,
"entityId": null,
"entityName": null,
"riskLevel": "low",
"riskScore": 12,
"requestPayload": { "chain": "bitcoin", "txid": "3a1bfd9d..." },
"responsePayload": { "riskLevel": "low", "riskScore": 12, "signals": [] },
"responseSummary": { "risk": "low" },
"errorMessage": null,
"metadata": { "source": "async-worker-1" },
"clientAppId": "app_2xMn1psnHkYRTH00nxdvp02IWfr",
"clientAppName": "LedgerLink Web"
}
Field reference
| Field | Type | Required | Description |
|---|---|---|---|
activityType | "kyt" | "kya" | "kye" | ✅ | Which engine produced the activity. |
userId | string | ✅ | Clerk user ID the activity belongs to. |
requestId | string (UUID) | Defaults to a fresh UUID if omitted. | |
responseId | string (UUID) | Upstream engine's response id, if available. | |
statusCode | number | HTTP status. Defaults to 200. Anything >= 400 is flagged as an error. | |
blockchain | string | Chain name (bitcoin, ethereum, solana, …). Required for KYT / KYA. | |
transactionHash | string | Required when activityType === "kyt". | |
address | string | Required when activityType === "kya". | |
entityId | string | Required when activityType === "kye". | |
entityName | string | Human-readable label for KYE entries. | |
riskLevel | "low" | "medium" | "high" | "critical" | Surfaced on dashboards. | |
riskScore | number (0–100) | Machine-readable risk score. | |
requestPayload | object | Full request body from your worker — useful for replay. | |
responsePayload | object | Full upstream response — surfaced in the activity detail drawer. | |
responseSummary | object | Compact version for list views. | |
errorMessage | string | Populated automatically for statusCode >= 400 if omitted. | |
metadata | object | Free-form; indexed as JSONB. | |
clientAppId | string | Which application initiated the call (maps to app_* ids). | |
clientAppName | string | Display name for the app (e.g. "LedgerLink Web"). |
Responses
| Status | Body | When |
|---|---|---|
201 | { "success": true, "id": "…", "requestId": "…" } | Activity persisted. |
400 | { "error": "Invalid activityType. Must be one of: kyt, kya, kye" } | Unknown activityType. |
400 | { "error": "userId is required" } | Missing userId. |
401 | { "error": "Unauthorized" } | Bad / missing secret. |
500 | { "error": "Failed to record activity" } | Database error — safe to retry. |
Example
Submit a KYA entry for an address lookup worker:
curl -X POST http://localhost:8090/api/webhook/activity \
-H "Content-Type: application/json" \
-H "X-Activity-Webhook-Secret: $ACTIVITY_WEBHOOK_SECRET" \
-d '{
"activityType": "kya",
"userId": "user_32T5kyEywX9x8X3P3XGxcyptIbn",
"statusCode": 200,
"blockchain": "bitcoin",
"address": "1Drt3c8pSdrkyjuBiwVcSSixZwQtMZ3Tew",
"riskLevel": "medium",
"riskScore": 42
}'
Downstream effects
Writing an activity record does not touch credits — credit accounting happens inline on the REST path. Use the webhook purely to make async/off-gateway work visible to the user.