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.

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.
FieldSet byValuesMeaning
orderStatusDr Green internal logicPENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLEDWhere the order is in the fulfilment pipeline
adminApprovalDr Green admin teamPENDING, VERIFIED, REJECTEDManual review outcome
paymentStatusPayment processor webhook (CoinRemitter / Payinn / PGPay)PENDING, PROCESSING, COMPLETED, FAILED, REFUNDEDDid 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 valueWhat it meansYour action
VERIFIEDApprovedContinue to payment if not already paid
REJECTEDNot approvedRefund 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:
paymentStatusMeaning
COMPLETEDPaid; order proceeds to dispatch
FAILEDPayment failed; customer can retry
PROCESSINGIn flight (e.g. crypto confirmations pending)
REFUNDEDMoney 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).
Order age / stateCadence
Just placed, not yet VERIFIEDEvery 5 min
VERIFIED, awaiting paymentEvery 5 min
paymentStatus=COMPLETED, awaiting SHIPPEDEvery 30 min
SHIPPED, awaiting DELIVEREDEvery 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:
TransitionNotifyTone
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

  1. 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' });
    
  2. 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 });
    }
    
  3. 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-patternWhy bad
Treating orderStatus=PENDING as “no progress”The order is awaiting admin review — that IS progress
Polling every order every minuteWastes API calls; your customers don’t need second-by-second updates
Showing customer the USD totalAmountThey paid in their local currency; show them localPrice.totalAmount
Auto-cancelling orders that sit in PENDING for 24hDr Green admin may legitimately take 24+ hours
Sending status notification on every pollOnly 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 responsesIt wraps in orderDetailsresponse.data.orderDetails, not response.data

Where to next