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

# 01 quickstart

# Quickstart

> **Goal:** From "I have a holder's API key pair" to "I've placed an order in the customer's name" in under 30 minutes.
>
> **Prerequisites:** Node.js 20+ (or Python 3.10+, or PHP 7.4+, or just `curl` + `openssl`), and an `(apiKey, secretKey)` pair from a Dr Green NFT holder.

***

## What you're building

A minimum-viable Dr Green-backed store flow:

1. ✅ Verify your key pair works against the live API
2. 🛒 Browse the strain catalogue (filtered to a country)
3. 👤 Onboard a customer (creates a "client" record + triggers KYC via FirstAML)
4. ⏳ Wait for KYC + admin approval
5. 📦 Place an order
6. 🔔 Detect status changes via polling

Once this works, scale it into a real storefront.

***

## Step 0 — set up the helpers

This walkthrough uses Node.js / TypeScript. Equivalent code in Python, cURL, and PHP is in `examples/{python,curl,php}/`.

```bash theme={null}
mkdir my-drgreen-store && cd my-drgreen-store
npm init -y
npm install --save-dev typescript ts-node @types/node
# Copy the helper from this docs repo:
cp /path/to/drgreen-api-docs/examples/nodejs/sign.ts ./sign.ts
```

Save your key pair as environment variables. **Never commit them to git.** Use `.env` + `.gitignore`, or your secrets manager:

```bash theme={null}
export DRGREEN_API_KEY='LS0tLS1CRUdJTiBQVUJMSUMgS0VZ...'   # Base64 PEM SPKI public key
export DRGREEN_SECRET_KEY='LS0tLS1CRUdJTiBQUklWQVRF...'   # Base64 PEM PKCS8 private key
```

***

## Step 1 — verify the keys (sanity check)

Before you write any real code, prove your key pair signs correctly. The cheapest authenticated read is the dashboard summary.

`step1_verify.ts`:

```typescript theme={null}
import { DrGreenClient } from './sign';

const client = new DrGreenClient(
  'https://api.drgreennft.com/api/v1',
  process.env.DRGREEN_API_KEY!,
  process.env.DRGREEN_SECRET_KEY!,
);

(async () => {
  const summary = await client.get<{
    clientCount: number;
    orderCount: number;
    productCount: number;
    totalProfit: number;
    profitRecieved: number;  // sic — typo preserved from API
  }>('/dapp/dashboard/summary');

  console.log('✅ Key pair works. Holder dashboard:');
  console.log(`   Clients:  ${summary.clientCount}`);
  console.log(`   Orders:   ${summary.orderCount}`);
  console.log(`   Products: ${summary.productCount}`);
  console.log(`   Profit:   $${summary.totalProfit.toFixed(2)}`);
})();
```

Run it:

```bash theme={null}
npx ts-node step1_verify.ts
```

**Expected output:**

```
✅ Key pair works. Holder dashboard:
   Clients:  19
   Orders:   21
   Products: 210
   Profit:   $157.13
```

