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:
- ✅ Verify your key pair works against the live API
- 🛒 Browse the strain catalogue (filtered to a country)
- 👤 Onboard a customer (creates a “client” record + triggers KYC via FirstAML)
- ⏳ Wait for KYC + admin approval
- 📦 Place an order
- 🔔 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:
isKYCVerified — flipped by FirstAML’s callback once verification completes
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
paymentMethod — CRYPTO (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:
- Build a cart first (
POST /dapp/carts), let the customer add/remove items, then convert the cart into an order. See reference/carts.md.
- 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.
- Implement webhook-style polling at scale with active-set tracking and jittered intervals across many customers. See 06-webhooks.md § The polling pattern.
- 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 together | guides/store-architecture.md |
| Master the KYC flow (FirstAML integration) | guides/kyc-flow.md |
| Track orders properly through their full lifecycle | guides/order-lifecycle.md |
| Reference any specific endpoint | reference/index.md |
| Debug a 401 | 02-authentication.md § Common 401 causes |
| Deploy with confidence | 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 |