第 09 課 · Lesson 09

KYC 與 eKYC | KYC & eKYC

HK AML/CFT 責任;ID vs 地址證明;OCR、liveness;制裁 / PEP;四眼審批。

Plays in the sticky player at the bottom of the page

課堂筆記

學習目標 · Learning Objectives

  1. State the Hong Kong legal and regulatory basis for KYC in insurance: the Anti-Money Laundering and Counter-Terrorist Financing Ordinance (打擊洗錢及恐怖分子資金籌集條例, Cap. 615) and its subsidiary regulation, the FATF standards it implements, the Insurance Authority's supervisory expectations for authorised institutions, and what an individual agent actually has to do differently because of it.
  2. Distinguish identity verification (who is this person: HKID, passport, travel permit, plus a genuine physical presence check) from address verification (where do they live: a document, its recency, and the rule about accepting third-party addresses), know why an ID alone is never sufficient for an insurance application, and explain how document OCR (optical character recognition), liveness detection (活體檢測), and matching work in an eKYC flow — the failure modes of each, and which of them an agent may never override at the counter.
  3. Run sanctions and PEP (politically exposed person) screening to a defensible outcome: interpret a match, apply the four-eyes (雙人覆核) escalation rule, keep records to the required retention period, and write the record in a shape that survives an IA examination.

KYC & eKYC (KYC 與 eKYC)

Lesson 09 · Insurance POS 101 · Hong Kong market · agent-facing

Learning Objectives

  • State the Hong Kong legal and regulatory basis for KYC in insurance: the Anti-Money Laundering and Counter-Terrorist Financing Ordinance (打擊洗錢及恐怖分子資金籌集條例, Cap. 615) and its subsidiary regulation, the FATF standards it implements, the Insurance Authority's supervisory expectations for authorised institutions, and what an individual agent actually has to do differently because of it.
  • Distinguish identity verification (who is this person: HKID, passport, travel permit, plus a genuine physical presence check) from address verification (where do they live: a document, its recency, and the rule about accepting third-party addresses), know why an ID alone is never sufficient for an insurance application, and explain how document OCR (optical character recognition), liveness detection (活體檢測), and matching work in an eKYC flow — the failure modes of each, and which of them an agent may never override at the counter.
  • Run sanctions and PEP (politically exposed person) screening to a defensible outcome: interpret a match, apply the four-eyes (雙人覆核) escalation rule, keep records to the required retention period, and write the record in a shape that survives an IA examination.

The Legal Basis in Hong Kong

In practice: when a client asks "why do you want a bank statement?", the answer is not politeness or bank secrecy — it is a statutory obligation under Cap. 615, and the agent who cannot name the obligation is in a weak position when the client pushes back.

The instruments

InstrumentWhat it isWhat it obliges an insurer/agent to do
AMLO (Cap. 615) — Anti-Money Laundering and Counter-Terrorist Financing OrdinanceThe primary AML statute in Hong KongCustomer due diligence (CDD) on establishing a relationship; ongoing due diligence; enhanced due diligence where risk warrants; suspicious transaction reporting to the JFIU
AMLO (General) Regulation (Cap. 615, subleg)Implements FATF 40 recommendations into HK lawCustomer identification, verification of identity, beneficial owner identification, risk classification, record-keeping periods, agent/introducer requirements
Guidance on AML/CFT for Authorized Institutions (HKMA) / Guidance on Anti-Money Laundering and Counter-Terrorist Financing for Insurance Companies (IA)Sector supervisory guidanceHow an insurer must operationalise CDD, how long records are kept, when EDD is triggered, what the MLRO (money laundering reporting officer) must approve
FATF RecommendationsThe international standard, risk-basedRisk-based approach: CDD depth scales with customer risk; higher risk → stronger verification
Personal Data (Privacy) Ordinance (Cap. 486)HK privacy lawCollection must be necessary, proportionate, transparent; client has data-access and correction rights; cross-border transfer constraints
Unconscionable Contracts Ordinance / general agency dutyContract fairnessA term or exclusion that is unconscionable given the parties is not enforced; relevant to how KYC information may be used
Insurance Authority Best Practice Guide seriesOperational compliance guidanceAgent conduct, disclosure, illustrations, record-keeping expectations; the practical layer an agent actually feels

The chain an agent should be able to state out loud, without notes: we are required by the AMLO, which implements FATF, supervised by the Insurance Authority, to identify you, verify who you are and where you live, understand the source of the money paying for the policy, screen you against sanctions and PEP lists, keep records for five years after the relationship ends or the policy ends — whichever is later — and report anything suspicious to the JFIU without telling you.

The two facts that surprise agents

1. Money laundering in insurance is mostly about getting money out, not putting it in. The classic predicate in HK insurance is an overpayment: a client pays HK$2m of premium into a policy they intend to surrender within months, and the money goes out to a beneficiary or a third party while a small proportion is retained as commission. The AML controls that catch this are (a) refusing third-party payment, (b) requiring the payment account to be in the proposer's own name, (c) watching for rapid surrender after inception, and (d) trigger reporting on refund requests. The POS therefore treats source of funds and payment account name as first-class fields with hard validation, not nice-to-have boxes.

2. Insurance agents are a recognised weak point, and the regime is built to close that gap. Under the AMLO's approach, where a business relationship is established through an intermediary, the insurer can rely on the intermediary having applied CDD, but it must instruct and monitor the intermediary and must obtain the CDD information. Practically this becomes: the agent must gather the information, the agent must not gather it sloppily, and the insurer's compliance function must be able to reconstruct the agent's steps years later. That is why the POS stores a StateTransition-style trail (Lesson 08) with actor and timestamp for every KYC step, and stores the raw evidence with a hash.

What an agent personally must do differently

