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.

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

MethodPathAuthDescription
POST/dapp/ordersAPI-key + sigCreate a new order from a cart
GET/dapp/ordersAPI-key + sigList orders, paginated
GET/dapp/orders/recentAPI-key + sigRecently-created orders (heartbeat polling target)
GET/dapp/orders/summaryAPI-key + sigOrder status counts
GET/dapp/orders/chart-dataAPI-key + sigTime-series, requires date range
GET/dapp/orders/status-breakdownAPI-key + sigStatus breakdown over date range
GET/dapp/orders/{orderId}API-key + sigFull order detail

Status enums

FieldValuesSet by
orderStatusPENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLEDServer-side based on lifecycle
adminApprovalPENDING, VERIFIED, REJECTEDDr Green admin
paymentStatusPENDING, PROCESSING, COMPLETED, FAILED, REFUNDEDPayment 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"
}
FieldTypeRequiredNotes
clientIdstring (UUID)Must be adminApproval=VERIFIED, isKYCVerified=true, isActive=true
shippingIdstring (UUID)One of client.shippings[].id
orderLinesarrayAt least one line
orderLines[].strainIdstring (UUID)
orderLines[].quantityintegerGrams; positive integer
paymentMethodstringCRYPTO (CoinRemitter), FIAT (Payinn), PGPAY (PGPay) — confirm with Dr Green which are enabled per country
cartIdstring (UUID)NoReference to the cart that fed this order, if any
notesstringNoFree-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

StatusCauseFix
400 ["clientId must be a UUID"]ValidationValidate input
400 “Client not verified” / similarClient isn’t VERIFIED + KYC + activeSurface to customer; can’t recover
400 “Shipping not found for client”shippingId doesn’t belong to this clientRe-fetch client and use a valid shipping id
400 “Strain not available in client’s country”Strain availableCountries doesn’t include client’s countryFilter strains to client’s country before placing order

GET /dapp/orders — list orders

Paginated list of all orders for the holder.

Query parameters

NameTypeDefaultNotes
pageinteger1
limitinteger10
statusstringFilter by orderStatus
adminApprovalstringFilter by adminApproval
clientIdstring (UUID)Filter to one client (or use /dapp/client/{id}/orders)
searchstringFree-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

ResourceTTLRationale
Active order detail30sCustomer expects ~real-time feel
Terminal-status order detail5mDoesn’t change
Order list (paginated)No cacheAlways fresh
Order summary counts1mDashboards

See also

  • Carts — orders are typically born from carts
  • Clients — orders are scoped to clients
  • StrainsorderLines[].strain fields and pricing snapshot
  • Sales — orders may be associated with a sales pipeline entry
  • 06-webhooks.md — why you have to poll