> ## 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

# 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

```typescript theme={null}
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

```typescript theme={null}
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

| Phase                               | Typical duration | Worst case                      |
| ----------------------------------- | ---------------- | ------------------------------- |
| Email delivery to customer          | seconds          | minutes (rare — provider issue) |
| Customer completes FirstAML steps   | 5–30 min         | days (customer doesn't engage)  |
| FirstAML processes verification     | minutes          | several hours (manual review)   |
| Dr Green admin review (if required) | hours            | a 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

* [Quickstart § Step 3](../01-quickstart.md#step-3--onboard-a-customer-this-triggers-kyc) — minimal create-client code
* [reference/clients.md](../reference/clients.md) — full client endpoint reference
* [order-lifecycle.md](./order-lifecycle.md) — what happens after KYC is complete