ObligationThe agent's concrete behaviour
Don't tip off the client about suspicious transaction reportingNever say "this looks like AML" or "I have to report you"; complete the transaction or stop it neutrally
Don't accept a third-party paymentIf the payer is not the proposer, stop and escalate; do not "just accept it this once"
Keep the client's documents safeScanned ID stored in HK-resident storage, masked in the UI, never in a personal WhatsApp, never on a personal laptop
Record the checks, not just the resultA tick-box saying "KYC done" is not a record; the record is the document, its hash, the source, the matcher version, and who reviewed it
Know when to stopAn agent who is uneasy can and must refuse to proceed and refer to the MLRO; that is a protected act, not a career limitation
Don't open the account for a friend, relative, or "network"Beneficial-ownership awkwardness is exactly what EDD is for; the agent may not be the customer's nominee or accountant on the same case

ID Verification vs Address Proof

In practice: the client slides their HKID across the table and says "is that enough?" — and the honest answer is "that tells me who you are; I still need to see where you live, because the application form asks for your residential address and I have to be able to prove it."

The two questions, and why they are separate

Identity verification (身分核實)Address verification (地址證明)
Question answeredWho is this person?Where do they live?
Acceptable evidence (typical HK agency)HKID card (original, both sides); HKID replacement; passport; Mainland Travel Permit; HKID Application Receipt for those without a cardBank statement; utility bill (水電費單); tenancy agreement (租約); government correspondence addressed to them; employer letter
Recency ruleMust be valid on the day of application (not expired)Commonly must be within 3 months; some institutions accept 6 months for a lower-risk case
Technical checkHKID number format + check digit; name in Latin/Chinese; DOB consistency; document authenticity; liveness if eKYCName and address on the document match the application; document is genuine; address is a residential address, not a PO box
Third-party addressNot applicableAllowed only with a documented reason (e.g. living with parents, in a dormitory, in a company-provided flat). A written statement from the client plus the third party's consent, and the third party's address as the mailing address, and often an extra confirmation call to the third party
Common failureExpired HKID; a photocopy; a photo of a photoA bank statement of an account the client does not hold; a mobile-phone bill (many carriers will not accept it alone); an old statement; an address on the ID card that is years out of date

A Hong Kong ID card shows an address, but it is often the address as at issue of the card, not the current one, and it is not a "document" issued for the purpose of proving residence. Treating the ID address as proof of residence is the most common KYC shortcut and it is the one the regulator looks for.

The decision logic, as code

export type IdDocumentKind =
  | "HKID_CARD" | "HKID_REPLACEMENT" | "HKID_APPLICATION_RECEIPT"
  | "PASSPORT" | "MAINLAND_TRAVEL_PERMIT" | "BIRTH_CERTIFICATE_HK"
  | "OTHER_ACCEPTED";

export interface IdentityEvidence {
  kind: IdDocumentKind;
  frontImageSha256: string;
  backImageSha256?: string;
  issuedCountry: string;                 // ISO-3166 alpha-3; "HKG" for HKID
  documentNumberMasked: string;          // e.g. "A123456(7)" → store masked + encrypted original
  nameZh: string;
  nameEn: string;
  dateOfBirth: string;                   // ISO
  addressOnDocument: string;
  validFrom?: string;
  validUntil?: string;
  hkidChecksumValid?: boolean;           // only for HKID kinds
  authenticityScore?: number;            // 0..1 from the document vendor
  tamperScore?: number;                  // 0..1, higher = more likely a photo of a photo
  capturedVia: "IN_PERSON_SCAN" | "UPLOAD" | "MOBILE_CAMERA" | "NFC_CHIP_READ";
}

export interface AddressEvidence {
  kind:
    | "BANK_STATEMENT" | "UTILITY_BILL" | "TENANCY_AGREEMENT"
    | "GOVERNMENT_CORRESPONDENCE" | "EMPLOYER_LETTER" | "THIRD_PARTY_STATEMENT";
  documentSha256: string;
  statementDate: string;
  nameOnDocument: string;
  addressOnDocument: string;
  monthsOld: number;
  issuingInstitution?: string;
  isResidential: boolean;
  isDomicileAddress?: boolean;
  thirdParty?: {
    reason: "LIVING_WITH_PARENT" | "DORMITORY" | "EMPLOYER_HOUSING" | "RELOCATION_PENDING" | "OTHER";
    thirdPartyName: string;
    thirdPartyConsentObtained: boolean;
    thirdPartyConfirmCallMade: boolean;
    mailingAddressOverride: string;      // where mail actually goes
  };
}

export type KycOutcome =
  | { outcome: "PASS"; confidence: number }
  | { outcome: "FAIL"; reasonCode: KycFailCode; reason: string; clientMessageZh: string }
  | { outcome: "ESCALATE"; reasonCode: string; reason: string; to: "MLRO" | "COMPLIANCE_OFFICER" };

export type KycFailCode =
  | "ID_EXPIRED" | "ID_CHECKSUM_FAIL" | "ID_AUTHENTICITY_LOW" | "ID_TAMPER_SUSPECTED"
  | "NAME_MISMATCH_ID_VS_APPLICATION" | "DOB_MISMATCH"
  | "ADDRESS_PROOF_MISSING" | "ADDRESS_PROOF_STALE" | "ADDRESS_NOT_RESIDENTIAL"
  | "ADDRESS_THIRD_PARTY_UNSUBSTANTIATED" | "LIVENESS_FAILED" | "FACE_MATCH_LOW"
  | "SCREENING_PEP_POTENTIAL" | "SCREENING_SANCTIONS_POTENTIAL" | "SOURCE_OF_FUNDS_UNCLEAR";

/**
 * Two-stage KYC. Identity and address are evaluated INDEPENDENTLY —
 * a perfect ID does not compensate for a missing address proof, and a good
 * address proof does not compensate for a failed liveness check.
 */
