KYC: levels, features and the application queue

Building verification levels and their 51 feature switches, the two settings that make those switches real, working the application queue, and proving a third-party verifier is wired up.

9 min readUpdated 6 August 2026kyc, compliance, verification, levels, features

KYC is the part of the platform you are personally answerable for. It has two screens and one very large trap.

The screens are Users → Compliance & Verification → Verification Levels (/admin/crm/kyc/level), where you define what a level asks for and what it unlocks, and KYC Applications (/admin/crm/kyc/application), where you approve or reject what customers send in.

The trap is that the per-level feature switches decide nothing until a second setting is turned on, and that setting is off by default on every install. Read What makes the feature switches real before you spend an afternoon curating them.

Permissions

Screen or action Keys
Verification Levels access.kyc.level + view.kyc.level
Create a level create.kyc.level
Edit / bulk status edit.kyc.level
Delete a level delete.kyc.level
KYC Applications access.kyc.application + view.kyc.application
Approve / reject / notes edit.kyc.application
Delete an application delete.kyc.application
Verification services (read, connection and env checks, results) view.kyc.verification
Run a verification edit.kyc.verification

/admin/crm/kyc/level/{id} is not in the URL permission map, so the page at that address falls back to access.admin; the data behind it is still gated on the keys above.

Verification levels

Lists levels with pagination and filtering
Creates a level
Updates a level

A level is a row in kyc_level with seven meaningful fields.

Field What it is
name The label customers see — "Basic", "Identity", "Address". Required.
description Free text explaining what this level requires. Required.
level An integer giving the tier. This is the ordering, and it is what "highest approved level" means for a customer. Required.
fields JSON. The form the applicant fills in.
features JSON. Which platform features the level unlocks.
serviceId Optional. The verification service that handles this level. Blank is stored as null; a service id that does not exist is refused with a 400 naming it.
status ACTIVE, DRAFT or INACTIVE. Required; defaults to ACTIVE.

DRAFT is how you build a level without exposing it. Bulk status changes go through PUT /api/admin/crm/kyc/level/status.

The level builder

Creating or editing a level opens a full-screen builder — a control rail, a live form preview and a right-hand editor.

Full-screen means what it says: the admin nav is not drawn over the builder, so all four panes get the height they need. The way back is the arrow at the top left of the builder's own bar, beside the level name, which returns you to the level list.

The field library offers twelve field types: Text, Textarea, Dropdown, Checkboxes, Radio Buttons, Date, File Upload, Number, Email, Phone, Address and Identity Verification. Each placed field gets its own editor panel for label, help text, whether it is required, validation and — for the choice types — its options.

Five presets seed a level with a sensible field set rather than a blank page: Basic Verification (Tier 1), Identity Verification (Tier 2), Address Verification (Tier 3), Financial Verification (Tier 4) and Trading Experience (Tier 5). They are starting points; edit freely afterwards.

The Features panel is the other half of the builder, and it is described below.

Saving a level looks up every approved application pointing at it and drops that customer's cached profile, so the change is live for them immediately. That is the behaviour you want — but it means a level edit is not a quiet change on a busy platform.

The feature catalogue

The features array on a level is a list of feature ids. The catalogue is defined twice, deliberately, and the two halves must agree: KYC_FEATURES in backend/src/utils/kyc.ts is the server's copy, and platformFeatures in the level builder is the admin's. An id that exists in only one of them fails open — the membership test is simply never true — which is the worst direction for a gate.

There are 51 feature ids, in fifteen groups. The builder's left rail carries sixteen buttons: the fifteen groups below plus All Features at the top, which is a filter rather than a group of its own. Its search box matches on name and description.

