# Onboarding onto Advantage

> No stop on the rail is callable until an account exists, and an account only exists once a
> register has confirmed it.

Every page after this one assumes you already hold a credential. This page covers how one is issued.

Self-service onboarding is a public, unauthenticated funnel. A registrant arrives at the welcome
landing with no token and leaves with an activated account they can sign in to. Identity is never
self-asserted anywhere in it. The registrant supplies an identifier, the rail retrieves the official
record behind it, and the verification code goes to the email address on that record rather than to
one the registrant types. By the time the account exists, whoever holds it has shown they can read
mail at an address a licensing body published.

Two tracks run off the same funnel. An individual, meaning a solo practitioner, verifies one
identity and activates. A business, meaning a clinic or facility with one location or many, verifies
the admin, then the facility, then activates. Both are the same four moves: look up, confirm, enter
the code, continue.

<Mermaid chart={`flowchart TD
  W["Welcome landing<br/>Get Started"] --> T{"Account type"}
  T -->|Individual| A["01 · Verify admin identity"]
  T -->|Business| A
  A --> AR["Registry search"]
  AR --> AC["Confirm details"]
  AC --> AO["Code to the registered email"]
  AO -->|Individual| Z["03 · Activate account"]
  AO -->|Business| B["02 · Setup business"]
  B --> BR["Facility registry search"]
  BR --> BC["Confirm business details"]
  BC --> BO["Code to the facility email"]
  BO --> Z
  Z --> ZE["Check your email<br/>temporary password"]
  ZE --> ZS["Sign in · set a new password"]
  ZS --> ZD["Account activated"]
  ZD -.->|"no subscription"| P["Plan · billing · M-Pesa"]
  ZD --> R["00 · Authenticate on the rail"]
  P --> R
  A -.->|"registry has nothing"| M["Enter details manually"]
  M --> Z
  AC -.->|"cannot open that inbox"| E["Manual verification<br/>1 to 2 business days"]
`} />

<Mermaid chart={`sequenceDiagram
    autonumber
    actor U as Registrant
    participant F as Advantage
    participant O as Onboarding API
    participant X as Registries (KMPDC, BRS, KRA)
    participant M as Their inbox

    U->>F: Identifier type + number
    F->>O: registry-search
    O->>X: Look up the official record
    X-->>O: Name, licence, status, email
    O-->>F: registry_result_id + details
    F-->>U: Confirm these details
    U->>F: Confirm
    F->>O: send-verification
    O->>M: 6-digit code to the registry's email
    U->>F: Code
    F->>O: verify-code
    O-->>F: verification_token
    F->>O: complete-registration
    O->>M: "Welcome - Your Temporary Password"
    U->>F: Sign in, set a new password
    Note over U,F: The account exists and can now hold credentials.
`} />

<Mermaid chart={`flowchart LR
  A["Identifier + type"] -->|"registry_result_id"| B["Confirmed record"]
  B -->|"the registry's email"| C["Code sent"]
  C -->|"verification_token"| D["Registration completed"]
  D -->|"temporary password"| E["First sign-in"]
  E -->|"a real password"| F["An account"]
  F -->|"client credentials"| G["00 · Authenticate"]
`} />

## The map

| # | Step | The question it answers | What the registrant leaves with |
|---|------|------------------------|-------------------------------|
| 0 | **Account type** | Solo practitioner, or a business with locations? | the track, and whether it runs two steps or three |
| 1 | **Verify admin identity** | Who is setting this up, and are they licensed to? | `registry_result_id` and `verification_token` |
| 2 | **Setup business** *(business only)* | Which facility is this, and is its registration current? | the facility's confirmed record |
| 3 | **Activate account** | Can they read mail at the address the registry holds? | a temporary password, then a real one |
| | **Plan and billing** *(where required)* | Is there an active subscription behind this org? | a receipt, and the app unlocked |

The step count is the only difference between the tracks. Individual runs two steps, verify then
activate. Business runs three, verify the admin, set up the business, then activate. The stepper at
the top of the screen is built from that choice.

## Step 0 · Which account is being set up