export function verifyIdentityAndAddress(
  id: IdentityEvidence,
  addr: AddressEvidence | null,
  live: LivenessResult,
  faceMatch: FaceMatchResult,
  declared: { nameEn: string; nameZh: string; dateOfBirth: string; address: string },
  ctx: { now: Date; maxAddressMonths: number; thirdPartyAllowed: boolean },
): KycOutcome {
  /* ---------------- IDENTITY ---------------- */
  const now = ctx.now;
  if (id.kind === "HKID_CARD" || id.kind === "HKID_REPLACEMENT") {
    if (id.validUntil && new Date(id.validUntil) < now) {
      return { outcome: "FAIL", reasonCode: "ID_EXPIRED",
        reason: `Identity document expired on ${id.validUntil}`,
        clientMessageZh: "你出示的身份證明文件已過期,請提供有效的證件。" };
    }
    if (id.hkidChecksumValid === false) {
      // Never auto-correct. Ask the client to re-read from the card.
      return { outcome: "FAIL", reasonCode: "ID_CHECKSUM_FAIL",
        reason: "HKID check digit does not validate; the number as typed is not a valid HKID number",
        clientMessageZh: "身份證號碼輸入不正確,請再核對身份證上的號碼。" };
    }
  }
  if ((id.authenticityScore ?? 1) < 0.6) {
    return { outcome: "FAIL", reasonCode: "ID_AUTHENTICITY_LOW",
      reason: `Document authenticity score ${id.authenticityScore} below threshold`,
      clientMessageZh: "證件鑒真度不足,請於分行或親身到門市重新辦理。" };
  }
  if ((id.tamperScore ?? 0) > 0.7) {
    // A photo of a photo, or an edited image. This is not a "try again" — this escalates.
    return { outcome: "ESCALATE", reasonCode: "ID_TAMPER_SUSPECTED",
      reason: `Tamper score ${id.tamperScore} suggests image manipulation or a screenshot`,
      to: "MLRO" };
  }
  if (normaliseName(id.nameEn) !== normaliseName(declared.nameEn) ||
      normaliseName(id.nameZh) !== normaliseName(declared.nameZh)) {
    return { outcome: "FAIL", reasonCode: "NAME_MISMATCH_ID_VS_APPLICATION",
      reason: `Name on ID (${id.nameEn} / ${id.nameZh}) does not match the application (${declared.nameEn} / ${declared.nameZh})`,
      clientMessageZh: "身份證明文件上的姓名同申請表不符,請確認並更正。" };
  }
  if (id.dateOfBirth !== declared.dateOfBirth) {
    return { outcome: "FAIL", reasonCode: "DOB_MISMATCH",
      reason: `Date of birth on ID (${id.dateOfBirth}) does not match the application (${declared.dateOfBirth})`,
      clientMessageZh: "出生日期同申請表不符,請確認並更正。" };
  }
  if (!live.passed) {
    // Liveness is never overridden by an agent. Not once. Not for a nice client.
    return { outcome: "FAIL", reasonCode: "LIVENESS_FAILED",
      reason: `Liveness check failed: ${live.reason}. An agent may not override liveness.`,
      clientMessageZh: "活體檢測未能通過,請親臨門市或分行辦理身份核實。" };
  }
  if (faceMatch.score < 0.85) {
    return { outcome: "ESCALATE", reasonCode: "FACE_MATCH_LOW",
      reason: `Face match score ${faceMatch.score} below auto-pass threshold; manual review required`,
      to: "COMPLIANCE_OFFICER" };
  }

  /* ---------------- ADDRESS ---------------- */
  if (!addr) {
    return { outcome: "FAIL", reasonCode: "ADDRESS_PROOF_MISSING",
      reason: "No address evidence supplied; an identity document alone is not sufficient",
      clientMessageZh: "請提供住址證明,例如三個月內的銀行月結單或水電費單。" };
  }
  if (addr.monthsOld > ctx.maxAddressMonths) {
    return { outcome: "FAIL", reasonCode: "ADDRESS_PROOF_STALE",
      reason: `Address proof is ${addr.monthsOld} months old; limit is ${ctx.maxAddressMonths}`,
      clientMessageZh: `住址證明已超過 ${ctx.maxAddressMonths} 個月,請提供較近期的文件。` };
  }
  if (!addr.isResidential) {
    return { outcome: "FAIL", reasonCode: "ADDRESS_NOT_RESIDENTIAL",
      reason: "Address evidence shows a PO box or commercial-only address; a residential address is required",
      clientMessageZh: "請提供居住地址的證明,郵政信箱或商業地址不適用。" };
  }
  if (normaliseAddress(addr.addressOnDocument) !== normaliseAddress(declared.address)) {
    return { outcome: "FAIL", reasonCode: "NAME_MISMATCH_ADDRESS" as KycFailCode,
      reason: "Address on the evidence does not match the address declared on the application",
      clientMessageZh: "住址證明上的地址同申請表填寫的不符,請更正。" };
  }
  if (addr.thirdParty) {
    const tp = addr.thirdParty;
    if (!ctx.thirdPartyAllowed) {
      return { outcome: "FAIL", reasonCode: "ADDRESS_THIRD_PARTY_UNSUBSTANTIATED",
        reason: "Third-party address not accepted for this product/channel",
        clientMessageZh: "此產品不接受第三方住址,請提供以你本人名義的住址證明。" };
    }
    if (!tp.thirdPartyConsentObtained || !tp.thirdPartyConfirmCallMade) {
      return { outcome: "FAIL", reasonCode: "ADDRESS_THIRD_PARTY_UNSUBSTANTIATED",
        reason: "Third-party address lacks consent or third-party confirmation",
        clientMessageZh: "使用第三方住址需要對方同意並經核實,請補齊文件。" };
    }
  }

  return { outcome: "PASS", confidence: Math.min(1, (id.authenticityScore ?? 0.7) * 0.5 + faceMatch.score * 0.5) };
}

Document OCR

In practice: the agent's tablet captures the HKID and the OCR fills the client's name, date of birth and ID number into the application in seconds — and the agent's job is to check the four critical fields against the card and never to accept an OCR field the client did not confirm.

What OCR does in this flow

FieldOCR reliabilityWhat the agent must do
HKID numberVery high, but a check-digit error turns a typo into an invalid or wrong identityRead back the number from the card; never let OCR set it silently
Chinese nameHigh on modern fonts, drops on stylized cardsRead back
English nameHighRead back for exactness of given names vs surname order
Date of birthHighRead back
Address on the IDMedium — multi-line, sometimes abbreviatedRead back; but this is not address proof (see above)
Bank statement: name + addressHighRead back the address against the application
Bank statement: balances/transactionsMediumNever auto-accept a figure into a financial-justification field; the client confirms

The engineering rules that make OCR safe to use in a regulated flow:

