KYC lifecycle

States
CREATED → IN_REVIEW → COMPLETED / FAILED (+ UPDATE_REQUIRED, see fallback page)
Status
Shipped
Gate
GET /psp/alfred/eligibility reads alfred_kyc_status
How the integration works. On our internal kyc_approved event the BE provisions an Alfred customer (POST /customers/create), files the KYC text (POST /customers/{id}/kyc) and pipes the SumSub documents (POST /customers/{id}/kyc/{subId}/files), then finalizes (updateKycStatusByCircle). Alfred reviews and phones home via webhook. GET /eligibility — the widget's mount gate — reads the resulting alfred_kyc_status kyc-property and decides what the widget renders.

State diagram — CREATED → IN_REVIEW → COMPLETED / FAILED

The four states a trader can land in with Alfred after passing our internal SumSub KYC.

Alfred KYC state machine
Submission filed → Alfred reviews → fans a terminal webhook back
CREATED → IN_REVIEW
Submission filed, awaiting verdict. Maps to PENDING.
eligibility throws ALFRED_KYC_PENDING
COMPLETED
Alfred cleared the user. Writes alfred_kyc_status: APPROVED.
eligibility → { eligible: true }
FAILED
Alfred refused. Terminal — no self-recovery. Writes REJECTED.
eligibility throws ALFRED_KYC_REJECTED
UPDATE_REQUIRED
Alfred wants corrections. Drives the prefilled fallback form.
see KYC fallback page

User stories

What each trader wants in each state. Each has an actor sequence below.

Verified · Approved
As a verified trader, I want to deposit / withdraw via Alfred the moment my KYC clears.
So I can move money without re-entering data. State: alfred_kyc_status: APPROVEDeligibility: { eligible: true }.
Submitted · Pending
As a trader who just submitted KYC, I want a clear "under review" message.
So I don't retry blindly while Alfred is still checking. State: PENDINGeligibility throws ALFRED_KYC_PENDING.
Rejected
As a trader Alfred rejected outright, I want to be told to contact support.
So I'm not stuck guessing. State: REJECTEDeligibility throws ALFRED_KYC_REJECTED.
Update required LIVE
As a trader whose Alfred KYC needs corrections, I want a prefilled form at the payment step to fix & re-submit.
So I never leave the widget. State: UPDATE_REQUIREDeligibility returns a fallback descriptor. See the KYC fallback page.

State flows — success / pending / reject

Reproduce-on-dev sequences for each terminal state. The update-required flow lives on the KYC fallback page.

① Success flow — KYC clears, payment unlocks
QA
Widget
vakotrade BE
Alfred
  1. 1
    QA
    Fire __dev/seed-kyc-and-provision?country=AR → customer created, KYC text + documents submitted.
  2. 2
    QA
    Fire __dev/approve-kyc → pushes KYC COMPLETED webhook to Alfred.
  3. 3
    Alfred
    Fans COMPLETED back → handleKycEvent writes alfred_kyc_status: APPROVED.
  4. 4
    Widget
    GET /eligibility{ eligible: true }.
  5. BE
    Result: deposit / withdrawal path unblocked. Proceed to the onramp/offramp scenarios.
② Pending flow — submitted, awaiting Alfred verdict
QA
Widget
vakotrade BE
Alfred
  1. 1
    QA
    Fire __dev/seed-kyc-and-provision?country=AR only — do not approve.
  2. 2
    BE
    Submission filed (statusKyc: IN_REVIEW). No terminal webhook arrives — Alfred dev does not auto-progress.
  3. 3
    Widget
    GET /eligibility → throws ALFRED_KYC_PENDING.
  4. Widget
    Result: "verification under review" screen. FE may poll eligibility; nothing unblocks until a webhook lands.
