> ## 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.

# 06 webhooks

# Webhooks

> **TL;DR.** Dr Green has **inbound** webhook endpoints that its payment and KYC processors call into. As an external store builder, you are **not** the consumer of these — they're between Dr Green and its third-party services. There is currently **no outbound webhook system** for stores: your store must poll the relevant endpoints to detect order, KYC, and commission state changes.

***

## What this means for your store

If you've built integrations with Stripe, Shopify, or PayPal, you're used to the pattern:

```
[Customer pays] → [Stripe receives event] → [Stripe POSTs to your /stripe-webhook] → [You react]
```

Dr Green doesn't currently work that way for store builders. The pattern is:

```
[Customer pays via your store]
  → [Your store POSTs /dapp/orders to Dr Green]
  → [Dr Green admins approve internally] (asynchronous, hours to days)
  → [You poll GET /dapp/orders/:orderId] (every N minutes)
  → [You detect status change] → [You notify your customer]
```

This is functional but inefficient. We've flagged this as a roadmap gap to the Dr Green backend team. See [§ The polling pattern](#the-polling-pattern) below for the workaround.

***

## Inbound webhooks (between Dr Green and its processors — you can ignore these)

For completeness, the four webhook endpoints that exist on the Dr Green API:

| Endpoint                              | Caller       | Purpose                                                            |
| ------------------------------------- | ------------ | ------------------------------------------------------------------ |
| `POST /api/v1/kyc/webhook`            | FirstAML     | KYC/AML verification status updates from FirstAML back to Dr Green |
| `POST /api/v1/payments/webhook`       | CoinRemitter | Crypto payment notifications                                       |
| `POST /api/v1/payments/fiat/webhook`  | Payinn       | Fiat payment notifications                                         |
| `POST /api/v1/payments/pgpay/webhook` | PGPay        | Alternate fiat payment gateway notifications                       |

These endpoints are **inbound to Dr Green** — they receive POSTs from third-party services. The payload format is dictated by each upstream processor, not by Dr Green, and is not declared in the OpenAPI spec.

> **As a store builder, you don't call these and they don't call you.** They're listed here only so you understand the full surface — and so you don't accidentally try to use them.

### How the FirstAML KYC flow actually works

This is the most-asked-about flow, so worth spelling out clearly:

```
[Your store] ──POST /dapp/clients──▶ [Dr Green backend]
                                            │
                                            ├─ (1) sends an email to the customer with KYC instructions
                                            │
                                            └─ (2) fires an outbound webhook to FirstAML to open a case
                                                                                        │
                                                                                        ▼
                                                                                [FirstAML runs verification]
                                                                                with the customer (out-of-band —
                                                                                ID upload, liveness, AML checks)
                                                                                        │
                                                                                        ▼
                                            ┌──POST /api/v1/kyc/webhook──── [FirstAML]
                                            │
                                            ▼
                              [Dr Green updates client.isKYCVerified
                               and client.adminApproval]
                                            │
                                            ▼
[Your store] ◀──poll GET /dapp/clients/{id}──┘
```

**Key takeaways for store builders:**

* Your store **does not integrate with FirstAML**. You don't have FirstAML credentials, you don't render their UI, you don't receive their callbacks.
* The outbound Dr Green → FirstAML webhook (step 2 above) is a separate, internal flow — not exposed in the public API surface.
* The customer interacts with FirstAML directly (typically via a hosted verification portal linked from the email Dr Green sends).
* All you do is `POST /dapp/clients` to start the flow, then poll `GET /dapp/clients/{clientId}` until `isKYCVerified === true` and `adminApproval === 'VERIFIED'`.

🔒 **Inbound signing.** The Dr Green backend should be verifying signatures on these inbound webhooks (FirstAML-specific signing scheme, plus IPN/HMAC from the payment processors). This was flagged for backend-team verification; status pending. Not your problem to solve, but worth being aware of when discussing platform security with Dr Green.

***

## The polling pattern (what your store actually does)

Until outbound webhooks exist, build your store around polling. Here's a battle-tested pattern.

### What to poll