export interface OcrField {
  path: string;                 // "/parties/1/hkidNumber"
  rawText: string;
  confidence: number;           // 0..1
  bbox: [number, number, number, number];
  sourceDocSha256: string;      // ties the field to the exact image
  engineVersion: string;
  autoAccepted: boolean;        // true only if autoAcceptRules passed
  confirmedBy?: "CLIENT_READBACK" | "AGENT_VISUAL_CHECK" | "CHIP_READ" | null;
}

const AUTO_ACCEPT_RULES: Record<string, (f: OcrField) => boolean> = {
  // ID number: high OCR confidence AND a valid HKID check digit AND read back.
  "/parties/*/hkidNumber": (f) => f.confidence >= 0.95 && hkidCheckDigit(f.rawText) !== null,
  // DOB: high confidence; still read back because a wrong DOB changes rating.
  "/parties/*/dateOfBirth": (f) => f.confidence >= 0.9,
  // Address on ID: NEVER auto-accept into the application's residential address —
  // it is not address proof and it is frequently stale.
  "/parties/*/addressOnDocument": () => false,
  // Bank statement balance: never auto-accept into a financial-justification field.
  "/financials/assets": () => false,
};

export function shouldAutoAccept(path: string, f: OcrField): boolean {
  const rule = AUTO_ACCEPT_RULES[path];
  if (!rule) return false;
  if (!rule(f)) return false;
  // Nothing identity-critical is auto-accepted into a signature-bearing field
  // without a human confirmation event.
  return f.confidence >= 0.9;
}

/** Every OCR value that lands in the application is bound to the image it came from. */
export function buildOcrProvenance(fields: OcrField[]): OcrProvenance {
  return {
    engine: "vendor-ocr/4.2.1",
    capturedAt: new Date().toISOString(),
    fields: fields.map((f) => ({
      ...f,
      // If the source document hash changes, this field is stale and must be re-derived.
      stale: f.sourceDocSha256 !== currentHashFor(f.path),
    })),
  };
}

The stale flag matters more than it looks. The client uploads a bank statement, the OCR pulls the address, then the client re-uploads a corrected or more recent statement — and if the application still carries the address from the first upload, the POS is holding evidence that contradicts itself. Provenance with staleness detection is how that is caught.

Liveness Detection

In practice: when the client is doing the eKYC from home on their phone, the camera asks them to turn their head, blink, and hold the HKID up next to their face — and if the check fails, the client is told to go to a branch or an agent. The agent's job is not to "try again until it passes"; it is to route the failure correctly.

The two checks, and why both exist

CheckWhat it defends againstMechanism
Liveness (活體檢測)Presenting a photo of someone else, a video, a mask, a deepfakeRandom challenge (turn left, blink, smile), passive micro-motions, texture/reflectance analysis, depth cue where available
Face match (人臉比對)The document belongs to someone other than the person in front of the cameraEmbedding comparison of the selfie frame vs the document portrait, with a threshold

A 3D mask attack and a printed-photo attack defeat different things. Liveness alone can be beaten by a good mask in weak conditions; face match alone can be beaten by a photo if the "selfie" is not live. The POS therefore requires both, and treats liveness as non-overridable while face-match has a manual-review band.

export interface LivenessResult {
  passed: boolean;
  method: "PASSIVE" | "ACTIVE_CHALLENGE" | "DEPTH_SENSOR" | "NFC_CHIP_READ";
  challengesCompleted: number;
  spoofScore: number;          // 0..1, higher = more likely spoof
  reasons: string[];            // e.g. "no_blink_in_3s", "flat_texture_print", "replay_same_frame"
  overridePermitted: false;     // type-level: there is no code path that sets this true
}

export interface FaceMatchResult {
  score: number;               // cosine similarity of embeddings
  autoPassThreshold: number;    // e.g. 0.85
  manualBand: [number, number]; // e.g. [0.70, 0.85] ⇒ manual review
  autoFailBelow: number;        // e.g. 0.70
  spoofCorrelation: number;     // if liveness looks odd, treat a high face score with suspicion
}

// A manual override of liveness is not a code path that exists. By typing the
// field as `overridePermitted: false` and never reading it as an if-condition,
// the capability is absent rather than merely discouraged.
export function decideFaceMatch(fm: FaceMatchResult, live: LivenessResult): KycOutcome {
  if (!live.passed) {
    return { outcome: "FAIL", reasonCode: "LIVENESS_FAILED",
      reason: "Liveness failed; no manual override exists in this system",
      clientMessageZh: "活體檢測未通過,請親身到門市或分行辦理。" };
  }
  if (fm.score >= fm.autoPassThreshold) {
    return { outcome: "PASS", confidence: fm.score };
  }
  if (fm.score < fm.autoFailBelow) {
    return { outcome: "ESCALATE", reasonCode: "FACE_MATCH_LOW",
      reason: `Face match ${fm.score} below auto-fail; treat as possible document/face mismatch`,
      to: "COMPLIANCE_OFFICER" };
  }
  return { outcome: "ESCALATE", reasonCode: "FACE_MATCH_REVIEW_BAND",
    reason: `Face match ${fm.score} inside the manual review band [${fm.autoBand.join(",")}]`,
    to: "COMPLIANCE_OFFICER" };
}

Real-world failure modes, and the right response

FailureWhat happenedCorrect responseWrong response
Elderly client's phone has no front camera / poor lightingLiveness fails repeatedlyOffer in-person or branch verification; do not coach the client to "hold the phone near a lamp" repeatedlyTell the client to keep trying until it passes (pressure + rubber-stamping)
Client wears a maskPassive/active challenge failsRe-ask for the active challenge without the mask, or route in personLower the threshold
Client is a twin of the document holderFace match in the manual bandManual review; capture additional ID; escalateAuto-pass "they look the same"
Video replay / deepfakeSpoof score highEscalate to MLRO; this is a fraud indicator, not a UX problemRetry
Screen re-photography of the IDTamper/print score highEscalate; capture the ID again in personAuto-accept the OCR

The mindset to hold: liveness failures are routing decisions, not obstacles. The system is not being difficult; it is refusing to let a document be accepted without a person.