**If you see `401 "User is not authorized"`:** your signature is being rejected. Check [02-authentication.md § Common 401 causes](./02-authentication.md#common-401-causes-the-diagnostic-table). The most common cause is signing `""` instead of `"{}"` for empty-query GETs.

***

## Step 2 — browse the strain catalogue

Strains are country-filtered. The customer's country determines what they can buy. Use ISO 3166-1 **alpha-3** codes (`GBR`, `USA`, `DEU` — not `GB`, `US`, `DE`).

`step2_strains.ts`:

```typescript theme={null}
import { DrGreenClient } from './sign';

const client = new DrGreenClient(
  'https://api.drgreennft.com/api/v1',
  process.env.DRGREEN_API_KEY!,
  process.env.DRGREEN_SECRET_KEY!,
);

(async () => {
  const customerCountry = 'ZAF'; // South Africa — pick the country your customer is in
  const result = await client.get<{
    strains: Array<{
      id: string;
      name: string;
      retailPrice: number;
      wholeSalePrice: number;
      currency: string;
      thcContent?: number;
      strainType?: string;
    }>;
    pageMetaDto: { itemCount: number; pageCount: number };
  }>('/dapp/strains', {
    countryCode: customerCountry,
    page: 1,
    limit: 10,
  });

  console.log(`Available in ${customerCountry}: ${result.pageMetaDto.itemCount} strains`);
  for (const s of result.strains) {
    console.log(`  ${s.name.padEnd(30)}  $${s.retailPrice}  ${s.strainType ?? ''}`);
  }
})();
```

> If the response is `strains: []`, the holder doesn't have any products available in that country. Try `GBR`, `USA`, `ZAF`, or `DEU`. If still empty, ask the holder to confirm their product allocation in the DAPP UI.

***

## Step 3 — onboard a customer (this triggers KYC)

`POST /dapp/clients` creates a customer record AND kicks off the KYC flow. Dr Green's backend will email the customer with verification instructions and fire a webhook to FirstAML to open a case. **Your store has nothing more to do for KYC** — just create the client and wait.

`step3_create_client.ts`:

```typescript theme={null}
import { DrGreenClient } from './sign';

const client = new DrGreenClient(
  'https://api.drgreennft.com/api/v1',
  process.env.DRGREEN_API_KEY!,
  process.env.DRGREEN_SECRET_KEY!,
);

(async () => {
  const newClient = await client.post<{ id: string }>('/dapp/clients', {
    firstName: 'Ava',
    lastName:  'Nguyen',
    email:     'ava+test@example.com',         // unique per holder
    phoneCode: '+27',                          // dial prefix only
    contactNumber: '821234567',                // digits only, no spaces
    shipping: {
      address1:    '12 Long Street',
      address2:    '',
      city:        'Cape Town',
      state:       'Western Cape',
      country:     'South Africa',
      countryCode: 'ZAF',                      // alpha-3, not alpha-2
      postCode:    '8001',
    },
  });

  console.log(`✅ Client created: ${newClient.id}`);
  console.log('   Dr Green has emailed the customer with KYC instructions.');
  console.log('   Now poll until both isKYCVerified and adminApproval=VERIFIED.');

  // Save this somewhere — your store DB
  // CLIENT_ID=ava-nguyen <newClient.id>
})();
```

> ⚠️ **Don't run this with real PII unless you're ready** — it creates a real client record on production and sends a real email. If you're just exploring, use a throwaway email address you control.

***

## Step 4 — poll for KYC + admin approval

Two things need to flip from `false`/`PENDING` to `true`/`VERIFIED` before the customer can transact:

1. `isKYCVerified` — flipped by FirstAML's callback once verification completes
2. `adminApproval` — flipped by Dr Green's admin team after manual review

Polling pattern (60-second intervals with 10% jitter, terminal-state detection):

`step4_wait_for_verification.ts`:

```typescript theme={null}
import { DrGreenClient } from './sign';

const client = new DrGreenClient(
  'https://api.drgreennft.com/api/v1',
  process.env.DRGREEN_API_KEY!,
  process.env.DRGREEN_SECRET_KEY!,
);

const CLIENT_ID = process.env.CLIENT_ID!;

interface ClientDetail {
  id: string;
  isKYCVerified: boolean;
  adminApproval: 'PENDING' | 'VERIFIED' | 'REJECTED';
  isActive: boolean;
}

async function pollUntilReady(): Promise<ClientDetail> {
  while (true) {
    const c = await client.get<ClientDetail>(`/dapp/clients/${CLIENT_ID}`);
    console.log(
      `  KYC=${c.isKYCVerified}  adminApproval=${c.adminApproval}  active=${c.isActive}`
    );
    if (c.adminApproval === 'REJECTED') {
      throw new Error('Client rejected — cannot proceed');
    }
    if (c.isKYCVerified && c.adminApproval === 'VERIFIED' && c.isActive) {
      return c;
    }
    // 5 min ± 10% jitter
    const jitter = 0.9 + 0.2 * Math.random();
    await new Promise((r) => setTimeout(r, 5 * 60 * 1000 * jitter));
  }
}

(async () => {
  console.log(`Polling client ${CLIENT_ID}...`);
  const ready = await pollUntilReady();
  console.log(`✅ Client ready to transact: ${ready.id}`);
})();
```

> **Tuning the interval.** 5 minutes is the recommended polling cadence. Faster won't make FirstAML or Dr Green's admin review go quicker, and adds load to the API. Slower may delay your customer experience. See [guides/kyc-flow.md](./guides/kyc-flow.md) for the full state machine.

***

## Step 5 — place an order

Once `isKYCVerified=true`, `adminApproval=VERIFIED`, and `isActive=true`, the customer can buy. Note the order needs:

* `clientId` — from step 3
* `shippingId` — from the client's `shippings[]` array (call `GET /dapp/clients/{id}` to read it)
* `orderLines[]` — `{strainId, quantity}` per line; strain must be available in the customer's country
* `paymentMethod` — `CRYPTO` (CoinRemitter), `FIAT` (Payinn), or `PGPAY`

`step5_place_order.ts`:

```typescript theme={null}
import { DrGreenClient } from './sign';

const client = new DrGreenClient(
  'https://api.drgreennft.com/api/v1',
  process.env.DRGREEN_API_KEY!,
  process.env.DRGREEN_SECRET_KEY!,
);

const CLIENT_ID = process.env.CLIENT_ID!;
const STRAIN_ID = process.env.STRAIN_ID!;  // pick one from step 2

(async () => {
  // 1. Read the client's shipping IDs
  const customer = await client.get<{
    shippings: Array<{ id: string; countryCode: string }>;
  }>(`/dapp/clients/${CLIENT_ID}`);

  if (customer.shippings.length === 0) {
    throw new Error('Customer has no shipping address — should not happen post-KYC');
  }

  const shippingId = customer.shippings[0].id;

  // 2. Place the order
  const order = await client.post<{
    id: string;
    invoiceNumber: string;
    totalAmount: number;
    orderStatus: string;
    paymentStatus: string;
  }>('/dapp/orders', {
    clientId: CLIENT_ID,
    shippingId,
    orderLines: [
      { strainId: STRAIN_ID, quantity: 1 },
    ],
    paymentMethod: 'CRYPTO',
  });

  console.log('✅ Order placed:');
  console.log(`   ID:      ${order.id}`);
  console.log(`   Invoice: ${order.invoiceNumber}`);
  console.log(`   Total:   $${order.totalAmount}`);
  console.log(`   Status:  ${order.orderStatus} (payment: ${order.paymentStatus})`);
})();
```

> ⚠️ **`POST /dapp/orders` is not idempotent.** A retry creates a duplicate. If the call fails, do NOT blindly retry — first `GET /dapp/orders` and look for a recent matching order before attempting again. See [04-errors.md § Idempotency](./04-errors.md#idempotency).

***

## Step 6 — track the order

Same polling pattern as KYC. Watch for status transitions until terminal state:

```typescript theme={null}
async function trackOrder(orderId: string) {
  const TERMINAL = new Set(['DELIVERED', 'CANCELLED']);
  while (true) {
    const { orderDetails: o } = await client.get<{
      orderDetails: {
        orderStatus: string;
        adminApproval: string;
        paymentStatus: string;
      };
    }>(`/dapp/orders/${orderId}`);

    console.log(`  status=${o.orderStatus} admin=${o.adminApproval} pay=${o.paymentStatus}`);

    if (TERMINAL.has(o.orderStatus) || o.adminApproval === 'REJECTED') return o;

    await new Promise((r) => setTimeout(r, 60_000));  // 60s
  }
}
```

> Notice the response wraps the data inside `orderDetails` — unlike most other endpoints which put data at the top of `data`. This is a known inconsistency. See [orders.md § GET /dapp/orders/{orderId}](./reference/orders.md#get-dapporders-orderid--order-detail).

***

## What you skipped

This walkthrough creates an order from a single strain directly. In a real store, you'd typically:

1. **Build a cart first** (`POST /dapp/carts`), let the customer add/remove items, then convert the cart into an order. See [reference/carts.md](./reference/carts.md).
2. **Display prices in the customer's local currency**, using `localPrice.currency` and `localPrice.totalAmount` from the order detail rather than `totalAmount` (USD). See [orders.md](./reference/orders.md).
3. **Implement webhook-style polling at scale** with active-set tracking and jittered intervals across many customers. See [06-webhooks.md § The polling pattern](./06-webhooks.md#the-polling-pattern).
4. **Handle errors and retries properly** including the no-Idempotency-Key workaround for writes. See [04-errors.md § Retry guidance](./04-errors.md#retry-guidance).

***

## Where to go next

| If you want to...                                  | Read...                                                                                                   |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Understand how everything fits together            | [guides/store-architecture.md](./guides/store-architecture.md)                                            |
| Master the KYC flow (FirstAML integration)         | [guides/kyc-flow.md](./guides/kyc-flow.md)                                                                |
| Track orders properly through their full lifecycle | [guides/order-lifecycle.md](./guides/order-lifecycle.md)                                                  |
| Reference any specific endpoint                    | [reference/index.md](./reference/index.md)                                                                |
| Debug a 401                                        | [02-authentication.md § Common 401 causes](./02-authentication.md#common-401-causes-the-diagnostic-table) |
| Deploy with confidence                             | [03-environment.md](./03-environment.md)                                                                  |

***

## Common stumbles when getting started

| Stumble                                           | Symptom                                    | Fix                                                                      |
| ------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
| Signing `""` for an empty-query GET               | `401 "User is not authorized"`             | Sign `"{}"` instead. Use the helpers in `examples/`, don't roll your own |
| Sending `Content-Type: application/json` on a GET | `400 "is not valid JSON"`                  | Only set Content-Type when you have a body                               |
| Using alpha-2 country codes                       | `400 "must be a valid ISO 3166-1 alpha-3"` | `GBR`, not `GB`                                                          |
| Trying to call `/user/me` or `/dapp/users/nfts`   | `401 "Unauthorized"` (43 bytes)            | These are JWT-only; the holder must use the DAPP UI for them             |
| Polling KYC status faster than 5 min              | More 200s, no faster results               | Stick to 5-minute polling — FirstAML's pace is what it is                |
| Retrying `POST /dapp/orders` blindly              | Duplicate orders                           | Always `GET /dapp/orders` first to check if the original landed          |
