Skip to content

Swap the KYC vendor

Goal: replace Sumsub with a different KYC / identity-verification vendor (Onfido, Veriff, Jumio, etc.). Audience: customer engineering team mandated to use a specific KYC provider for compliance. Time: 1–2 weeks code + 2–4 weeks compliance / legal sign-off. Friction: very high — the current KYC abstraction is vendor-specific, not strategy-pattern. Plan to rewrite, not swap.

What you'll change

Layer Path Action
KYC module apps/api/src/kyc/ Significant rewrite. Replace sumsub/ subfolder with <vendor>/ and adapt the service calls.
KYC controllers apps/api/src/kyc/controller/{kyc,kyc-webhook,admin.kyc}.controller.ts Adapt webhook signature + payload to vendor spec.
KYC repository apps/api/src/kyc/kyc.repository.ts Likely unchanged (state machine is vendor-agnostic).
Prisma schema libs/_prisma/src/schema/api.prisma (search KycLevel, KycRejectType, KycVerificationVerdict) Verify enums cover new vendor's verdicts. Migration if not.
Doppler .example.env SUMSUB_* keys Replace with new vendor keys.
Frontend KYC widget ebit-fe/src/... (search Sumsub) Replace iframe / SDK with vendor's.
Compliance (legal) Re-verify customer's KYC obligations against the new vendor's coverage.

Canonical references

Verified file inventory (2026-04-25):

apps/api/src/kyc/
├── kyc.module.ts
├── kyc.service.ts                  # high-level orchestration
├── kyc-crud.service.ts             # DB-only operations
├── kyc.repository.ts               # Prisma calls
├── const.ts                        # cache keys, TTLs, webhook key
├── controller/
│   ├── kyc.controller.ts           # user-facing endpoints
│   ├── kyc-webhook.controller.ts   # provider callbacks
│   └── admin.kyc.controller.ts     # backoffice review
├── dto/
├── sumsub/
│   ├── sumsub.service.ts           # outbound Sumsub API + webhook signature verify
│   ├── sumsub.dto.ts
│   └── utils.ts
└── utils.ts

Read sumsub.service.ts first — every public method is a Sumsub-namespaced call, not an interface implementation. That's the friction.

Doppler keys (per .example.env):

SUMSUB_APP_TOKEN=token
SUMSUB_SECRET_KEY=key
SUMSUB_WEBHOOK_SECRET_KEY=key

Steps

1. Compliance pre-flight [non-engineering]

Before any code:

  • [ ] Confirm the new vendor is licensed for every jurisdiction the customer operates in.
  • [ ] Confirm the vendor's KYC levels align with our internal state machine (KycLevel: BASIC, FULL, ENHANCED, etc. — check Prisma enum).
  • [ ] Confirm the vendor exposes the same verdict types we use (approved, rejected_temporary, rejected_permanent, pending, re-verify). If not, agree on the mapping.
  • [ ] Get sign-off from the customer's legal / compliance officer in writing.
  • [ ] Confirm document retention policy alignment (vendor's data-retention vs the customer's legal requirements).

2. Decide: refactor or rewrite

Two paths:

Option A — direct replacement (faster, similar shape). Copy kyc/sumsub/ to kyc/<vendor>/, replace the API calls, swap webhook signature verification. Update KycService to inject the new module instead of SumsubService. Plan: 1–2 weeks. No interface introduced.

Option B — strategy pattern (slower, future-proof). Introduce KycProviderInterface + an @Inject('KYC_PROVIDER') token. Implement two adapters (Sumsub, new vendor) gated by env (KYC_PROVIDER_NAME). Plan: 3–4 weeks. Useful if the customer expects to rotate KYC vendors. Coordinate with the platform team — this changes the central abstraction.

Most customers want Option A. Pick Option B only with explicit platform-team buy-in. The rest of this recipe assumes Option A.

3. Set up vendor sandbox creds

Add to .example.env:

# Replace the SUMSUB_* block with:
KYC_PROVIDER_NAME="yourvendor"
YOURVENDOR_API_URL="https://sandbox.yourvendor.example/v1"
YOURVENDOR_API_KEY=""
YOURVENDOR_WEBHOOK_SECRET=""

Set the real values in Doppler. Keep SUMSUB_* available as fallback during the cutover.

4. Scaffold the new vendor module