Sanctions and PEP Screening

In practice: every client — and often every beneficiary and every payer — is screened against the sanctions and PEP lists at onboarding, and again on a rolling basis. When something matches, the agent sees a red card and a number of buttons, exactly one of which is acceptable to press.

The three lists and what each means

ListWhat it coversEffect of a true match
UN sanctions (via HK, implementing UNSCR)Named individuals/entities designated by the UN Security CouncilMust not provide any service; report to the JFIU; cannot be tipped off
Local sanctions under the UNATMO (United Nations Ant-Terrorist Motives Ordinance, Cap. 537)UN-designated terrorist persons/entitiesMust not provide any service; report
PEP — politically exposed personCurrent or former senior officials: heads of state, ministers, senior civil servants, military officers, judges, senior political party officials, their immediate family and close associatesNot an automatic block. Requires enhanced due diligence (EDD): source of wealth, source of funds, senior approval, ongoing monitoring. A PEP is not a criminal; a PEP who is also a sanctions target is a block
Adverse mediaNews linking the person to corruption, fraud, bribery, launderingNot a list in the legal sense but a screening source; a hit triggers EDD and human judgement, not an automatic block

Interpreting a screening hit: the decision table

This is the table that decides an agent's case. Note that none of the outcomes are "carry on anyway".

#List / matchMatch strengthClient contextOutcomeWho decidesRequired recordsTimebox
1Sanctions (UN / UNATMO)True match on name + DOB (or DOB alone)AnyBLOCK — do not proceed, do not tip offAutomatic + MLRO notifiedReason code, list version, match evidence, MLRO ackSame day
2SanctionsFuzzy name match only, no DOB/other matchAnyESCALATE → MLRO; manual name adjudicationMLROFuzzy score, alternatives considered, decision + reasoningSame day
3SanctionsTrue match, but likely false positive (e.g. very common name, wrong nationality)AnyESCALATE → MLRO; MLRO documents why it is falseMLROFull adjudication note; never agent-decidedSame day
4PEP — domesticExact match, senior officialAnyEDD: source of wealth, source of funds, senior approval, ongoing monitoring. May still be insuredCompliance officerEDD file, approval name/timeBefore submission
5PEP — foreignExact matchAnyEDD + enhanced monitoring + senior approval; heightened scrutiny of third-party paymentsCompliance officerEDD file, senior approvalBefore submission
6PEP — close associate / family of a PEPPossible matchAnyEDD, documented reasonCompliance officerEDD fileBefore submission
7Adverse mediaReport mentioning the clientAnyEDD + manual review of the report; consider reporting if it suggests launderingCompliance officer / MLROMedia report ref, review note, decision3 working days
8Sanctions — client's beneficiary or payerAnyAnySame as 1–3, applied to that person tooMLROAs aboveSame day
9Client's common name on a list, with a different nationality and DOBFuzzy, low likelihoodAnyDocumented as "considered and excluded" — do not drop the hitAgent records, MLRO samplesExclusion reasoningSame day

The last row is the one agents forget: a screening hit that is not reported is as much of a problem as one that is mishandled, because "we looked and it was fine" is only credible if you recorded the lookup.

Screening result schema

export interface ScreeningResult {
  screeningId: string;
  caseId: string;                     // ties to an application/policy/customer
  subject: ScreeningSubject;
  screenedAt: string;                  // ISO-8601
  screenerVersion: string;             // vendor + list build, e.g. "vendor-x/5.2.0 lists/2026-03-11"
  listsChecked: Array<{
    listId: "UN_SANCTIONS" | "UNATMO" | "HKSAR_LOCAL" | "PEP_GLOBAL" | "ADVERSE_MEDIA";
    buildId: string;
    entriesScreened: number;
  }>;
  outcome: "CLEAR" | "HIT" | "ESCALATE";
  hits: ScreeningHit[];
  matchFactors: MatchFactors;          // which identifiers were compared
  disposition: ScreeningDisposition;
  reviewer?: FourEyesReview;           // required whenever disposition != AUTO_CLEAR
  auditHash: string;                   // tamper-evidence over the whole record
}

export interface ScreeningSubject {
  type: "PROPOSER" | "LIFE_ASSURED" | "BENEFICIARY" | "TRUSTEE" | "PAYER" | "AUTHORISED_SIGNATORY";
  partyId: string;
  nameEn: string; nameZh: string;
  dateOfBirth?: string;                // a strong disambiguator when present
  nationality?: string;
  hkidNumberMasked?: string;
  countryOfResidence?: string;
}

export interface MatchFactors {
  nameExact: boolean;
  nameFuzzyScore: number;             // 0..1
  nameTokenOverlap: string[];          // e.g. ["CHAN","SIU","MING"]
  dobMatches: boolean | null;          // null = no DOB to compare
  nationalityMatches: boolean | null;
  countryMatches: boolean | null;
  idNumberMatches: boolean | null;
  addressMatches: boolean | null;
  listEntry: {                        // what was actually on the list
    listId: string;
    primaryName: string;
    aliases: string[];
    dob?: string;
    nationality?: string;
    programme?: string;               // e.g. "SDGT", "CD-UN", "EBL-UK" (if adopted)
    listedOn?: string;
  };
}

export interface ScreeningHit {
  hitId: string;
  listId: string;
  matchStrength: "EXACT" | "STRONG" | "FUZZY" | "WEAK";
  /** The single most important field: what makes this a true match. */
  corroboratingFactors: string[];      // e.g. ["nameExact","dobMatches","nationalityMatches"]
  /** A true sanctions match requires sanctions-relevant corroboration OR MLRO adjudication. */
  trueMatchProbability: number;
}

export type ScreeningDisposition =
  | "AUTO_CLEAR"                                   // no hit; auto-cleared by screener
  | "FALSE_POSITIVE_DOCUMENTED"                     // agent documents why excluded; MLRO samples
  | "EDD_REQUIRED"                                  // PEP / adverse media ⇒ enhanced due diligence
  | "BLOCK_AND_REPORT"                              // true sanctions match ⇒ block + JFIU report
  | "REFERRED_TO_MLRO";                             // uncertain ⇒ MLRO adjudicates

