Geldstuck’s KYC flow is simple at the integration layer: your backend creates a short-lived session, your client launches the hosted verification experience, and your backend waits for the signed webhook that carries the final result.

Before you start

  • The user must already exist in your tenant (sync users first).
  • Register a webhook endpoint subscribed to at least kyc.*.
  • Decide where the verification runs: web (React / vanilla JS), iOS, or Android.

Step 1 - Create a verification session

Your backend asks Geldstuck for a short-lived verification session token for a specific user. The token is scoped to one session.
curl https://api.geldstuck.com/v1/kyc/onfido \
  -H "x-api-key: $GELDSTUCK_PUBLIC_KEY" \
  -H "x-api-secret: $GELDSTUCK_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "usr_01HX3ZAB...",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "dob": "1815-12-10"
  }'
Response:
{
  "applicantId": "appl_01HX3ZH...",
  "workflowRunId": "wf_01HX3ZI...",
  "sdkToken": "eyJhbGciOi...",
  "expiresIn": 3600
}

Step 2 - Launch the verification flow

Use the client integration configured for your account and pass the token plus session ID returned by the API.
launchVerificationFlow({
  sessionToken: sdkToken,
  sessionId: workflowRunId,
  mount: "#kyc-mount",
  onComplete: () => {
    // Don't trust this callback for authorization. Wait for the webhook.
    router.push("/kyc/pending");
  },
});
A client-side completion callback only tells you the user finished the submission UI - not that they passed. The authoritative signal is the kyc.completed webhook.

Step 3 - Handle the completion webhook

import { verifyWebhook } from "./verify"; // see /webhooks/verify

app.post(
  "/webhooks",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = verifyWebhook(
        req.body,
        req.headers["geldstuck-signature"] as string,
        process.env.GELDSTUCK_WEBHOOK_SECRET!,
      );
    } catch {
      return res.status(400).send("Invalid signature");
    }

    switch ((event as any).type) {
      case "kyc.completed":
        await markUserVerified((event as any).data.userId);
        break;
      case "kyc.failed":
        await flagForReview((event as any).data.userId, (event as any).data.reason);
        break;
    }

    res.sendStatus(200);
  },
);
Event payload:
{
  "id": "evt_01HX3ZJ...",
  "type": "kyc.completed",
  "tenantId": "tnt_01HX3Z8MQW...",
  "data": {
    "userId": "usr_01HX3ZAB...",
    "status": "complete",
    "result": {
      "identity": "clear",
      "document": "clear",
      "liveness": "clear"
    }
  },
  "createdAt": "2026-04-22T10:34:17.000Z"
}

Reusing KYC across transactions

A user who’s completed KYC for a transaction can often skip it for another one in the same tenant. Check eligibility before minting a new session:
GET /v1/kyc/:userId/reuse-eligibility
{ "eligible": true, "expiresAt": "2027-04-22T00:00:00.000Z" }
If eligible is true, you can skip Step 1 and mark the new transaction verified directly.

Statuses

statusMeaning
pendingRecord created, user hasn’t started.
in_progressUser has started the verification flow.
reviewAwaiting additional automated or manual checks.
completeVerified. Safe to let the user transact.
failedDid not pass. See reason for details.

Common pitfalls

Tokens are valid for 60 minutes. If the client reports an expired session, mint a new one and resume the same workflowRunId.
Check the dashboard’s Events → Deliveries view. A 503 or connection timeout at your endpoint will trigger retries - the event isn’t lost.
Additional review can take longer than the automated path. If it stays in review longer than expected, inspect the KYC record for the latest status or contact support.