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.
Order Lifecycle Guide
Audience: Engineers building order flows in a Dr Green-backed store.
TL;DR: Orders pass through five conceptual stages — placed, admin-reviewed, paid, dispatched, delivered. Each stage updates one of three independent status fields. Polling is your only signal for transitions.
The three status fields
Every order has three independent statuses. They evolve in parallel, not strictly sequentially.
| Field | Set by | Values | Meaning |
|---|
orderStatus | Dr Green internal logic | PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED | Where the order is in the fulfilment pipeline |
adminApproval | Dr Green admin team | PENDING, VERIFIED, REJECTED | Manual review outcome |
paymentStatus | Payment processor webhook (CoinRemitter / Payinn / PGPay) | PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED | Did the customer pay |
Critical: these three are independent. An order can be adminApproval=VERIFIED while paymentStatus=PENDING (admin pre-approves, customer hasn’t paid yet) and vice versa. Always check all three before considering an order “complete.”
🔒 The complete enum lists above are partly inferred. Verified live values: orderStatus=PENDING|DELIVERED, adminApproval=PENDING|VERIFIED|REJECTED, paymentStatus=PENDING. Other values are likely supported but not yet seen in production data — defensively render unknown values as the raw string.
The lifecycle visualised
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ POST /dapp/orders │
│ Your store sends the order request │
│ │
└──────────────────────────┬──────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ STAGE 1: PLACED │
│ │
│ orderStatus = PENDING │
│ adminApproval = PENDING │
│ paymentStatus = PENDING │
│ │
│ Returned with a fresh `id` and a 32-char `invoiceNumber` │
│ │
└──────────────────────────┬──────────────────────────────────────────────────┘
│
│ Dr Green admin reviews (manual, hours to days)
│
┌────────┴────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ STAGE 2a: REJECTED │ │ STAGE 2b: APPROVED │
│ adminApproval= │ │ adminApproval= │
│ REJECTED │ │ VERIFIED │
│ │ │ │
│ TERMINAL. │ │ Customer prompted to │
│ Refund any payment. │ │ pay (if not already) │
│ Notify customer. │ │ │
└──────────────────────┘ └──────────┬───────────┘
│
│ Customer pays via processor
│
┌──────────┴────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ STAGE 3: PAID │ │ STAGE 3': FAILED│
│ paymentStatus= │ │ paymentStatus= │
│ COMPLETED │ │ FAILED │
└────────┬────────┘ └─────────────────┘
│ │
│ │ Customer can retry
▼ ▼ payment via Dr Green
┌────────────────────────────────────────┐
│ STAGE 4: DISPATCHED │
│ orderStatus = SHIPPED │
│ (Dr Green ships from their distribution)│
└────────────────┬────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ STAGE 5: DELIVERED (TERMINAL) │
│ orderStatus = DELIVERED │
│ Commission accrues to the holder │
└─────────────────────────────────────────┘
Reading the order through this lifecycle
Stage 1 — placed
Right after POST /dapp/orders, every status is PENDING:
{
"data": {
"id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
"invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
"orderStatus": "PENDING",
"adminApproval": "PENDING",
"paymentStatus": "PENDING",
"totalAmount": 9.09
}
}
Your UI: Show “Order received — awaiting review by Dr Green.” Include the invoiceNumber as the customer-facing reference (Dr Green generates it; don’t make your own).
Stage 2 — admin review
A Dr Green admin reviews each order before it can proceed. This is a manual step and can take hours to days, especially during business off-hours.
After review, adminApproval flips:
| New value | What it means | Your action |
|---|
VERIFIED | Approved | Continue to payment if not already paid |
REJECTED | Not approved | Refund any payment via Dr Green; notify customer with reason if Dr Green provides one |
🔒 Reason for rejection — whether Dr Green surfaces a reason in the order detail isn’t formally documented. Defensively render: if you find a rejectionReason field, surface it; otherwise, “Your order could not be approved at this time.”
Stage 3 — payment
Payment is collected via the processor specified in paymentMethod at order time:
CRYPTO → CoinRemitter (BTC, ETH, USDT, etc.)
FIAT → Payinn (card / bank transfer depending on country)
PGPAY → PGPay (alternate fiat in supported regions)
🔒 Customer-facing payment UX — whether Dr Green provides a hosted checkout, sends a payment link via email, or relies on your store to render a checkout isn’t formally documented from the API surface. Confirm with Dr Green which model applies. The most common pattern is “Dr Green emails a payment link to the customer” — but verify before building.
When payment completes (the processor’s webhook fires to Dr Green), paymentStatus flips:
paymentStatus | Meaning |
|---|
COMPLETED | Paid; order proceeds to dispatch |
FAILED | Payment failed; customer can retry |
PROCESSING | In flight (e.g. crypto confirmations pending) |
REFUNDED | Money returned to customer |
Stage 4 — dispatch
When the order ships, orderStatus flips to SHIPPED. Dr Green handles the dispatch from their distribution partner (e.g. Takoda1 in the UK).
🔒 Tracking number visibility — whether the order detail exposes a courier tracking number or transactions[] array entries are not yet captured live. The order detail does include a transactions array which is empty in observed data; this may populate with shipping info, payment info, or both.
Stage 5 — delivered
orderStatus = DELIVERED is the terminal happy-path state. At this point, the holder’s commission for this order accrues — visible via /dapp/commissions.
How to detect transitions in your store
There are no outbound webhooks, so polling is your signal. The pattern:
Per-order polling (active orders)
async function trackOrder(orderId: string, intervalMs = 60_000) {
const TERMINAL = new Set(['DELIVERED', 'CANCELLED']);
let lastFingerprint = '';
while (true) {
const { orderDetails: o } = await drGreenClient.get<{
orderDetails: {
orderStatus: string;
adminApproval: string;
paymentStatus: string;
};
}>(`/dapp/orders/${orderId}`);
const fingerprint = `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`;
if (fingerprint !== lastFingerprint) {
await onStatusChange(orderId, o); // notify customer, update your DB
lastFingerprint = fingerprint;
}
if (TERMINAL.has(o.orderStatus) || o.adminApproval === 'REJECTED') return o;
// 60s ± 10% jitter
const jitter = 0.9 + 0.2 * Math.random();
await new Promise((r) => setTimeout(r, intervalMs * jitter));
}
}
Note data.orderDetails (not data directly) — order detail wraps in an extra orderDetails field, unlike most other endpoints.
Bulk polling (all active orders across customers)
For at-scale stores, use /dapp/orders/recent as a heartbeat:
async function heartbeatAllActiveOrders() {
const recent = await drGreenClient.get<{
recentOrders: Array<{ id: string; orderStatus: string; adminApproval: string; paymentStatus: string }>;
}>('/dapp/orders/recent');
for (const o of recent.recentOrders) {
const local = await db.orders.find(o.id);
if (!local) continue;
if (local.fingerprint !== `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`) {
await onStatusChange(o.id, o);
await db.orders.updateFingerprint(o.id, o);
}
}
}
// Run every 60s
cron.schedule('* * * * *', heartbeatAllActiveOrders);
recent returns the most recent ~10 orders without pagination — ideal for catching changes across customers cheaply.
For older orders (>1 day old, not in recent anymore), poll them individually but at lower frequency (e.g. every 30 min until terminal).
Recommended polling cadence
| Order age / state | Cadence |
|---|
Just placed, not yet VERIFIED | Every 5 min |
VERIFIED, awaiting payment | Every 5 min |
paymentStatus=COMPLETED, awaiting SHIPPED | Every 30 min |
SHIPPED, awaiting DELIVERED | Every 60 min |
Terminal (DELIVERED, CANCELLED, adminApproval=REJECTED) | Stop polling |
Customer notifications by transition
Suggested template for what to email/notify the customer at each transition:
| Transition | Notify | Tone |
|---|
| Order placed | ”We’ve received your order. Reference: <invoiceNumber>. Awaiting review.” | Confirmation |
adminApproval=VERIFIED, awaiting payment | ”Your order is approved. Please complete payment via the link sent by Dr Green.” | Action required |
paymentStatus=COMPLETED | ”Payment confirmed. Your order will ship shortly.” | Confirmation |
orderStatus=SHIPPED | ”Your order has been dispatched.” (include tracking if available) | Update |
orderStatus=DELIVERED | ”Delivered. Thank you!” | Closure |
adminApproval=REJECTED | ”We were unable to approve this order. Please contact support.” | Apology + escalation |
paymentStatus=FAILED | ”Your payment didn’t complete. You can retry via the link sent by Dr Green.” | Retry prompt |
Important: these are emails YOUR store sends, separate from any transactional emails Dr Green sends (KYC instructions, payment links). You own the customer relationship; Dr Green handles the parts that require their direct involvement.
Idempotency and retry semantics
POST /dapp/orders is not idempotent. There’s no Idempotency-Key header support. Naive retries produce duplicate orders.
The pattern that works
-
Generate a correlation ID client-side at checkout, store it in your DB before the API call:
const correlationId = crypto.randomUUID();
await db.orders.insert({ correlationId, customerId, items, status: 'pending_post' });
-
POST the order:
try {
const order = await drGreenClient.post('/dapp/orders', { ... });
await db.orders.update(correlationId, { drGreenOrderId: order.id, status: 'placed' });
} catch (e) {
// network error — DON'T blindly retry
await db.orders.update(correlationId, { status: 'unknown', error: e.message });
}
-
For any retry, first check Dr Green’s side:
if (order.status === 'unknown') {
// Look for orders placed in the last 5 minutes for this customer
const recent = await drGreenClient.get('/dapp/orders', {
clientId: customer.drGreenClientId,
page: 1,
limit: 20,
});
const match = recent.orders.find(o =>
Math.abs(o.totalAmount - expectedTotal) < 0.01 &&
Date.parse(o.createdAt) > Date.now() - 5 * 60 * 1000
);
if (match) {
// Order DID land — recover state, don't re-POST
await db.orders.update(correlationId, { drGreenOrderId: match.id, status: 'placed' });
} else {
// Order didn't land — safe to re-POST
const order = await drGreenClient.post('/dapp/orders', { ... });
await db.orders.update(correlationId, { drGreenOrderId: order.id, status: 'placed' });
}
}
This is verbose but safe. If you find yourself implementing this, please also raise the gap with Dr Green — Idempotency-Key support is a reasonable backend ask.
Pricing and currency
The order detail returns prices in two currencies:
totalAmount (USD) — accounting / reporting figure
localPrice.totalAmount (customer’s local currency) — what the customer was charged
{
"totalAmount": 9.09,
"localPrice": {
"currency": "ZAR",
"totalAmount": 165,
"totalProfit": 66,
"wholeSalePrice": 99,
"retailPrice": 165
}
}
For customer-facing UI: always use localPrice.currency + localPrice.totalAmount.
For internal reporting / accounting: use totalAmount (USD).
The exchange rate is captured at order time and frozen for that order — even if FX moves, the customer is charged the locked-in localPrice.totalAmount.
Anti-patterns to avoid
| Anti-pattern | Why bad |
|---|
Treating orderStatus=PENDING as “no progress” | The order is awaiting admin review — that IS progress |
| Polling every order every minute | Wastes API calls; your customers don’t need second-by-second updates |
Showing customer the USD totalAmount | They paid in their local currency; show them localPrice.totalAmount |
Auto-cancelling orders that sit in PENDING for 24h | Dr Green admin may legitimately take 24+ hours |
| Sending status notification on every poll | Only notify on transitions (use the fingerprint pattern above) |
| Creating an order, then later POSTing to “update status” | There’s no PATCH for orders; status changes are server-side only |
Trusting data directly on order-detail responses | It wraps in orderDetails — response.data.orderDetails, not response.data |
Where to next