Group Feature ids
Trading trade, binary_trading, view_forex, deposit_forex, withdraw_forex, trade_forex, create_forex_account, futures_trading
Wallet view_wallets, deposit_wallet, withdraw_wallet, transfer_wallets, api_keys
Content author_blog, comment_blog
E-commerce view_ecommerce, order_ecommerce
Investment invest_forex, invest_general, invest_ai
ICO view_ico, purchase_ico, create_ico
P2P / affiliate affiliate_mlm, withdraw_affiliate, make_p2p_offer, buy_p2p_offer
Staking view_staking, invest_staking, withdraw_staking
Support ask_faq, support_ticket
NFT view_nft, create_nft, buy_nft, sell_nft, transfer_nft, deploy_nft_contract
Gateway view_gateway, use_gateway
Copy trading view_copy_trading, copy_traders, become_trader
Trading bot view_trading_bot, trade_bot_live, buy_bot_strategy, become_bot_seller
Hummingbot view_hb
Swap (DEX) view_dex, swap_dex, swap_direct

Each feature carries a recommended level (1 to 4) that the builder shows as an L2+ badge and counts in the footer as "enabled above recommended level". It is advice, not enforcement — the switch is yours.

Three pairs are worth deciding deliberately rather than in bulk:

  • view_* and the acting verb are separate on purpose. view_dex lets a customer see routes and prices; swap_dex lets them execute. Same for staking, ICO, NFT, copy trading and the bots.
  • swap_direct is a third id, not a duplicate of swap_dex. On an operator-seeded liquidity pool you are the counterparty rather than an interface, which is exactly the thing you might want a higher level for.
  • withdraw_staking must never sit above invest_staking. Staked principal is locked until the end date and this is the only exit; setting the exit higher than the entrance traps customers' money.

Features are cumulative across approved levels. A customer's effective feature set is the union of the features on every level they hold an approved application for, and their effective level number is the highest of them. A customer does not lose a Tier 1 feature by being approved at Tier 2.

What makes the feature switches real

Two settings must both be on. Neither is redundant.

Setting key Where Default
kycStatus System → Platform Settings → Features → KYC Verification on
kycFeatureEnforcement System → Platform Settings → Features → Enforce KYC Feature Access off

kycStatus is already on out of the box, so on a fresh install the second switch is the only one you have to find. Turning kycStatus off is the way to take KYC out of the platform entirely: with it off there are no levels for anyone to hold, so the enforcement check returns false without even reading kycFeatureEnforcement.

With kycFeatureEnforcement off, the non-throwing check used by read routes returns true for everybody, and the throwing gate returns without looking at anything. Every switch you set in the level builder does precisely nothing.

kycFeatureEnforcement defaults off on purpose. The switches existed in the admin UI long before anything on the server read them, so every live platform has levels whose feature lists were never curated against real enforcement — turning it on by default would have revoked access from working customers on deploy.

Once kycFeatureEnforcement is on, a customer whose approved levels do not list a feature is refused that action — including customers who used it yesterday. Open the level builder and review every level, at every tier, before you flip it. Both switches are Super-Admin-only saves.

Two behaviours are worth knowing because they decide what happens when something is broken:

  • A settings read error fails open. A cache hiccup must not stand between a customer and their own money.
  • An unreadable KYC table fails closed. If enforcement is on and the applications cannot be loaded, the customer is treated as unverified. You asked for verification; silently skipping it is the worse of the two failures.

Newer addons (the Swap/DEX gate is the reference) use a legacy-aware form of the check: while feature enforcement is off they still require any approved application, and hand over to the level builder's switch the moment you turn enforcement on. So switching enforcement on does not loosen anything.

The application queue

/admin/crm/kyc/application opens on the PENDING tab, sorted oldest first — the applicant who has waited longest is the one to review next. The tabs are the status filter: Pending, Additional Info Required, Approved, Rejected. There is also a level filter, a manual/service filter and a search box that matches application id, applicant name, applicant email and level name, all of which run server-side.

The KPI cards above the queue come from GET /api/admin/crm/kyc/application/analytics and take the same scope parameters as the list (levelId, verification, search), so the figures always describe the population you are looking at: total, pending, approved, rejected, additional info required, completion rate and average processing time.