export interface FourEyesReview {
  firstReviewer: { agentId: string; name: string; at: string; conclusion: string };
  secondReviewer: { complianceOfficerId: string; name: string; at: string; conclusion: string };
  samePersonCheck: "PASS";                          // system asserts reviewer IDs differ
  decision: "PROCEED_STANDARD" | "PROCEED_WITH_EDD" | "BLOCK_AND_REPORT" | "REFER_EXTERNAL_COUNSEL";
  rationale: string;                                // free text, min 40 chars, cannot be boilerplate
}

export const SCREENING_POLICY: Record<string, {
  requiresFourEyes: boolean;
  allowedDispositions: ScreeningDisposition[];
  slaHours: number;
  notify?: "MLRO" | "COMPLIANCE_OFFICER";
}> = {
  UN_SANCTIONS:      { requiresFourEyes: false, allowedDispositions: ["BLOCK_AND_REPORT"], slaHours: 0, notify: "MLRO" },
  UNATMO:            { requiresFourEyes: false, allowedDispositions: ["BLOCK_AND_REPORT"], slaHours: 0, notify: "MLRO" },
  HKSAR_LOCAL:       { requiresFourEyes: false, allowedDispositions: ["BLOCK_AND_REPORT"], slaHours: 0, notify: "MLRO" },
  PEP_GLOBAL:        { requiresFourEyes: true,  allowedDispositions: ["EDD_REQUIRED"], slaHours: 24, notify: "COMPLIANCE_OFFICER" },
  ADVERSE_MEDIA:     { requiresFourEyes: true,  allowedDispositions: ["EDD_REQUIRED"], slaHours: 72, notify: "COMPLIANCE_OFFICER" },
  FUZZY_ANY:         { requiresFourEyes: true,  allowedDispositions: ["FALSE_POSITIVE_DOCUMENTED","REFERRED_TO_MLRO"], slaHours: 24, notify: "MLRO" },
};

falsePositiveDocumented is available to the agent, but BLOCK_AND_REPORT is not: SCREENING_POLICY.UN_SANCTIONS.allowedDispositions contains only BLOCK_AND_REPORT, so no code path lets an agent clear a sanctions hit. That asymmetry is the design.

Four-Eyes Escalation

In practice: a case that hits any of these — face match in the manual band, PEP match, adverse-media hit, fuzzy sanctions match, third-party address, premium above a configured threshold, source of funds over a configured amount — does not proceed on the agent's authority. It goes into a queue, a compliance officer reviews it, and the case waits. The agent's job is to write the referral well, not to push it through.

What triggers escalation

TriggerThreshold (typical)Why
Face match in manual bandscore 0.70–0.85Avoid both auto-fail on a genuine match and auto-pass on a mismatch
PEP matchany true matchFATF Recommendation 12
Adverse-media hitanyNeeds human judgement on materiality
Fuzzy sanctions matchanyNever clear without adjudication
Third-party residential addressanyKnown vector for nominee addresses
Premium / single premiumabove e.g. HK$1mEnhanced scrutiny of source of funds
Source of funds not ordinary (asset sale, inheritance, third-party gift, crypto proceeds)anyClassic laundering indicator
Client declines liveness after several attemptsanyA refusal is itself a signal
The agent feels uneasy and cannot articulate whyanyThe single most valuable escalation reason, and always valid

The escalation mechanics

export interface Escalation {
  escalationId: string;
  caseId: string;
  applicationId: string;
  reason: EscalationReason;
  raisedBy: string;                   // agent id
  raisedAt: string;
  summary: string;                     // min 40 chars, must state the facts not the conclusion
  evidenceRefs: string[];              // screeningId, doc hashes, liveness result
  requestedOf: "COMPLIANCE_OFFICER" | "MLRO";
  status: "OPEN" | "IN_REVIEW" | "RESOLVED_EDD" | "RESOLVED_PROCEED" | "RESOLVED_BLOCK" | "REJECTED";
  slaExpiresAt: string;
  resolution?: {
    decidedBy: string;
    decidedAt: string;
    decision: "PROCEED_STANDARD" | "PROCEED_WITH_EDD" | "BLOCK_AND_REPORT" | "ASK_MORE_EVIDENCE";
    conditions?: string[];             // e.g. "require 6 months' source-of-funds documentation"
    rationale: string;                 // min 40 chars
  };
}

export enum EscalationReason {
  FACE_MATCH_REVIEW_BAND,
  LIVENESS_REPEATEDLY_FAILED,
  PEP_MATCH,
  ADVERSE_MEDIA_HIT,
  FUZZY_SANCTIONS_MATCH,
  THIRD_PARTY_ADDRESS,
  SOURCE_OF_FUNDS_UNUSUAL,
  PREMIUM_ABOVE_THRESHOLD,
  THIRD_PARTY_PAYMENT,
  DOCUMENT_TAMPER_SUSPECTED,
  AGENT_CONCERN_UNARTICULATED,
  DECLINE_IN_LANGUAGE_PERSONA,     // same person, several IDs, language shifts
}

export function createEscalation(input: {
  applicationId: string; reason: EscalationReason; summary: string;
  evidenceRefs: string[]; requestedOf: Escalation["requestedOf"];
}): Escalation {
  const now = new Date();
  const slaHours = input.reason === EscalationReason.FUZZY_SANCTIONS_MATCH ? 24 : 48;
  return {
    escalationId: `ESC-${input.applicationId}-${now.getTime()}`,
    caseId: input.applicationId,
    applicationId: input.applicationId,
    reason: input.reason,
    raisedBy: currentAgentId(),
    raisedAt: now.toISOString(),
    summary: requireMinChars(input.summary, 40, "state the facts, not your conclusion"),
    evidenceRefs: input.evidenceRefs,
    requestedOf: input.requestedOf,
    status: "OPEN",
    slaExpiresAt: new Date(now.getTime() + slaHours * 3600_000).toISOString(),
  };
}

/**
 * Four-eyes: the second reviewer must not be the first, and a case cannot be
 * released by the person who raised it. This is enforced as data, not as advice.
 */