③ Reject flow — Alfred refuses the customer
QA
Widget
vakotrade BE
Alfred
  1. 1
    QA
    Force a KYC FAILED webhook with the customer's live submissionId (via __dev/emit-kyc-status on the playground).
  2. 2
    Alfred
    Fans FAILED back → handleKycEvent maps to alfred_kyc_status: REJECTED.
  3. 3
    Widget
    GET /eligibility → throws ALFRED_KYC_REJECTED.
  4. Widget
    Result: "verification was rejected by the payment provider — contact support". No fallback form (terminal state).
Dev helper — force any KYC state on dev. POST /psp/alfred/__dev/emit-kyc-status resolves your customerId + live submissionId server-side and pushes the synthetic KYC webhook to Alfred, which fans it back and updates alfred_kyc_status. Pick a status (CREATED / IN_REVIEW / COMPLETED / FAILED / UPDATE_REQUIRED) to drive any flow — fire it from the Dev playground.

Widget error codes — what QA should see

Each code corresponds to a distinct widget screen / inline message. Verify the FE renders the right copy per code.

CodeWhere it comes fromUser-visible message (expected)
USER_NOT_VERIFIEDsetSessionData, initiate-*"Please complete your KYC to use this feature."
ALFRED_KYC_PENDINGsetSessionData, initiate-*"Your verification is under review — please try again shortly."
ALFRED_KYC_REJECTEDsetSessionData, initiate-*"Verification was rejected by the payment provider — contact support."
ALFRED_NOT_AVAILABLE_FOR_COUNTRYcheckEligibility, setSession"This payment method is not available in your country."
ALFRED_CUSTOMER_NOT_PROVISIONEDcheckEligibilityInternal — should auto-resolve via kyc_approved event.
ALFRED_MAINTENANCE_MODEinitiate-*"The service is undergoing maintenance — try again shortly."
ALFRED_WITHDRAWALS_FROZENinitiate-withdrawal only"Withdrawals are temporarily frozen."
ALFRED_SESSION_NOT_OWNEDrequireSessionGeneric 403 — should never reach a real user.
ALFRED_CROSS_BORDER_NOT_SUPPORTEDsetSessionData"Your account is registered in X — you can't deposit Y."
Raw 111301 bleeding throughAlfred (any 4xx)Should be wrapped by the widget — verify it never shows literally. Bug #1 (AR/CO/DO source) is closed, but the FE wrapper stays as defense.

Webhook payload reference

For QA who want to inspect inbound Alfred webhook traffic in BE logs.

ONRAMP success chain

  • FIAT_DEPOSIT_RECEIVED — Alfred saw the bank credit. Persisted to session; widget stepper updates.
  • TRADE_COMPLETED — fiat → USDC conversion done on Alfred's side.
  • ON_CHAIN_INITIATED — Alfred dispatched the on-chain transfer to our liquidation address.
  • ON_CHAIN_COMPLETED — funds confirmed on-chain. Triggers DepositManualCreateJob and credits the user.

OFFRAMP terminal events

  • FIAT_TRANSFER_COMPLETEDsetWithdrawalStatus({ status: completed })WithdrawalCompleteJob.
  • FAILEDsetWithdrawalStatus({ status: rejected })WithdrawalRejectJob (balance restored).

KYC events

  • COMPLETED → our handler writes alfred_kyc_status: APPROVED to user_kyc_data.
  • REJECTEDalfred_kyc_status: REJECTED.
  • UPDATE_REQUIREDLIVE writes alfred_kyc_status: UPDATE_REQUIRED → drives the fallback form (see KYC fallback).
  • PENDING / others → mapped to PENDING; does not unblock the widget.

Signature verification

HMAC-SHA256. Every inbound webhook arrives with a Signature: t=<unix>,s=<hex> header. We HMAC-SHA256 the body with alfred_webhook_secret (from PSP_ALFRED_CLIENT_CONFIG env var). Replay window is 5 minutes. Failed signature → 401, no state mutation.
Run it. Drive any KYC state from the Dev playground using the KYC status runner — pick CREATED / IN_REVIEW / COMPLETED / FAILED / UPDATE_REQUIRED and Fire.
Drive a KYC state
emit-kyc-status · seed-kyc-and-provision · approve-kyc.