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.
Orders
Endpoint reference: ✅ Verified live against production on 10 May 2026.
Orders are the conversion event in your store — when a customer’s cart becomes a purchase. The Dr Green admin team manually approves each order before it ships, so expect an asynchronous lifecycle: place → pending → admin-verified → paid → shipped.
Order lifecycle (the high-level flow)
[Customer checks out]
│
▼
POST /dapp/orders ← your store's call
│
▼
order.orderStatus = PENDING
order.adminApproval = PENDING
order.paymentStatus = PENDING
│
▼
[Dr Green admin reviews] ← manual, hours to days
│
├── REJECTED → adminApproval=REJECTED (terminal — refund, notify customer)
└── VERIFIED → adminApproval=VERIFIED
▼
[Customer pays via processor]
▼
paymentStatus = COMPLETED
▼
[Dr Green ships] (no separate shipped status currently surfaced)
There’s no outbound webhook for status changes — your store must poll. See 06-webhooks.md § The polling pattern.
Endpoints
| Method | Path | Auth | Description |
|---|
POST | /dapp/orders | API-key + sig | Create a new order from a cart |
GET | /dapp/orders | API-key + sig | List orders, paginated |
GET | /dapp/orders/recent | API-key + sig | Recently-created orders (heartbeat polling target) |
GET | /dapp/orders/summary | API-key + sig | Order status counts |
GET | /dapp/orders/chart-data | API-key + sig | Time-series, requires date range |
GET | /dapp/orders/status-breakdown | API-key + sig | Status breakdown over date range |
GET | /dapp/orders/{orderId} | API-key + sig | Full order detail |
Status enums
| Field | Values | Set by |
|---|
orderStatus | PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED | Server-side based on lifecycle |
adminApproval | PENDING, VERIFIED, REJECTED | Dr Green admin |
paymentStatus | PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED | Payment processor webhook |
🔒 The complete enum lists above are inferred from observed values (PENDING, VERIFIED, REJECTED confirmed live). Other values are likely supported but not yet captured. Defensively handle unknown statuses — render them as the raw string.
POST /dapp/orders — create an order
Creates an order from explicit line items + shipping + (optionally) the cart it came from. The shipping address must be one of the client’s shippings[] (referenced by id).
Request body
The shape follows CreateOrderDto. Verified pattern:
{
"clientId": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
"shippingId": "167d30ee-52d1-48eb-b501-b1203ad72074",
"orderLines": [
{ "strainId": "c77dd198-ec9a-4ced-8105-937fde104424", "quantity": 1 }
],
"paymentMethod": "CRYPTO"
}
| Field | Type | Required | Notes |
|---|
clientId | string (UUID) | ✅ | Must be adminApproval=VERIFIED, isKYCVerified=true, isActive=true |
shippingId | string (UUID) | ✅ | One of client.shippings[].id |
orderLines | array | ✅ | At least one line |
orderLines[].strainId | string (UUID) | ✅ | |
orderLines[].quantity | integer | ✅ | Grams; positive integer |
paymentMethod | string | ✅ | CRYPTO (CoinRemitter), FIAT (Payinn), PGPAY (PGPay) — confirm with Dr Green which are enabled per country |
cartId | string (UUID) | No | Reference to the cart that fed this order, if any |
notes | string | No | Free-text |
Canonical payload (for signing)
JSON.stringify(body) — compact, no whitespace.
Response
201 Created with the order’s id and invoiceNumber. The invoiceNumber is a 32-char random string Dr Green generates server-side — use it as your customer-facing reference. Don’t generate your own.
{
"success": true,
"statusCode": 201,
"message": "Success",
"data": {
"id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
"invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
"totalAmount": 9.09,
"orderStatus": "PENDING",
"adminApproval": "PENDING",
"paymentStatus": "PENDING"
}
}
Idempotency warning
POST /dapp/orders is not idempotent. A retry creates a duplicate order. Two patterns to avoid this:
Pattern 1 — pre-flight check. Before any retry, query GET /dapp/orders?page=1&limit=20 and look for a recent order matching the customer + total amount. If found, treat your original POST as having succeeded.
Pattern 2 — your-side correlation ID. Generate a UUID at checkout, store order_in_flight: <uuid> in your store DB, only POST once. On any retry, check if the previous attempt eventually returned a 2xx (which you’d have logged) before retrying.
See 04-errors.md § Idempotency for the full discussion.
Errors
| Status | Cause | Fix |
|---|
400 ["clientId must be a UUID"] | Validation | Validate input |
400 “Client not verified” / similar | Client isn’t VERIFIED + KYC + active | Surface to customer; can’t recover |
400 “Shipping not found for client” | shippingId doesn’t belong to this client | Re-fetch client and use a valid shipping id |
400 “Strain not available in client’s country” | Strain availableCountries doesn’t include client’s country | Filter strains to client’s country before placing order |
GET /dapp/orders — list orders
Paginated list of all orders for the holder.
Query parameters
| Name | Type | Default | Notes |
|---|
page | integer | 1 | |
limit | integer | 10 | |
status | string | — | Filter by orderStatus |
adminApproval | string | — | Filter by adminApproval |
clientId | string (UUID) | — | Filter to one client (or use /dapp/client/{id}/orders) |
search | string | — | Free-text on invoiceNumber |
Response shape (verified)
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"orders": [
{
"id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
"invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
"orderStatus": "PENDING",
"adminApproval": "VERIFIED",
"paymentStatus": "PENDING",
"totalAmount": 9.09,
"shipping": {
"country": "South Africa",
"countryCode": "ZAF",
"currency": null
},
"transactions": [],
"createdAt": "2025-05-12T17:29:57.100Z",
"nft": { "tokenId": 56, "ownerId": "0x553374bd..." },
"client": {
"id": "f1281910-f463-4da9-92c0-feb81a9db994",
"firstName": "Jacques",
"lastName": "De Reuck",
"shippings": [ { "countryCode": "ZAF" } ]
},
"_count": { "orderLines": 1 },
"totalQuantity": 1,
"totalPrice": 9.09,
"localPrice": { "currency": "ZAR", "totalAmount": 165 }
}
],
"pageMetaDto": { "page": "1", "take": 10, "itemCount": 21, "pageCount": 3, "hasPreviousPage": false, "hasNextPage": true }
}
}
🪲 _count is a Prisma-leaked field — it’s { orderLines: <count> }. Treat it as informational, not authoritative.
🪲 totalAmount, totalPrice, and localPrice.totalAmount are different things:
totalAmount and totalPrice are the same USD figure (mirroring of fields)
localPrice.totalAmount is the customer-currency equivalent (e.g. ZAR for South Africa)
Use localPrice for display, totalAmount for accounting.
GET /dapp/orders/recent — recent orders
Same shape as GET /dapp/orders but always returns the most recent 10 (no pagination needed). Use this as a heartbeat polling target — cheap call that surfaces anything new across all customers.
Canonical payload
{}
GET /dapp/orders/summary — status counts
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"summary": {
"PENDING": 0,
"VERIFIED": 11,
"REJECTED": 10,
"totalCount": 21
}
}
}
The keys are adminApproval values (not orderStatus). For an orderStatus breakdown over time, use /dapp/orders/status-breakdown with a date range.
GET /dapp/orders/chart-data and /status-breakdown
Both require startDate and endDate in YYYY-MM-DD format. Without them: 400.
Canonical payload (with date range)
startDate=2026-04-01&endDate=2026-05-01
🔒 Response shapes pending live capture with valid date ranges.
GET /dapp/orders/{orderId} — order detail
Full detail including line items with embedded strain info, transactions, and the multi-currency price breakdown.
Canonical payload
{}
Response shape (verified)
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"orderDetails": {
"id": "603b3f1a-4cab-44af-972d-a26a77fbdff9",
"invoiceNumber": "XUnt8v3wYkUWcTEEKeuYtPz1ia0SBNmN",
"orderStatus": "PENDING",
"paymentStatus": "PENDING",
"totalAmount": 9.09,
"deliveryFee": 6,
"totalProfit": 3.59,
"totalOrdered": 1,
"totalQuantity": 1,
"createdAt": "2025-05-12T17:29:57.100Z",
"updatedAt": "2025-05-13T13:05:08.284Z",
"transactions": [],
"nft": { "tokenId": 56, "ownerId": "0x553374bd..." },
"client": {
"id": "f1281910-f463-4da9-92c0-feb81a9db994",
"firstName": "Jacques",
"lastName": "De Reuck",
"phoneCode": "+27",
"contactNumber": "832854483",
"shippings": [ /* full shipping details */ ]
},
"orderLines": [
{
"quantity": 1,
"strain": {
"id": "c77dd198-ec9a-4ced-8105-937fde104424",
"name": "Femme Fatale",
"imageUrl": "33eac80b-58c4-46d3-a82b-b70c875d333f-cakes-and-cream.png"
}
}
],
"localPrice": {
"currency": "ZAR",
"totalAmount": 165,
"totalProfit": 66,
"wholeSalePrice": 99,
"retailPrice": 165,
"location": {
"country": "South Africa",
"currency": "ZAR"
}
}
}
}
}
⚠️ The order detail wraps the data inside an extra orderDetails field — i.e. data.orderDetails, not data directly. Other endpoints don’t wrap like this. Mind the inconsistency.
🪲 totalAmount is in USD, localPrice.totalAmount is in the customer’s local currency. Don’t render totalAmount to the customer — use localPrice.totalAmount with localPrice.currency.
🪲 totalOrdered and totalQuantity are the same number in observed responses. The duplication is a backend wart.
Common patterns
Polling order status
async def poll_order_until_terminal(order_id: str):
terminal = {"DELIVERED", "CANCELLED"}
while True:
order = await get_order(order_id)
details = order["orderDetails"]
if details["orderStatus"] in terminal or details["adminApproval"] == "REJECTED":
return details
await asyncio.sleep(60 + random.uniform(-6, 6)) # 60s ± 10% jitter
Surface multi-currency totals
function displayTotal(order: OrderDetails): string {
const lp = order.localPrice;
if (lp?.currency && lp.totalAmount != null) {
return new Intl.NumberFormat(undefined, { style: 'currency', currency: lp.currency }).format(lp.totalAmount);
}
return `$${order.totalAmount.toFixed(2)} USD`;
}
Detecting status changes between polls
function statusFingerprint(o: OrderDetails): string {
return `${o.orderStatus}|${o.adminApproval}|${o.paymentStatus}`;
}
// Fire customer notifications only when fingerprint changes
Caching guidance
| Resource | TTL | Rationale |
|---|
| Active order detail | 30s | Customer expects ~real-time feel |
| Terminal-status order detail | 5m | Doesn’t change |
| Order list (paginated) | No cache | Always fresh |
| Order summary counts | 1m | Dashboards |
See also
- Carts — orders are typically born from carts
- Clients — orders are scoped to clients
- Strains —
orderLines[].strain fields and pricing snapshot
- Sales — orders may be associated with a sales pipeline entry
- 06-webhooks.md — why you have to poll