← Code knowledge base
Reviewed Flutter implementation · 2026-10-27

kyc_api.dart

mobile_flutter/lib/features/kyc/data/kyc_api.dart

Immutable source snapshot based on revision de78e63ed65c. Original line numbers are retained. This is implementation evidence; check the public API catalogue before adapting it.

The mobile /api/v1/mobile routes belong to the example backend. Company credentials stay on that backend. Some sections outside the documented product scope may be omitted.

Lines 1–305

import 'package:dio/dio.dart'; class SumsubToken {  const SumsubToken({required this.token, this.expiresAt});   factory SumsubToken.fromJson(Map<String, dynamic> json) {    final token = json['token'] ??        json['Token'] ??        json['accessToken'] ??        json['AccessToken'];    if (token is! String || token.isEmpty) {      throw const FormatException('KYC token response did not include a token');    }     return SumsubToken(      token: token,      expiresAt: (json['expiresAt'] ?? json['ExpiresAt']) as String?,    );  }   final String token;  final String? expiresAt;} class OccupationCode {  const OccupationCode({    required this.value,    required this.title,    this.majorGroup,    this.minorGroup,  });   factory OccupationCode.fromJson(Map<String, dynamic> json) {    final value =        (json['value'] ?? json['Value'] ?? json['code'] ?? json['Code'])                ?.toString() ??            '';    final title = (json['title'] ??                json['Title'] ??                json['name'] ??                json['Name'] ??                json['label'] ??                json['Label'])            ?.toString() ??        value;     return OccupationCode(      value: value,      title: title,      majorGroup: (json['majorGroup'] ?? json['MajorGroup'])?.toString(),      minorGroup: (json['minorGroup'] ?? json['MinorGroup'])?.toString(),    );  }   final String value;  final String title;  final String? majorGroup;  final String? minorGroup;} class KycApi {  const KycApi(this._dio);   final Dio _dio;   Future<Uri> resumeVerification() async {    final response = await _dio.post<dynamic>('/api/v1/mobile/kyc/resume');    final uri = hostedKycUrlFromJson(response.data);    if (uri == null || uri.scheme != 'https') {      throw const FormatException(          'A secure verification link was not returned.');    }    return uri;  }   Future<SumsubToken> createSumsubToken({    required Map<String, Object?> applicantProfile,  }) async {    final response = await _dio.post<Map<String, dynamic>>(      '/api/v1/mobile/kyc/sumsub-token',      data: applicantProfile,    );     final data = response.data;    if (data == null) {      throw const FormatException('KYC token response was empty');    }     return SumsubToken.fromJson(data);  }   /// Hosted Sumsub verification page issued by Hoppa  /// (`/users/{id}/sumsub/kyc-url`). Returns null when the provider does not  /// offer one, so callers can fall back to the embedded SDK.  Future<Uri?> createHostedKycUrl({    required Map<String, Object?> applicantProfile,  }) async {    final response = await _dio.post<dynamic>(      '/api/v1/mobile/kyc/hosted-url',      data: applicantProfile,    );    return hostedKycUrlFromJson(response.data);  }   Future<void> startOnboarding() async {    await _dio.post<Map<String, dynamic>>(      '/api/v1/mobile/onboarding/start',      data: const {        'customerType': 'individual',        'productCode': 'neobanking',      },    );  }   Future<void> markKycSubmitted() async {    await _dio.patch<Map<String, dynamic>>(      '/api/v1/mobile/onboarding/steps',      data: const {        'step': 'kyc_submitted',        'status': 'submitted',        'notes': 'SumSub verification session was closed by the user.',      },    );  }   Future<List<OccupationCode>> getOccupationCodes() async {    late final Response<dynamic> response;    try {      response = await _dio.get<dynamic>(        '/api/v1/mobile/kyc/occupation-codes',      );    } on DioException catch (error) {      if (!_canUseFallbackOccupationCodes(error)) {        rethrow;      }       return fallbackOccupationCodes;    }     final payload = response.data;    final list = _extractOccupationList(payload);     final occupations = list        .map((item) {          if (item is Map<String, dynamic>) {            return OccupationCode.fromJson(item);          }          if (item is Map) {            return OccupationCode.fromJson(              item.map((key, value) => MapEntry(key.toString(), value)),            );          }          if (item is String && item.trim().isNotEmpty) {            return OccupationCode(value: item.trim(), title: item.trim());          }           return const OccupationCode(value: '', title: '');        })        .where((occupation) => occupation.value.isNotEmpty)        .toList(growable: false);     return occupations.isEmpty ? fallbackOccupationCodes : occupations;  }} List<dynamic> _extractOccupationList(dynamic payload) {  if (payload is List) {    return payload;  }   if (payload is Map) {    final normalized =        payload.map((key, value) => MapEntry(key.toString(), value));    for (final key in const [      'occupationCodes',      'OccupationCodes',      'occupations',      'Occupations',      'codes',      'Codes',      'items',      'Items',      'list',      'List',      'data',      'Data',    ]) {      final value = normalized[key];      if (value is List) {        return value;      }      if (value is Map) {        final nested = _extractOccupationList(value);        if (nested.isNotEmpty) {          return nested;        }      }    }     if (_looksLikeCodeTitleMap(normalized)) {      return normalized.entries          .map((entry) => {                'value': entry.key,                'title': entry.value?.toString() ?? entry.key,              })          .toList();    }  }   return const [];} bool _looksLikeCodeTitleMap(Map<String, dynamic> payload) {  if (payload.isEmpty) {    return false;  }   const wrapperKeys = {    'data',    'Data',    'items',    'Items',    'list',    'List',    'occupationCodes',    'OccupationCodes',    'occupations',    'Occupations',    'codes',    'Codes',  };  if (payload.keys.any(wrapperKeys.contains)) {    return false;  }   return payload.values.every((value) => value is String || value is num);} bool _canUseFallbackOccupationCodes(DioException error) {  final status = error.response?.statusCode;  return status == 400 || status == 404 || status == 409;} const fallbackOccupationCodes = [  OccupationCode(value: '2611', title: 'Software and applications developers'),  OccupationCode(value: '2411', title: 'Accountants'),  OccupationCode(value: '2421', title: 'Management and organisation analysts'),  OccupationCode(      value: '2431', title: 'Advertising and marketing professionals'),  OccupationCode(value: '3312', title: 'Credit and loans officers'),  OccupationCode(value: '4110', title: 'General office clerks'),  OccupationCode(value: '5223', title: 'Shop sales assistants'),  OccupationCode(value: '5322', title: 'Home-based personal care workers'),  OccupationCode(value: '7115', title: 'Carpenters and joiners'),  OccupationCode(value: '9999', title: 'Other occupation'),]; const _hostedUrlKeys = {  // Hoppa's /users/{id}/sumsub/kyc-url returns the Sumsub link in  // `accessToken` (https://in.sumsub.com/websdk/p/...).  'accesstoken',  'token',  'url',  'kycurl',  'link',  'permalink',  'websdklink',  'verificationurl',  'hostedurl',  'redirecturl',  'sdklink',}; /// Finds the verification link in a provider response regardless of the/// key it was returned under (also looks inside nested `data`/`result`).Uri? hostedKycUrlFromJson(Object? json) {  if (json is String) {    final uri = Uri.tryParse(json.trim());    return uri != null && uri.hasScheme && uri.host.isNotEmpty ? uri : null;  }  if (json is! Map) return null;  if (json['success'] == false) {    final reason = (json['errorMessage'] ?? json['message'] ?? '').toString();    throw StateError(      reason.trim().isEmpty          ? 'The verification link could not be created. Try again shortly.'          : reason,    );  }  for (final entry in json.entries) {    final key = entry.key.toString().toLowerCase();    if (_hostedUrlKeys.contains(key)) {      final found = hostedKycUrlFromJson(entry.value);      if (found != null) return found;    }  }  for (final key in const ['data', 'Data', 'result', 'Result', 'payload']) {    final nested = json[key];    if (nested is Map || nested is String) {      final found = hostedKycUrlFromJson(nested);      if (found != null) return found;    }  }  return null;}
HOPPA DEVELOPER ASSISTANT

What are you building?

From a question to an implementation.

Get a step-by-step flow, example code and the docs behind it. Choose a starting point or describe your feature.

Answers use the public API guide and example app code. Check the linked reference before shipping. Explore code sources ↗

Your question and recent conversation are sent to our AI provider. Don’t include API keys, personal data or card details.