Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dr.green/llms.txt

Use this file to discover all available pages before exploring further.

KYC Flow Guide (FirstAML Integration)

Audience: Engineers integrating customer onboarding into a Dr Green-backed store. TL;DR: You create the customer; Dr Green handles everything else. Poll until verified. You do not integrate with FirstAML directly.

The mental model

KYC is a Dr Green ↔ FirstAML concern. Your store sits outside that loop:
  • Dr Green owns the FirstAML relationship (commercial, contractual, technical)
  • Dr Green emails the customer with verification instructions
  • Dr Green fires the outbound webhook to FirstAML when a client is created
  • Dr Green receives FirstAML’s verification result via /api/v1/kyc/webhook
  • Dr Green updates the client’s isKYCVerified flag in its database
  • Your store sees the updated flag via GET /dapp/clients/{clientId}
You never call FirstAML, never embed their JS, never hold their credentials, never receive their webhooks. If a customer asks about KYC issues, you escalate to Dr Green — you can’t resolve them yourself.

The full flow

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  1. CUSTOMER signs up in your store UI                                      │
│     (provides name, email, phone, shipping address)                         │
│                                                                             │
└──────────────────────────┬─────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  2. YOUR STORE BACKEND calls POST /api/v1/dapp/clients                      │
│     - Returns { id: <new-client-uuid> } with status 201                     │
│     - Your DB:  client.id, client.email, kyc_state='pending', etc.          │
│                                                                             │
└──────────────────────────┬─────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  3. DR GREEN BACKEND, in response to step 2:                                │
│                                                                             │
│     a) Sends an email to the customer with KYC instructions                 │
│        (from a Dr Green address, not yours — branding handled by Dr Green)  │
│                                                                             │
│     b) Fires an outbound webhook to FirstAML to open a verification case    │
│        (POST to FirstAML's API; payload includes the customer's email,     │
│         name, country, and a Dr Green case reference)                       │
│                                                                             │
│     The customer's adminApproval is now PENDING; isKYCVerified is false.    │
│                                                                             │
└──────────────────────────┬─────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  4. CUSTOMER clicks the link in the email                                   │
│     - Lands on FirstAML's hosted verification portal                        │
│     - Uploads ID document(s) (passport / driver's licence / national ID)    │
│     - Completes liveness check (camera selfie)                              │
│     - Provides any additional info FirstAML requires for the AML side       │
│     - Submits                                                                │
│                                                                             │
│     This step happens entirely between the CUSTOMER and FIRSTAML.           │
│     Your store and Dr Green's backend are not involved during this step.    │
│                                                                             │
└──────────────────────────┬─────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  5. FIRSTAML completes verification (timing varies — see below)             │
│                                                                             │
│     POST /api/v1/kyc/webhook  →  DR GREEN BACKEND                          │
│     (Dr Green is the consumer of FirstAML's outbound webhook)               │
│                                                                             │
│     Dr Green updates:                                                        │
│        - client.isKYCVerified = true (or false on rejection)                │
│        - client.adminApproval may transition (often manual review)          │
│                                                                             │
└──────────────────────────┬─────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────────────┐
│                                                                             │
│  6. YOUR STORE BACKEND, polling GET /dapp/clients/{clientId} every ~5 min: │
│     - Sees isKYCVerified flip from false to true                            │
│     - Sees adminApproval flip from PENDING to VERIFIED                      │
│     - Updates your DB: kyc_state='verified', notifies the customer          │
│                                                                             │
│  7. CUSTOMER can now place orders.                                          │
│                                                                             │
└────────────────────────────────────────────────────────────────────────────┘

What you actually code

For most stores, exactly two pieces of code:

Piece 1 — submit on signup

async function onCustomerSignup(form: SignupForm) {
  // Validate inputs client-side first
  validateEmail(form.email);
  validatePhone(form.phoneCode, form.contactNumber);

  // Create the client; Dr Green takes care of everything else
  const { id } = await drGreenClient.post<{ id: string }>('/dapp/clients', {
    firstName: form.firstName,
    lastName: form.lastName,
    email: form.email,
    phoneCode: form.phoneCode,
    contactNumber: form.contactNumber,
    shipping: {
      address1: form.address1,
      address2: form.address2 ?? '',
      city: form.city,
      state: form.state,
      country: form.country,
      countryCode: form.countryCode,  // alpha-3
      postCode: form.postCode,
    },
  });

  // Save to your DB
  await db.customers.insert({
    id,
    email: form.email,
    drGreenClientId: id,
    kycState: 'pending',
    createdAt: new Date(),
  });

  // Show "check your email" UI
  return { customerId: id, awaiting: 'kyc' };
}

Piece 2 — poll until verified

async function pollPendingCustomers() {
  const pending = await db.customers.where({ kycState: 'pending' });

  for (const c of pending) {
    try {
      const fresh = await drGreenClient.get<{
        isKYCVerified: boolean;
        adminApproval: 'PENDING' | 'VERIFIED' | 'REJECTED';
        isActive: boolean;
      }>(`/dapp/clients/${c.drGreenClientId}`);

      if (fresh.adminApproval === 'REJECTED') {
        await db.customers.update(c.id, { kycState: 'rejected' });
        await emailCustomer(c, 'kyc_rejected');
        continue;
      }

      if (fresh.isKYCVerified && fresh.adminApproval === 'VERIFIED' && fresh.isActive) {
        await db.customers.update(c.id, { kycState: 'verified' });
        await emailCustomer(c, 'kyc_verified_can_order');
        continue;
      }

      // Still pending — leave for next tick
    } catch (e) {
      log.warn(`Poll failed for ${c.id}: ${e.message}`);
    }
  }
}

// Run every 5 minutes
cron.schedule('*/5 * * * *', pollPendingCustomers);
That’s it. Two functions. No FirstAML SDK, no FirstAML credentials, no webhook endpoint to expose.

State machine

A client’s KYC-relevant fields transition like this:
                    POST /dapp/clients


      ┌─────────────────────────────────────────────────┐
      │ adminApproval=PENDING  isKYCVerified=false      │
      │ isActive=true                                    │
      └────────────┬────────────┬────────────────────────┘
                   │            │
   FirstAML verifies            FirstAML rejects /
                   │            adminApproval rejects
                   ▼            │
      ┌──────────────────┐      ▼
      │ isKYCVerified=   │   ┌──────────────────────┐
      │ true             │   │ adminApproval=REJECTED│
      │ adminApproval=   │   │ (or isKYCVerified=   │
      │ PENDING          │   │  false with reason)   │
      └────────┬─────────┘   └──────────────────────┘
               │                     │
   Admin approves                    │ TERMINAL
               │                     │ (re-submit may be possible —
               ▼                     │  contact Dr Green)
      ┌────────────────────┐
      │ isKYCVerified=true │
      │ adminApproval=     │
      │ VERIFIED           │
      │ ► CAN PLACE ORDERS │
      └────────────────────┘
🪲 adminApproval and isKYCVerified are independent. Both can change in either order. Don’t write code that assumes adminApproval flips first or isKYCVerified flips first — guard on both being correct before you allow the customer to transact.

Timing expectations

PhaseTypical durationWorst case
Email delivery to customersecondsminutes (rare — provider issue)
Customer completes FirstAML steps5–30 mindays (customer doesn’t engage)
FirstAML processes verificationminutesseveral hours (manual review)
Dr Green admin review (if required)hoursa few days
🔒 These are estimates based on typical KYC/AML pipelines; specific timings for FirstAML’s Dr Green integration are not formally documented to store builders. Confirm with Dr Green if you need SLAs.
Polling cadence: every 5 minutes is the recommended sweet spot. Faster doesn’t make verification go quicker and adds load. Slower delays your customer experience. Total expectation to set with the customer:
  • Best case: 10–15 minutes from signup to “you can order”
  • Realistic case: 1–2 hours
  • Worst case: 1–2 days (manual review queues, AML escalations)
Don’t promise a fixed time. Let the email do the talking.

Edge cases

Customer abandons KYC

The customer signs up, gets the email, never clicks the link. Their record sits in adminApproval: PENDING, isKYCVerified: false indefinitely. Handling:
  • After ~24 hours, send them a follow-up email from your store reminding them
  • After ~7 days, mark as “abandoned” in your DB and stop polling them (set their kycState='abandoned')
  • If they later return and click the original email link, FirstAML will still process them; periodically (e.g. weekly) re-poll abandoned customers in case they re-engaged

KYC rejected

If adminApproval flips to REJECTED, the customer cannot transact. Show them a clear “your application was not approved — please contact support” message.
🔒 Re-submission policy — whether a rejected customer can re-apply with corrected info is a Dr Green policy decision. From the API surface, PATCH /dapp/clients/{id} exists for updates, but whether updating a rejected client moves them back to PENDING and re-fires the FirstAML webhook is not formally documented. Confirm with Dr Green before you build a re-submission UX.

KYC stuck >24 hours

Most cases verify within a few hours. If a customer has been PENDING + isKYCVerified: false for >24 hours, escalate:
  • Surface the customer’s id and email to your support team
  • Your support contacts Dr Green support
  • Dr Green checks the FirstAML case status (which they can see; you can’t)
  • Resolution path depends on the issue (customer didn’t complete? FirstAML asking for more info? Manual review queue?)

Customer wants to update their address

Use PATCH /dapp/clients/{id} for non-PII updates (phone, name, isActive).
🔒 Editing the shipping address post-verification has implications for AML — if FirstAML verified them at one address and they move, Dr Green may require re-verification. The exact policy isn’t formally documented. Build the UX cautiously: allow address edits, but warn that “your verification may need to be repeated.”

What’s in the email Dr Green sends

You don’t control the email content, but for customer-support purposes, here’s what your customers will see in broad strokes:
  • From address: a Dr Green / Cannexis Biopharma address (not yours)
  • Subject: something like “Complete your verification” or “Welcome — finish setting up your account”
  • Content: a link to the FirstAML hosted portal, with the customer’s name pre-populated
  • Branding: Dr Green’s, not yours
You should:
  • Tell customers in your post-signup UI to check their inbox (and spam folder)
  • Set expectations: “You’ll receive an email from Dr Green to complete verification”
  • Provide your support email for issues
You should not:
  • Promise specific timing
  • Try to forward or recreate the email
  • Embed a FirstAML-style verification flow yourself

Frequently asked questions

Q: Can I show the customer a real-time KYC progress UI? A: Limited. You can show the binary state (pending / verified / rejected) by polling GET /dapp/clients/{id}. You don’t have visibility into FirstAML’s internal substates (e.g. “ID document accepted, awaiting liveness”). Q: Can I trigger the email to be re-sent? A: There’s no documented API for this. If a customer needs the email re-sent, they contact Dr Green support, or you escalate. Q: Can my store skip KYC for “low-risk” customers? A: No. KYC is a regulatory requirement; Dr Green enforces it for every customer. There’s no opt-out. Q: Are KYC documents accessible to me? A: No, and that’s by design. They’re held by FirstAML / Dr Green for compliance. You shouldn’t have access — it would push you into a compliance scope you don’t want to be in. Q: What happens if I create a duplicate client (same email twice for the same holder)? A: 409 Conflict typically. Handle it gracefully: surface “an account already exists for this email; would you like to log in?” rather than retrying the create. Q: Different holders, same customer email — what happens? A: Each holder has their own client list. The same email can exist as a client under multiple holders independently. From your store’s perspective, you’re scoped to one holder’s API key, so you only see “your” holder’s clients.

Where to next