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.

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}/.
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:
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:
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:
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. 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:
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:
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:
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 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
  • paymentMethodCRYPTO (CoinRemitter), FIAT (Payinn), or PGPAY
step5_place_order.ts:
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.

Step 6 — track the order

Same polling pattern as KYC. Watch for status transitions until terminal state:
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/.

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.
  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.
  3. Implement webhook-style polling at scale with active-set tracking and jittered intervals across many customers. See 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.

Where to go next

If you want to…Read…
Understand how everything fits togetherguides/store-architecture.md
Master the KYC flow (FirstAML integration)guides/kyc-flow.md
Track orders properly through their full lifecycleguides/order-lifecycle.md
Reference any specific endpointreference/index.md
Debug a 40102-authentication.md § Common 401 causes
Deploy with confidence03-environment.md

Common stumbles when getting started

StumbleSymptomFix
Signing "" for an empty-query GET401 "User is not authorized"Sign "{}" instead. Use the helpers in examples/, don’t roll your own
Sending Content-Type: application/json on a GET400 "is not valid JSON"Only set Content-Type when you have a body
Using alpha-2 country codes400 "must be a valid ISO 3166-1 alpha-3"GBR, not GB
Trying to call /user/me or /dapp/users/nfts401 "Unauthorized" (43 bytes)These are JWT-only; the holder must use the DAPP UI for them
Polling KYC status faster than 5 minMore 200s, no faster resultsStick to 5-minute polling — FirstAML’s pace is what it is
Retrying POST /dapp/orders blindlyDuplicate ordersAlways GET /dapp/orders first to check if the original landed