| Concern                            | Endpoint                               | Why                                      |
| ---------------------------------- | -------------------------------------- | ---------------------------------------- |
| Order status change (the big one)  | `GET /api/v1/dapp/orders/:orderId`     | Detect "approved", "rejected", "shipped" |
| KYC status change for a customer   | `GET /api/v1/dapp/clients/:clientId`   | Detect verification completion           |
| Recent orders across all customers | `GET /api/v1/dapp/orders/recent`       | Cheap heartbeat — surfaces anything new  |
| Commission accrual                 | `GET /api/v1/dapp/commissions/summary` | Update holder's earnings widget          |
| Strain catalogue freshness         | `GET /api/v1/dapp/strains`             | Detect new products and price changes    |

### How to poll efficiently

**Pattern: tiered polling intervals.**

| Resource freshness                                 | Poll interval | Justification                                                             |
| -------------------------------------------------- | ------------- | ------------------------------------------------------------------------- |
| Order status (active orders only)                  | 60s           | Customers tolerate \~1min staleness on order status                       |
| Order status (orders >7 days old, not yet shipped) | 30 min        | Long-tail; rarely changes                                                 |
| KYC status (pending)                               | 5 min         | FirstAML verification timing varies by case complexity (minutes to hours) |
| Commission summary                                 | 15 min        | Updates aren't time-critical                                              |
| Strain catalogue                                   | 1 hour        | Catalogue is stable; refresh on cache miss                                |

**Pattern: only poll active items.**

Don't poll for orders that are already in a terminal state (`shipped`, `cancelled`, `rejected`). Maintain a list of "active" order IDs in your store DB; remove them once they reach a terminal status.

**Pattern: jittered polling.**

If your store has many holders, don't poll all of them on the same cron-second — you'll create thundering herds against the Dr Green API. Add ±10% random jitter to every interval.

```python theme={null}
# Minimal polling implementation
import asyncio, random

async def poll_active_orders(client, db):
    while True:
        active_order_ids = await db.get_active_order_ids()
        for oid in active_order_ids:
            try:
                resp = await client.get(f'/dapp/orders/{oid}')
                order = resp.data
                if order['status'] != await db.get_cached_status(oid):
                    await db.update_status(oid, order['status'])
                    await notify_customer(order)
                    if order['status'] in {'shipped', 'cancelled', 'rejected'}:
                        await db.deactivate(oid)
            except Exception as e:
                log.warning(f'Poll failed for {oid}: {e}')
        # 60s ± 10% jitter
        await asyncio.sleep(60 * (0.9 + 0.2 * random.random()))
```

### What not to do

* **Don't poll every order on every tick.** Use the active-set pattern above.
* **Don't poll faster than 1 req/sec per holder.** The backend isn't currently rate-limited but politeness avoids future enforcement pain.
* **Don't poll without backoff on errors.** A backend hiccup shouldn't get amplified by your retry storm. Exponential backoff on consecutive failures.

***

## Future outbound webhook design (when it ships)

When Dr Green ships outbound webhooks for store builders, the design we'd recommend (and have flagged) looks like:

* **HMAC-SHA256 signing** of payload with a per-store secret you configure in the DAPP
* **Replay-protection timestamp** in the signature payload
* **Standard event types:** `order.created`, `order.approved`, `order.rejected`, `order.shipped`, `client.kyc.completed`, `commission.accrued`
* **At-least-once delivery** with idempotency on the receiving side keyed by event ID
* **Retry policy** of \~5 attempts over 24 hours

This isn't the current state — it's what we'd advocate for. If Dr Green ships outbound webhooks with a different design, this doc will be updated.

***

## Summary

| Question                                      | Answer                                                                                                                                                  |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Does Dr Green call my store on events?        | ❌ Not currently.                                                                                                                                        |
| Do I need to expose a webhook endpoint?       | ❌ No.                                                                                                                                                   |
| How do I detect order status changes?         | Poll `GET /api/v1/dapp/orders/:orderId`.                                                                                                                |
| What about FirstAML KYC results?              | Poll `GET /api/v1/dapp/clients/:clientId`. The KYC flow is handled entirely between Dr Green and FirstAML — your store doesn't integrate with FirstAML. |
| Are the `/payments/webhook` endpoints for me? | ❌ No — those are between Dr Green and CoinRemitter / Payinn / PGPay.                                                                                    |
| When will outbound webhooks ship?             | 🔒 Not on a published roadmap. Worth requesting from Dr Green.                                                                                          |
