Assetera Docs
Distribution partners

KYC webhooks

The authoritative KYC result channel. The common envelope, every event type MetaKYC emits, sample payloads, and how to register a subscription.

Webhooks are how you learn a KYC result. The browser callback tells you the embedded flow ended, not what was decided: a review can still be running after the user sees the last screen.

Subscriptions are configured per tenant in the admin panel under Config → Webhooks. Each subscription pairs a WebhookType with a target URL, and more than one subscription can listen to the same event.

Delivery

Every webhook is a JSON POST with Content-Type: application/json. MetaKYC adds two helper fields to every type-specific payload:

{
  "...": "type-specific fields",
  "internalAppId": { "applicantId": 1248 },
  "provider":      { "name": "sumsub" },
  "timestamp":     "2026-09-17T08:21:47.1234567Z",
  "deliveryId":    "b75fa64e-c506-3c30-a690-a46dbcec75e7"
}
BehaviourDetail
LoggingEvery call is logged with its HTTP status and response body, visible in the admin panel.
RetriesA transport error, 408, 429 or any 5xx is retried for that subscription with exponential backoff, from 30s up to a 1 hour cap.
Giving upAfter 5 consecutive failures the subscription is dead-lettered and skipped until an admin re-enables it. A 2xx resets the counter.
Permanent errorsAny other 4xx is recorded and never retried. A URL answering 404 loses that event for good.
OrderingNot guaranteed. Treat each delivery as a statement about current state.

The same event can arrive more than once

Retries reuse the same deliveryId, so de-duplicate on it rather than on the payload. Acknowledge fast with a 2xx and process asynchronously: a slow handler that times out is a retryable failure and will be re-sent. Reconcile with GetApplicantProgress to cover a dead-lettered subscription.

Verifying a delivery

When a signing secret is configured, each request carries these headers. Verify the signature before trusting the body.

HeaderValue
X-Assetera-Delivery-IdStable idempotency key, unchanged across retries.
X-Assetera-TimestampUnix seconds at send time.
X-Assetera-Signaturet=<unix>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<unix>.<raw body>").
X-Webhook-SecretThe shared secret, when one is configured.
// Compute over the RAW body, before any JSON parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

Event types

WebhookTypeFires whenPayload
OnApplicantCreatedAn applicant registration succeeds, when called with WebSdk = true.Progress result.
OnApplicantResultAn identity provider finishes verification (Sumsub, Sardine, Onfido).Raw provider result.
OnWatchlistResultA sanction or PEP screening completes.Screening result.
OnRiskAssessmentCompletedA risk-scoring plan finishes.{ riskLevel, riskPlan, score }.
OnFinishWorkflowThe applicant reaches the end of the workflow tree.Workflow finish output.
OnHoldPrgocessThe workflow pauses, typically for a flag or manual review.Snapshot plus hold reason.
OnBeneficiariesAddedKYB: a UBO, director, shareholder or representative is added.{ company, type, email, link }.
OnProgressChangedAfter every step transition, including on-hold flips.Lightweight status.

OnApplicantResult is the verdict channel, and OnFinishWorkflow is the one that tells you the whole tree is done. Spellings such as OnHoldPrgocess are kept for backwards compatibility: match them exactly.

Sample payloads

{
  "applicantId": 1248,
  "workflowRisks": [
    { "planName": "Default", "riskLevel": "LowRisk", "riskAssesmentProvider": "internal" }
  ],
  "finalStatus":      "Approved",
  "workflowResult":   "Success",
  "nextWorkflowKey":  null,
  "nextWorkflowName": null,
  "internalAppId":    { "applicantId": 1248 },
  "provider":         { "name": null }
}
{
  "applicantId":      1248,
  "workFlowKey":      "INDIVIDUAL_FULL_KYC",
  "workflowResult":   "InProgress",
  "riskLevel":        null,
  "kycStatus":        "Pending",
  "reviewStatus":     "InProgress",
  "status":           "InProgress",
  "nextWorkflowKey":  null,
  "nextWorkflowName": null,
  "internalAppId":    { "applicantId": 1248 },
  "provider":         { "name": null }
}
{
  "riskLevel":     "MediumRisk",
  "riskPlan":      "Default risk plan",
  "score":         42,
  "internalAppId": { "applicantId": 1248 },
  "provider":      { "name": null }
}
{
  "company": "Acme Holdings Ltd.",
  "type":    "UBO",
  "email":   "jane.doe@example.com",
  "link":    { "url": "https://your-domain.example/u/abc123" },
  "internalAppId": { "applicantId": 1248 },
  "provider":      { "name": null }
}

Field values such as finalStatus, workflowResult and riskLevel use the vocabularies in the enum reference.

Registering a subscription

The admin panel is the usual route. Subscriptions can also be managed over the admin REST API:

POST {baseUrl}/api/services/app/WebhookInfo/CreateOrEditWebhookInfo
{
  "webhookType": "OnApplicantCreated",
  "url":         "https://your-domain.example/api/webhooks/metakyc",
  "isEnabled":   true
}

On this page