<Wireframe
  title="Let's get you set up!"
  actor="Registrant · not signed in"
  rows={[
    [
      { label: "Who are you setting this up for?", kind: "text", span: 4, note: "You can change this later if needed" },
    ],
    [
      { label: "Individual", kind: "field", swatch: 1, span: 2, note: "Solo practitioners and independent health or wellness professionals. Two steps." },
      { label: "Business", kind: "field", swatch: 2, span: 2, note: "Clinics, facilities, or businesses operating one or more locations. Three steps." },
    ],
    [
      { label: "Back", kind: "action", span: 1 },
      { label: "Continue", kind: "action", span: 3 },
    ],
  ]}
  caption={<>A deployment that only registers one kind of account skips this screen and carries its pinned type into the funnel, so the registrant is never offered a choice that does not apply to them.</>}
/>

The choice travels as a route parameter for the rest of the funnel, so it survives a refresh or a
back-navigation. It is also the last thing the registrant asserts on their own. Everything after it
is checked against a register.

## Step 1 · Verify the admin identity

Whoever sets up an account may be an admin, the owner, or a staff member assigned to do it, and is
not always the person the account is about. This step establishes who is at the keyboard and whether
a licensing body knows them.

It opens on a choice of identifier, and that choice decides how much typing follows.

<Wireframe
  title="Verify your professional registration"
  actor="Registrant · step 1 of 2 or 3"
  rows={[
    [
      { label: "I'm registered with KMPDC", kind: "field", swatch: 1, span: 2, note: "Doctors, dentists, clinical officers. We retrieve the details automatically." },
      { label: "Other professional registration", kind: "field", swatch: 2, span: 2, note: "Nurses, therapists, public health. Details entered by hand, about 3 minutes." },
    ],
    [
      { label: "Identifier number", kind: "field", span: 3, from: "identifier.number", note: "Exactly as it appears on the official record, for example A1234" },
      { label: "Can't find it?", kind: "action", span: 1, note: "Leads to the manual form" },
    ],
    [
      { label: "I confirm compliance with the Data Protection Act and that patient consent is obtained before sharing health records", kind: "text", span: 4, note: "The button stays disabled until this is ticked" },
    ],
    [
      { label: "Back", kind: "action", span: 1 },
      { label: "Continue, then Checking registry", kind: "action", span: 3, from: "POST registry-search" },
    ],
  ]}
  caption={<>Nothing is saved at this point. The identifier only retrieves a record, and the registrant reviews it before anything is written, which is what the screen says.</>}
/>

A hit comes back as a complete professional record, so the next screen reviews it instead of asking
for it. The fields are read only, and the screen says where to go when something is wrong: the
licensing body holds the record and Advantage only displays it. A copy anyone could edit would prove
nothing.

<Wireframe
  title="Confirm your professional details"
  actor="Registrant · retrieved from the registry"
  rows={[
    [
      { label: "Dr Jane Doe", kind: "text", swatch: 1, span: 2, from: "full_name" },
      { label: "Speciality / cadre", kind: "text", swatch: 1, span: 2, from: "speciality_cadre" },
    ],
    [
      { label: "KMPDC", kind: "text", span: 1, from: "licensing_body" },
      { label: "Registration / licence number", kind: "text", span: 1, from: "registration_number" },
      { label: "Licence active", kind: "field", tone: "positive", span: 1, from: "licence_status" },
      { label: "Valid to 31 Dec 2026", kind: "text", span: 1, from: "licence_validity_end_date" },
    ],
    [
      { label: "j****@practice.co.ke", kind: "field", tone: "attention", span: 4, from: "email", note: "To protect your privacy, we'll send a verification code to your registered email address. This one, taken from the record." },
    ],
    [
      { label: "Contact your licensing body to correct anything wrong here", kind: "text", span: 2 },
      { label: "Send code", kind: "action", span: 2, from: "POST send-verification" },
    ],
  ]}
  caption={<>Everything turns on this email. It comes from the registry, it cannot be edited, and the code goes to it, so a licence number on its own gets nobody an account.</>}
/>

The code is six digits, expires in minutes, and can be resent. Verifying it returns a
`verification_token`, and what happens next is the one place the two tracks diverge:

<Wire
  rows={[
    { call: "Individual", does: "The registration completes here. The token is spent immediately and the registrant goes straight to activation.", keep: "an account, pending activation" },
    { call: "Business", does: "The result id and token are held rather than spent, and travel into the business payload later. A business account exists only once the facility behind it is confirmed too.", keep: "registry_result_id and verification_token" },
  ]}
/>

Business admins skip the KMPDC lookup and land straight on the manual admin form. Whoever registers
a clinic is often an administrator with no clinical licence to look up, so asking them for a
practitioner number first would strand them on the first screen.

<Wireframe
  title="Enter admin details"
  actor="Registrant · manual branch"
  rows={[
    [
      { label: "Full name", kind: "field", span: 2, from: "full_name" },
      { label: "Profession", kind: "field", span: 2, from: "profession", note: "Searchable list" },
    ],
    [
      { label: "Work email", kind: "field", span: 2, from: "email", note: "We'll send a verification link to this address" },
      { label: "Phone number", kind: "field", span: 2, from: "phone", note: "A 6-digit code goes here by SMS" },
    ],
    [
      { label: "I am the business owner", kind: "field", span: 4, from: "is_business_owner", note: "The owner is legally responsible for the account. Setting it up on someone's behalf? Leave it unchecked." },
    ],
    [
      { label: "Consent to collection, processing and storage under the Terms & Privacy Policy", kind: "text", span: 3 },
      { label: "Continue", kind: "action", span: 1, from: "POST manual-registration" },
    ],
  ]}
  caption={<>The manual branch serves every profession whose register the rail does not read live. It costs one form, and it produces the same account.</>}
/>

## Step 2 · Setup business

Business only. It repeats step 1 one level up, asking the same four questions about a facility
instead of a person.

<Wire
  rows={[
    { call: "Registered with KMPDC", does: "The facility register is read live. Details come back complete: name, level, ownership, type, licence validity, location.", keep: "the facility's record" },
    { call: "Other business registration", does: "KRA, BRS, county government, or any other permit. The registrant supplies business name, industry, business email, phone, physical and postal address by hand.", keep: "the same record, typed" },
  ]}
/>

The code for this step goes to the facility's registered email, often a reception or practice
manager address rather than the admin's own inbox. That is deliberate. Verifying a person and
verifying the business they claim to represent are two separate checks, and running both through one
inbox would make the second one free.

Completing this step creates the business account. The admin's held `registry_result_id` and
`verification_token` go up alongside the facility's, and both identities are registered in one call.

<Wireframe
  title="Confirm your business details"
  actor="Registrant · step 2 of 3"
  rows={[
    [
      { label: "Nairobi Family Clinic", kind: "text", swatch: 2, span: 2, from: "business_name" },
      { label: "Level 3 · private", kind: "text", swatch: 2, span: 2, from: "facility_level, business_ownership" },
    ],
    [
      { label: "Business ID", kind: "text", span: 1, from: "business_id" },
      { label: "Licensing body", kind: "text", span: 1, from: "licensing_body" },
      { label: "Licence active", kind: "field", tone: "positive", span: 1, from: "licence_status" },
      { label: "Valid to 30 Jun 2027", kind: "text", span: 1, from: "licence_validity_end_date" },
    ],
    [
      { label: "Business location", kind: "text", span: 4, from: "business_location" },
    ],
    [
      { label: "Back", kind: "action", span: 1 },
      { label: "Send code to the business email", kind: "action", span: 3, from: "POST facility/send-verification" },
    ],
  ]}
  caption={<>An expired facility licence is the most common reason a business account cannot be completed. The status line shows it here, minutes into onboarding, instead of at the first claim.</>}
/>

## Step 3 · Activate the account

Registration and activation are deliberately separate. Registration establishes that the identities
check out; activation establishes that a human can read the mailbox they are attached to.

<Wireframe
  title="Check your email"
  actor="Registrant · final step"
  rows={[
    [
      { label: "One more step. We've sent an activation email to j****@practice.co.ke", kind: "text", span: 4 },
    ],
    [
      { label: 'Find the email titled "Welcome - Your Temporary Password"', kind: "text", span: 2, note: "Spam and junk included. This is where most stalls happen." },
      { label: "Copy the temporary password", kind: "text", span: 2 },
    ],
    [
      { label: "Open Gmail", kind: "action", span: 1 },
      { label: "Open Outlook", kind: "action", span: 1 },
      { label: "Open Yahoo", kind: "action", span: 1 },
      { label: "Sign in", kind: "action", span: 1, note: "You'll be asked to create a new password" },
    ],
    [
      { label: "The activation link expires in 24 hours", kind: "field", tone: "attention", span: 4 },
    ],
  ]}
  caption={<>The temporary password is single use: the first sign-in forces a reset. Until that sign-in happens there is a registration on file but no usable account.</>}