cp -r apps/api/src/kyc/sumsub apps/api/src/kyc/yourvendor
# rename: yourvendor.service.ts, yourvendor.dto.ts, utils.ts

Adapt yourvendor.service.ts:

  • Replace the constructor's SUMSUB_* config reads with YOURVENDOR_*.
  • Replace each outbound method (generateAccessToken, getApplicationStatus, etc.) with the new vendor's API path + payload.
  • Replace verifyWebhookSignature with the new vendor's signature scheme. Constant-time comparison only, per docs/security-register.md.
  • Map the new vendor's verdicts onto our KycVerificationVerdict enum.

5. Update the webhook controller

Edit apps/api/src/kyc/controller/kyc-webhook.controller.ts:

  • Change the webhook path if needed (Sumsub's path is fixed; switching vendor likely means a new path on the customer's side too).
  • Inject YourVendorService instead of SumsubService (or, in Option B, the strategy token).

The webhook handler must:

  1. Verify signature.
  2. Map vendor verdict → internal verdict via KycRepository.
  3. Update User.kycLevel.
  4. Emit any downstream events (notification, admin alert) that Sumsub currently emits.

Document any divergence in the verdict mapping in docs/api/changelog.md.

6. Frontend integration

Search ebit-fe/src/ for sumsub (case-insensitive). Typical hits:

  • A KYC widget loader (Sumsub's WebSDK).
  • A "Start verification" CTA that fetches an access token from the backend.

Replace with the new vendor's SDK loader. The new vendor likely supplies an embeddable iframe or JS SDK — follow their docs.

The backend access-token endpoint stays the same path (POST /kyc/access-token or similar — verify in docs/api-reference/api.md "KYC API"); only the response payload type changes.

7. Wire the module

Edit apps/api/src/kyc/kyc.module.ts:

  • Remove (or keep as fallback) the Sumsub module import.
  • Import YourVendorModule.
  • Update KycService injection.

If you went with Option B, register the strategy token here:

{
  provide: 'KYC_PROVIDER',
  useFactory: (config: ConfigService) =>
    config.get('KYC_PROVIDER_NAME') === 'yourvendor' ? YourVendorService : SumsubService,
  inject: [ConfigService],
}

8. Migration of in-flight applications [non-engineering]

Existing users with a Sumsub application id cannot magically appear in the new vendor's system. Decide:

  • Hard cutover — new applications use the new vendor, old applications stay tracked under Sumsub but cannot progress (acceptable if Sumsub remains operational for read-only).
  • Forced re-verification — every user re-submits documents to the new vendor. Communicate to users; estimate churn.
  • Bulk export / import — request the new vendor support an import. Rare; expensive.

Hard cutover is the default. Document the decision in docs/api/changelog.md and brief support.

9. Tests

npm test -- apps/api/src/kyc/

Required:

  1. Vendor signature verification: positive + negative.
  2. Verdict mapping: vendor verdict → internal verdict for every recognised verdict.
  3. Idempotent webhook (replaying the same callback does not double-update).
  4. Access-token endpoint returns the new vendor's payload shape.

10. Update the API surface SOT

./docs/api/sync-postman.sh

Append docs/api/changelog.md entries:

  • "Modified: POST /kyc/access-token response shape changed from Sumsub's to vendor's."
  • "Modified: webhook payload."
  • "Migration: in-flight Sumsub applications → policy decision (see step 8)."

This is potentially a breaking API change for any customer-side code that depends on the access-token shape. Communicate accordingly.

Verification

  1. Sandbox flow: sign up a fresh user, trigger KYC submission, watch the new vendor's webhook hit /kyc/webhook, see User.kycLevel advance.
  2. Negative test: mutate the webhook payload — signature verify rejects with 401.
  3. Replay: send the same approval webhook twice — User.kycLevel advances once.
  4. Old user: an existing Sumsub user (pre-cutover) sees the new vendor's flow on next login (or the messaging chosen in step 8).
  5. Loki: search by user_id — the full KYC handler chain is logged.

Notes

  • The current kyc.repository.ts is vendor-agnostic; you should not need to touch it (the schema lives in Prisma, not in the repository).
  • The Sumsub WaitMutex pattern (in sumsub.service.ts) for de-duplicating concurrent token requests is a per-vendor concern — re-implement only if the new vendor has rate limits that warrant it.
  • Don't delete the Sumsub module immediately; keep it for one release behind a feature flag in case rollback is needed.