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.
Clients (Customers)
Endpoint reference: ✅ Verified live against production on 10 May 2026.
“Clients” in Dr Green parlance are your store’s end customers. They’re scoped to the holder’s primary NFT — so all clients you create via your store belong to the holder whose apiKey you’re using. KYC, addresses, business details, and medical records all hang off the client record.
Endpoints
| Method | Path | Auth | Description |
|---|
POST | /dapp/clients | API-key + sig | Create a client (KYC submission) |
GET | /dapp/clients | API-key + sig | List clients, paginated |
GET | /dapp/clients/list | API-key + sig | List clients without pagination wrapper (for selectors) |
GET | /dapp/clients/summary | API-key + sig | Status counts (PENDING / VERIFIED / REJECTED / total) |
GET | /dapp/clients/chart-data | API-key + sig | Time-series data for charts (date-range required) |
GET | /dapp/clients/status-breakdown | API-key + sig | Status breakdown over date range |
GET | /dapp/clients/export | API-key + sig | CSV export |
GET | /dapp/clients/{clientId} | API-key + sig | Full detail for one client |
PATCH | /dapp/clients/{clientId} | API-key + sig | Update client fields |
GET | /dapp/client/{clientId}/orders | API-key + sig | Orders for one client |
GET | /dapp/client/{clientId}/transactions | API-key + sig | Transactions for one client |
GET | /dapp/clients/{clientId}/orders/{orderId} | API-key + sig | One specific order for a client |
🪲 Naming inconsistency in the API: sub-routes are split between /dapp/clients/{clientId}/... and /dapp/client/{clientId}/... (singular vs plural). This is a backend wart — not a typo in this doc. Use whichever path each endpoint expects.
Status enums
Clients have three relevant status fields:
| Field | Values | Set by |
|---|
adminApproval | PENDING, VERIFIED, REJECTED | Dr Green admin team after review |
isKYCVerified | true, false | FirstAML callback (Dr Green ↔ FirstAML; store has no direct involvement) |
isActive | true, false | Holder (soft-disable a client) |
A client must be adminApproval: VERIFIED AND isKYCVerified: true AND isActive: true before they can place orders. New clients land in PENDING and require human review.
POST /dapp/clients — create a client
Submit a new client for KYC review. The Dr Green admin team reviews each client manually before they can transact.
Request body
The CreateClientDto is the input shape. Verified-required and most-common fields:
{
"firstName": "Ava",
"lastName": "Nguyen",
"email": "ava@example.com",
"phoneCode": "+44",
"contactNumber": "7700900123",
"shipping": {
"address1": "12 Baker Street",
"address2": "",
"city": "London",
"state": "England",
"country": "United Kingdom",
"countryCode": "GBR",
"postCode": "NW1 6XE"
}
}
| Field | Type | Required | Notes |
|---|
firstName | string | ✅ | |
lastName | string | ✅ | |
email | string | ✅ | Validated as RFC 5322; must be unique per holder |
phoneCode | string | ✅ | E.g. "+44", "+1" |
contactNumber | string | ✅ | Digits only, no spaces |
shipping | object | ✅ | Use CreateShippingDto shape, country code in alpha-3 |
medicalRecord | object | string | ⚠️ | Required if customer is purchasing for medical reasons; format depends on country |
clientBusiness | object | No | Use CreateClientBusinessDto if B2B |
Canonical payload (for signing)
JSON.stringify(body) — compact, no whitespace.
Response
201 Created with the new client’s id in data. Detailed shape:
{
"success": true,
"statusCode": 201,
"message": "Client created successfully",
"data": { "id": "<new-uuid>" }
}
Errors
| Status | Cause | Fix |
|---|
400 ["email must be a valid email"] | Invalid email format | Validate client-side first |
400 ["countryCode must be a valid ISO 3166-1 alpha-3 country code"] | Sent "GB" instead of "GBR" | Use alpha-3 |
409 Conflict (email duplicate) | Email already exists for this holder | Surface to user, let them log in instead |
GET /dapp/clients — list clients
Paginated list of all clients owned by the current holder.
Query parameters
| Name | Type | Required | Notes |
|---|
page | integer | No (default 1) | |
limit | integer | No (default 10) | |
search | string | No | Free-text on name / email |
status | string | No | Filter by adminApproval (e.g. PENDING) |
kyc | boolean | No | Filter on isKYCVerified |
Canonical payload
urlencode(query) if any params present, else "{}".
Response shape (verified)
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"clients": [
{
"id": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
"firstName": "Dheeraj",
"lastName": "Kumar",
"email": "dheeraj.k+11@rejolut.com",
"phoneCountryCode": null,
"phoneCode": "+91",
"contactNumber": "6394234342",
"isActive": true,
"adminApproval": "VERIFIED",
"isKYCVerified": false,
"verifiedAt": "2025-05-14T09:28:04.804Z",
"rejectedAt": null,
"nft": { "tokenId": 56, "ownerId": "0x553374bd6e81f952..." },
"shippings": [ { "country": "United States", "currency": null } ],
"createdAt": "2025-02-26T05:49:11.465Z",
"updatedAt": "2025-05-14T09:28:04.804Z",
"clientCart": [ { "id": "0a44cc00-3f2c-45b3-a09d-919ba4d8f5bb" } ]
}
],
"pageMetaDto": {
"page": "1", "take": 10, "itemCount": 19,
"pageCount": 2, "hasPreviousPage": false, "hasNextPage": true
}
}
}
🪲 phoneCountryCode and phoneCode are different fields. phoneCountryCode is sometimes null while phoneCode ("+91") holds the dial prefix. Use phoneCode + contactNumber for E.164.
🪲 verifiedAt and rejectedAt appear in the list response but not in the detail response. If you need them, capture them at list time.
Worked example (Node)
async function listClients(page = 1, limit = 10) {
const query = { page: String(page), limit: String(limit) };
const payload = canonicalPayload('GET', null, query);
const headers = authHeaders(API_KEY, SECRET_KEY, payload);
const url = new URL('https://api.drgreennft.com/api/v1/dapp/clients');
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url, { headers });
return (await res.json()).data; // { clients, pageMetaDto }
}
GET /dapp/clients/list — unpaginated list
Same data as the paginated list but without pageMetaDto and without server-side limit. Use for <select> dropdowns where you need every client at once.
Canonical payload
{} (no query params).
Response
{ "data": { "clients": [ /* all clients */ ] } }
⚠️ Don’t use this for catalogue pages. It returns the full list and gets expensive at scale. Only use for autocomplete / picker UIs.
GET /dapp/clients/summary — status counts
Top-line counts for dashboards.
Canonical payload
{}
Response (verified)
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"summary": {
"PENDING": 11,
"VERIFIED": 7,
"REJECTED": 1,
"totalCount": 19
}
}
}
The keys are the adminApproval values plus totalCount. Useful for a “tabs with counts” UI.
GET /dapp/clients/chart-data and /status-breakdown — time-series
Both endpoints require startDate and endDate query parameters. Without them you get a 400.
Required query parameters
| Name | Format | Notes |
|---|
startDate | YYYY-MM-DD or MM-DD-YYYY | Inclusive |
endDate | YYYY-MM-DD or MM-DD-YYYY | Inclusive |
Error if omitted (verified)
{
"statusCode": 400,
"message": "startdate must be a valid date. ex: YYYY-MM-DD or MM-DD-YYYY, startDate should not be empty, endDate must be a valid date. ex: YYYY-MM-DD or MM-DD-YYYY, endDate should not be empty"
}
🪲 The error message has typo’d casing — it says “startdate” lowercase but the actual field is startDate. The validator will accept correctly-cased query params.
Canonical payload
startDate=2026-04-01&endDate=2026-05-01
🔒 Response shape for these is not yet captured live — pending a date-range probe. Update once verified.
GET /dapp/clients/{clientId} — full client detail
The complete record including shipping addresses, businesses, KYC, NFT linkage, and cart references.
Canonical payload
{}
Response shape (verified)
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": {
"id": "2cbab026-9dc6-46fd-8580-77e3dfd2f086",
"firstName": "Dheeraj",
"lastName": "Kumar",
"email": "dheeraj.k+11@rejolut.com",
"phoneCountryCode": null,
"phoneCode": "+91",
"contactNumber": "6394234342",
"isActive": true,
"nft": { "tokenId": 56, "ownerId": "0x553374bd6e81f952..." },
"adminApproval": "VERIFIED",
"isKYCVerified": false,
"createdAt": "2025-02-26T05:49:11.465Z",
"updatedAt": "2025-05-14T09:28:04.804Z",
"clientCart": [ { "id": "0a44cc00-3f2c-45b3-a09d-919ba4d8f5bb" } ],
"shippings": [
{
"id": "167d30ee-52d1-48eb-b501-b1203ad72074",
"address1": "F-203 Test address",
"address2": "",
"landmark": "",
"city": "...",
"state": "...",
"country": "...",
"countryCode": "USA",
"postCode": "..."
}
],
"clientBusinesses": [
{
"id": "7f7c1ec9-2006-4593-acfc-903ec76e64a8",
"name": "",
"businessType": "",
"address1": "", "address2": "", "landmark": ""
}
],
"medicalRecord": null
}
}
🪲 shippings and clientBusinesses are arrays — a client can have multiple. The order in the array is not guaranteed; sort/select by id or isPrimary when implementing.
PATCH /dapp/clients/{clientId} — update a client
Updates a subset of fields on an existing client.
Request body
Use UpdateClientDto. All fields are optional; only the ones you send will be updated:
{
"firstName": "Ava",
"isActive": true
}
To update shipping or business records, use the dedicated endpoints (TODO — UpdateShippingDto and UpdateClientBusinessDto flow not yet documented).
Canonical payload
JSON.stringify(body) — compact.
Response
200 OK with data.id of the updated client.
GET /dapp/client/{clientId}/orders — client’s orders
🪲 Note: path is /dapp/client/ (singular), not /dapp/clients/. Catch this in your routing.
Canonical payload
{} if no query, else urlencode(query). Supports page and limit.
Response
Same shape as GET /dapp/orders, but scoped to one client.
{ "data": { "orders": [...], "pageMetaDto": {...} } }
GET /dapp/client/{clientId}/transactions — client’s transactions
🪲 Same singular-path warning.
Response
{ "data": { "transactions": [...], "pageMetaDto": {...} } }
Shape of each transaction record is not yet captured live. 🔒
Common patterns
Surfacing approval status to the customer
function readableStatus(c: Client): string {
if (!c.isActive) return 'Account disabled';
if (c.adminApproval === 'REJECTED') return 'Application rejected';
if (c.adminApproval === 'PENDING') return 'Pending review by Dr Green';
if (!c.isKYCVerified) return 'KYC verification required';
return 'Verified — can place orders';
}
Polling for KYC completion
After a client submits, poll the detail endpoint every 5 minutes until isKYCVerified === true or adminApproval === 'REJECTED'. FirstAML verification timing varies by case complexity — straightforward cases complete in minutes, more involved AML reviews can take hours. Don’t poll faster than 5 minutes; it adds load without speeding anything up. See guides/kyc-flow.md for the full architecture.
Treating client identifiers
Always use the UUID id field server-side. Email is not unique across holders (different holders can have customers with the same email). Don’t use email as a primary key in your store DB.
See also