export function resolveEscalation(esc: Escalation, decision: NonNullable<Escalation["resolution"]>): Escalation {
  if (decision.decidedBy === esc.raisedBy) {
    throw new FourEyesViolationError("the agent who raised the escalation cannot decide it");
  }
  const reviewer = lookupReviewer(decision.decidedBy);
  if (reviewer.role !== (esc.requestedOf === "MLRO" ? "MLRO" : "COMPLIANCE_OFFICER")) {
    throw new WrongRoleError(`escalation to ${esc.requestedOf} must be decided by that role`);
  }
  if (requireMinChars(decision.rationale, 40, "reasoning") !== true) {
    throw new RationaleTooShortError();
  }
  // A sanctions-related escalation may only be released one way.
  if (esc.reason === EscalationReason.FUZZY_SANCTIONS_MATCH &&
      !["PROCEED_STANDARD", "BLOCK_AND_REPORT"].includes(decision.decision)) {
    throw new IllegalDispositionError("sanctions escalations may only be PROCEED_STANDARD or BLOCK_AND_REPORT");
  }
  return {
    ...esc,
    status: decision.decision === "BLOCK_AND_REPORT" ? "RESOLVED_BLOCK"
           : decision.decision === "PROCEED_WITH_EDD" ? "RESOLVED_EDD"
           : "RESOLVED_PROCEED",
    resolution: decision,
  };
}

Why the escalation is not a delay penalty

The instinct is to treat the compliance queue as friction to route around. Two facts reverse that instinct:

  1. The cases that get escalated are the cases that would have become complaints or SARs. A face match in the manual band is either a genuine client whose insurer must not decline, or someone using another person's documents — which is fraud and possibly a reportable matter. Guessing is a coin flip with a 50% downside.
  2. A well-written escalation that comes back in 24 hours is faster than a case that bounces. A clean referral — facts, evidence refs, the specific question asked — is resolved in one pass. An informal "hey can you check this?" to the compliance officer's phone generates two days of back-and-forth.

The referral summary is a skill worth teaching: state the facts, ask a specific question, do not state your conclusion. "The client's face match scored 0.78, which is inside the review band. Their HKID is valid to 2029 and the name matches. Is this a genuine match we should accept, or do you want additional ID?" is a referral. "The client looks fake to me" is not.

Record-Keeping Obligations

In practice: every KYC artefact — the ID images, the address proof, the screening result, the escalation, the decision, the liveness frames — is stored in Hong Kong with a hash and a timestamp, and is kept for five years after the relationship ends or the policy ends, whichever is later. When the compliance officer asks for the file, the agent clicks one button.

What is kept, and for how long

RecordRetentionWhy
Identity document copies5 years after the end of the relationship or the policyAMLO customer due diligence record-keeping
Address proof copiesSamePart of CDD
Health disclosure (Lesson 08)Same, plus any longer period the carrier requiresClaim defence; mis-sale investigation
Application + proposal + signed forms + signature evidenceSameContract formation evidence
Screening results (including cleared)SameProving the check was done, including false positives
Escalations and four-eyes decisionsSameProving the review happened and who did it
Liveness frames / video snippetsPer the carrier's policy; typically shorter than the application recordBiometric data — minimise retention; a video of a person's face is more sensitive than a document scan
Source-of-funds documentation5 years after the policy endsEnhanced DD for high risk
Suspension / adverse media reports reviewedSame as screeningPart of the disposition record

The two rows that agents get wrong are the liveness video (agents think "keep everything" and store biometric footage for five years, which is disproportionate and raises its own PDPO question) and cleared screening hits (agents think a false positive needs no record, when in fact the record is the only proof you adjudicated rather than ignored it).

The storage and retrieval shape

export interface KycEvidenceBundle {
  bundleId: string;
  caseId: string;
  createdAt: string;
  retentionUntil: string;              // computed, never set by hand
  retentionBasis: "RELATIONSHIP_END_PLUS_5Y" | "POLICY_END_PLUS_5Y" | "LATER_OF_THE_TWO_PLUS_5Y";
  residency: "HONG_KONG";              // enforced; cross-border transfer needs a separate basis
  encryption: { atRest: "AES-256-GCM"; inTransit: "TLS1.3"; keysIn: "HK-managed KMS" };
  items: KycEvidenceItem[];
  manifestHash: string;                // over all item hashes; the tamper-evidence anchor
  legalHold: boolean;                  // if true, deletion is suspended
}

export interface KycEvidenceItem {
  itemId: string;
  type: "ID_FRONT" | "ID_BACK" | "ADDRESS_PROOF" | "HEALTH_DISCLOSURE" | "SIGNED_FORM"
      | "LIVENESS_FRAME" | "SCREENING_RESULT" | "ESCALATION" | "EDD_FILE";
  storageKey: string;                  // hk-resident object store; never a public URL
  sha256: string;
  mimeType: string;
  capturedAt: string;
  capturedBy: string;                  // agent or system; "client portal upload" is a distinct actor
  containsBiometric: boolean;          // liveness frames ⇒ true ⇒ minimisation rules apply
  maskedViewData: Record<string, string>;  // what the agent UI is allowed to display
}

export function retentionUntil(input: {
  relationshipEndedAt?: string | null;
  policyEndedAt?: string | null;
  relationshipStart: string;
  policyEnd?: string | null;
}): { retentionUntil: string; basis: KycEvidenceBundle["retentionBasis"] } {
  // The retention clock is the LATER of the two end events, plus five years.
  const candidates: Date[] = [];
  if (input.relationshipEndedAt) candidates.push(new Date(input.relationshipEndedAt));
  if (input.policyEndedAt) candidates.push(new Date(input.policyEndedAt));
  // If neither has ended yet, the clock has not started — retention runs from the future end event.
  const latestEnd = candidates.length ? new Date(Math.max(...candidates.map((d) => d.getTime()))) : null;
  const basis: KycEvidenceBundle["retentionBasis"] = latestEnd
    ? (input.relationshipEndedAt && input.policyEndedAt ? "LATER_OF_THE_TWO_PLUS_5Y"
       : input.policyEndedAt ? "POLICY_END_PLUS_5Y" : "RELATIONSHIP_END_PLUS_5Y")
    : "RELATIONSHIP_END_PLUS_5Y";
  // Until then, keep it as long as the live relationship requires.
  const anchor = latestEnd ?? new Date();
  return { retentionUntil: addYears(anchor, 5).toISOString(), basis };
}