/>

After that the account is live and the registrant lands on the dashboard.

## Where the funnel forks

Three branches, each there so a registrant who falls out of the happy path still has a way to finish.

<Wire
  rows={[
    { call: "The registry has nothing", does: "Every lookup screen carries a 'can't find your identifier' link into the manual form. The registrant types what the registry would have supplied.", keep: "the same account, a form later" },
    { call: "The inbox is unreachable", does: "The address on the registry record is stale, or belongs to a former employer. Manual verification is queued instead and reviewed within 1 to 2 business days.", keep: "a pending verification" },
    { call: "A licence needs evidence", does: "The compliance screen takes the regulator and a licence or accreditation upload (PDF, JPG or PNG). Checks run automatically, and a failed automatic check queues a manual review within 48 hours.", keep: "an uploaded licence, verified or queued" },
  ]}
/>

Two behaviours that generate support tickets when nobody knows about them:

<BuildSequence
  steps={[
    { do: "A refresh resumes the step, a restart clears it", detail: <>Progress inside a step is cached: the retrieved record, the view the registrant is on, and the verification token. A reload picks up where it left off. Entering the funnel again clears all of it, so a half finished attempt from last week cannot leak stale details into a new one.</> },
    { do: "Signed-in users are redirected off the funnel", detail: <>Every public onboarding screen sends an authenticated user elsewhere, including on a typed URL or a back-navigation. Somebody who already has an account and wants to attach it to an organisation needs account linking, which is a separate flow.</> },
  ]}
/>

## The calls behind the screens

The funnel is unauthenticated by design. There is no bearer token yet, so the check that carries the
weight is the code sent to the address the registry holds.

<Wire
  rows={[
    { call: "POST /api/v1/onboarding/individual/registry-search", does: "Look up a professional by identifier type and number.", keep: "registry_result_id and the record" },
    { call: "POST /api/v1/onboarding/individual/send-verification", does: "Send a 6-digit code to the email on the retrieved record.", keep: "nothing" },
    { call: "POST /api/v1/onboarding/individual/verify-code", does: "Exchange the code for a token proving the address was read.", keep: "verification_token" },
    { call: "POST /api/v1/onboarding/individual/complete-registration", does: "Create the account from the confirmed record and token.", keep: "an account, pending activation" },
    { call: "POST /api/v1/onboarding/individual/manual-registration", does: "The same, from details typed by hand when no register answers.", keep: "an account, pending activation" },
    { call: "POST /api/v1/onboarding/business/facility/registry-search", does: "The facility equivalent of the first call.", keep: "registry_result_id and the facility record" },
    { call: "POST /api/v1/onboarding/business/facility/send-verification · verify-code", does: "Code to the facility's registered email, then the exchange.", keep: "the facility's verification_token" },
    { call: "POST /api/v1/onboarding/business/admin/validate", does: "Check the admin identity against the facility being registered before the two are joined.", keep: "a validated admin" },
    { call: "POST /api/v1/onboarding/business/complete-registration", does: "Register admin and facility together, both tokens in one payload.", keep: "a business account" },
  ]}
/>

## After activation, before the rail

One gate can still sit between an activated account and the app. Where the organisation needs a
subscription, the first navigation into any authenticated screen is intercepted and sent to plan
selection: a plan, then billing, then an M-Pesa STK push confirmed on the phone, then a receipt. It
runs as a hook on the router, so it fires however the user arrived, whether straight out of
onboarding or from a bookmark six weeks later.

Past that the account holds credentials, which is where the rest of this documentation starts.

  The account exists, so now get your system onto the rail. Client credentials, the facility your
  token is scoped to, and the token every later call carries.

  Invoicing through the revenue branch needs a separate registration against KRA, done once and
  independently of this one.