Opening an application gives four tabs — Details (the submitted form and its uploaded documents), Verify (the verification service, if the level has one), User (the applicant) and Tips.

The Verification Levels screen has its own analytics at GET /api/admin/crm/kyc/level/analytics: total users, verified, pending, rejected, and a per-level completion rate with the number of approved customers, sorted by level.

One decision path

Every status change — yours from this screen, or one written by a verification service — goes through a single function, applyKycDecision. That is what makes the two routes behave identically, and it is what you can rely on having happened after a decision:

  1. The status is set to one of PENDING, APPROVED, REJECTED or ADDITIONAL_INFO_REQUIRED, and your admin notes are saved with it. Notes are sanitised and capped at 5000 characters.

  2. reviewedAt is stamped with the moment of the decision. This is what the average-processing-time figure is computed from.

  3. The applicant's cached profile is dropped, but only when the status actually changed. Feature access is read from that cache, so an approval that skipped this step would leave the customer locked out of what you had just granted.

  4. The applicant is emailedKycApproved, KycRejected or KycUpdate for "additional info required". A PENDING decision sends nothing. A mail failure is logged and does not fail the decision.

  5. An entry is written to the applicant's own activity feedkyc.approved, kyc.rejected or kyc.updated, naming the level and, when a service decided, saying so. Again, only on a real change: editing your notes without changing the status does not notify anybody.

Editing notes on a settled application is therefore safe. Changing the status is not — the customer hears about it immediately.

Verification services

Lists the configured verification services

Three services are seeded on a fresh install: SumSub (sumsub-1, a global identity verification platform), DeepSeek (deepseek-1) and Gemini (gemini-1.5-pro) — the last two being AI document verification. A level points at one through its serviceId.

Their credentials live in the environment, not in the database, which is why there are two separate checks: one that reads your .env and one that talks to the vendor.

Reports which required environment variables are missing

Returns success and a list of missingEnvVars. It never leaves the box.

Service Environment variables
SumSub SUMSUB_API_KEY, SUMSUB_API_SECRET
Gemini GEMINI_API_KEY
DeepSeek DEEPSEEK_API_KEY
Makes a real call to the vendor and reports whether it worked

This is the one that proves the integration. It signs and sends a real request — a /resources/checks call for SumSub, a minimal generation call for Gemini and DeepSeek — and returns connected plus a message that distinguishes a missing credential from a rejected one, a rate limit and an outage.

Run check-env first, check-connection second. The env check tells you what you forgot to set; the connection check tells you whether what you set is right.

Runs a service against one application

The body takes applicationId and an optional applyDecision.

applyDecision defaults to false. The service's verdict is written to kyc_verification_result — status, score, structured checks and the document verifications — and the application keeps its current status so a human can read the verdict first.

Send applyDecision: true and the verdict is applied through the same path as a manual review: VERIFIED becomes APPROVED, FAILED becomes REJECTED, anything else is left PENDING. The applicant is emailed and their activity feed records that a machine decided, naming the service.

Reads the recorded verification results

This is where those recorded verdicts are read back.

Deleting a level

Deletes one level
Deletes several levels

kyc_level is not soft-deleted — the row is gone. Applications are associated to it with ON DELETE CASCADE, so every application at that level, including approved ones, is deleted with it. There is no confirmation beyond the dialog and no undo, and neither endpoint refuses a level that is in use.

Set the level to INACTIVE instead. That takes it out of circulation while the approvals already granted keep their evidence.

If the cascade is missing — which happens after a schema re-sync — the applications survive but are orphaned from a level row that no longer exists. The platform tolerates this deliberately: an approved application with no level is still counted as approved, and its level number and features are recovered from what the application itself carries, rather than turning a verified customer into an unverified one. That is a repair behaviour, not a plan. Do not rely on it.

  • Settings reference — the Features tab, where both KYC switches live.
  • Roles and permissions — how edit.kyc.application and the rest are granted, and why a change needs a backend restart.
  • The admin panel — the Operations inbox, where the Verification queue carries a 7-day target.