export async function retrieveForExamination(bundleId: string, requester: {
  complianceOfficerId: string; purpose: "IA_EXAMINATION" | "INTERNAL_AUDIT" | "SAR_SUPPORT" | "CLAIM_INVESTIGATION";
}): Promise<KycEvidenceBundle> {
  // Access is logged; it is not an implicit read.
  await auditLog.record({ bundleId, requester, at: new Date().toISOString(), action: "RETRIEVE" });
  const bundle = await store.load(bundleId);
  verifyManifestHash(bundle);          // if this fails, the bundle is corrupt ⇒ escalate, do not return
  return bundle;
}

The PDPO angle deserves one paragraph of its own, because it constrains what a POS may do that a pure compliance design might ignore. Under the PDPO, collecting more than is necessary is itself a breach. That gives concrete rules: collect the address proof that the carrier requires, no more; do not retain a passport page the application did not need; keep liveness frames only as long as the challenge result is a live risk; and give the client a real answer when they ask "what do you hold on me and for how long", including how to request access and correction under the PDPO's data-access and correction rights.

flowchart TD
  A[Client identity presented<br/>in person or via portal] --> B{KYC risk profile}
  B -->|Standard| C[Identity verification]
  B -->|Elevated| Z[Enhanced due diligence<br/>source of wealth + senior approval]

  C --> C1[Capture ID document<br/>HKID / passport / travel permit]
  C1 --> C2[Document OCR<br/>+ check digit + authenticity]
  C2 --> C3{Document checks pass?}
  C3 -->|No — expired / checksum| C4[FAIL: ask client to correct<br/>never auto-correct]
  C3 -->|No — tamper signal| Z2[ESCALATE to MLRO]
  C3 -->|Yes| C4b[Read back critical fields<br/>agent confirms vs card]
  C4b --> C5[Liveness detection<br/>challenge + spoof score]
  C5 --> C6{Face match}
  C6 -->|>= 0.85 auto-pass| D[Address verification]
  C6 -->|0.70 - 0.85 manual band| Z3[Four-eyes review]
  C6 -->|< 0.70| Z3
  C5 -->|liveness failed| C7[FAIL + route to branch<br/>no override]

  D --> D1[Collect address proof<br/>bank stmt / utility / tenancy]
  D1 --> D2{Recency <= 3 months?<br/>name + address match?}
  D2 -->|No| D3[FAIL: request recent proof]
  D2 -->|Yes — third party?| Z4[Third-party address:<br/>documented reason + consent<br/>+ confirm call]
  D2 -->|Yes — own address| E

  Z --> Z5[EDD file compiled]
  Z5 --> Z6[Senior approval<br/>not the originating agent]
  Z6 --> E
  Z2 --> Z5
  Z3 --> Z5

  E[Sanctions / PEP / adverse media screening<br/>proposer, assured, beneficiary, trustee] --> F{Result}
  F -->|Clear| G[Record CLEAR<br/>include any false-positive reasoning]
  F -->|PEP true match| Z
  F -->|Adverse media| Z
  F -->|Fuzzy sanctions| Z2
  F -->|True sanctions match| H[BLOCK + report to JFIU<br/>do not tip off client]
  F -->|False positive| G

  G --> I[Bundle sealed<br/>manifest hash + retention clock]
  I --> J[Application released to submission<br/>KYC case closed, monitoring continues]
  H --> K[Case closed<br/>no service provided]
  Z6 --> I

Key Takeaways

  1. The basis is Cap. 615 (AMLO) and its General Regulation, implementing FATF, supervised by the IA — not internal company preference. "I need a bank statement" is a statutory requirement the agent can explain in one sentence.
  2. Insurance AML is mostly about getting money out. Refuse third-party payment, require the payment account in the proposer's own name, capture source of funds, and watch for rapid surrender after inception.
  3. Identity verification and address verification are separate obligations. A valid HKID proves who the person is; a bank statement or utility bill within three months proves where they live. The address printed on the HKID is not address proof. A third-party residential address requires a documented reason, the third party's written consent, and a confirmation call — and often the third party's address becomes the mailing address, so it must be said out loud rather than buried. An unsubstantiated third-party address escalates.
  4. OCR may pre-fill; it may not decide. Auto-accept is limited to high-confidence, check-digit-validated, read-back-confirmed fields. OCR values are bound to the source image hash and flagged stale when the source is replaced.
  5. A checksum failure on an HKID is a "read it again", never an auto-correction. Auto-fixing a typo into a valid but wrong number creates an identity mismatch later that is worse than the original error.
  6. Liveness has no override path in the system, by construction. Face match has a manual band; liveness failures are routing decisions (branch visit), not obstacles to be coached past.
  7. A sanctions true match has exactly one disposition: BLOCK_AND_REPORT, decided automatically or by the MLRO, with no agent-clearing path. PEP is not a block — it is EDD with senior approval and ongoing monitoring. A false positive must be documented as considered, not dropped.
  8. Four-eyes is data, not advice. The originating agent cannot decide their own escalation, a minimum-length rationale is required, and a sanctions escalation can only end in PROCEED_STANDARD or BLOCK_AND_REPORT.
  9. Record-keeping is five years after the later of relationship end or policy end, in Hong Kong, with hashes and a manifest — and biometric liveness frames are minimised rather than kept for five years. A cleared screening hit is kept precisely because "we checked and dismissed it" must be provable.

課堂測驗 · 8 題

Question 1 of 8Answered 0 / 8
Question 1 of 8

保險公司要求客戶提供最近三個月嘅銀行月結單。客戶話「我張卡上面印咗地址㗎喎」。你應該點答?

Pick an answer to lock it in. We'll tell you immediately whether you got it right and show an explanation. Then press Enter or click Next to continue.

Shortcuts:ABCDpick answer on current questionEntergo to next unanswered
8 unanswered