File size: 6,312 Bytes
d705bb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | ---
title: "Commerce Endpoints"
description: "Checkout, customer portal, product catalog, and referral endpoints — thin edge proxies over Convex and Dodo Payments for World Monitor billing."
---
WorldMonitor uses [Dodo Payments](https://dodopayments.com) for PRO subscriptions and [Convex](https://convex.dev) as the source-of-truth for entitlements. These edge endpoints are thin auth proxies — they validate the Clerk JWT, then forward to Convex HTTP actions via `RELAY_SHARED_SECRET`.
## Checkout
### `POST /api/create-checkout`
Creates a Dodo checkout session and returns the hosted-checkout URL.
- **Auth**: Clerk bearer (required)
- **Body**:
```json
{ "productId": "pro-monthly", "returnUrl": "https://www.worldmonitor.app/pro/success" }
```
- **Response**: `{ "checkoutUrl": "https://checkout.dodopayments.com/..." }`
- **Idempotency**: optional `Idempotency-Key` supported for 10 minutes after a successful response. Retrying the same key with an identical body replays the original checkout response instead of creating another checkout attempt.
- **returnUrl** is validated against an allowlist on the Convex side.
### `POST /api/customer-portal`
Creates a Dodo customer-portal session for an existing subscriber (update card, cancel, view invoices).
- **Auth**: Clerk bearer + active entitlement
- **Response**: `{ "portalUrl": "..." }`
- **Idempotency**: optional `Idempotency-Key` supported. Retrying the same key replays the original portal response instead of creating another portal attempt.
## Product catalog
### `GET /api/product-catalog`
Returns the tier view-model used by the `/pro` pricing page. Cached in Redis under `product-catalog:v3` for 1 hour; on cache miss, fetches live prices from Dodo Payments and falls back to `_product-fallback-prices.js` if Dodo is unreachable. Response carries an `X-Product-Catalog-Source` header so probes can tell cache hits from live fetches.
**Response** (tiers ordered `free`, `pro`, `pro_business`, `api_starter`, `api_business`, `enterprise`):
```json
{
"tiers": [
{
"name": "Free",
"localeKey": "free",
"description": "Get started with the essentials",
"features": ["Core dashboard panels", "..."],
"cta": "Get Started",
"href": "https://worldmonitor.app",
"highlighted": false,
"price": 0,
"period": "forever"
},
{
"name": "Pro",
"localeKey": "pro",
"description": "Full intelligence dashboard",
"features": ["..."],
"highlighted": true,
"monthlyPrice": 39.99,
"monthlyProductId": "pdt_0Nbtt71uObulf7fGXhQup",
"annualPrice": 359.99,
"annualProductId": "pdt_0NbttMIfjLWC10jHQWYgJ"
},
{
"name": "Pro Business",
"localeKey": "proBusiness",
"description": "The Pro dashboard, licensed for work",
"features": ["..."],
"highlightFeatures": ["Commercial license included"],
"highlighted": false,
"monthlyPrice": 49.99,
"monthlyProductId": "pdt_...",
"annualPrice": 449.99,
"annualProductId": "pdt_..."
},
{
"name": "API Starter",
"localeKey": "api",
"description": "Build internal tools on live intelligence data",
"features": ["..."],
"highlighted": false,
"monthlyPrice": 99.99,
"monthlyProductId": "pdt_0NbttVmG1SERrxhygbbUq",
"annualPrice": 899.99,
"annualProductId": "pdt_0Nbu2lawHYE3dv2THgSEV"
},
{
"name": "API Business",
"localeKey": "apiBusiness",
"description": "Launch your own product on WorldMonitor data",
"features": ["..."],
"highlighted": false,
"monthlyPrice": 299.99,
"monthlyProductId": "pdt_0Nbttg7NuOJrhbyBGCius"
},
{
"name": "Enterprise",
"localeKey": "enterprise",
"description": "Custom solutions for organizations",
"features": ["..."],
"cta": "Contact Sales",
"href": "mailto:enterprise@worldmonitor.app",
"highlighted": false,
"price": null
}
],
"fetchedAt": 1751799600000,
"cachedUntil": 1751803200000,
"priceSource": "dodo"
}
```
Notes:
- Price fields are flat on the tier. Paid tiers always expose `monthlyPrice` / `monthlyProductId`, and add `annualPrice` / `annualProductId` only when an annual variant exists — API Business is monthly-only, so those fields are absent on it. Free uses `price: 0, period: "forever"`; Enterprise uses `price: null`.
- `highlightFeatures` is an optional array of license/commercial-use callouts, rendered separately from `features` on the pricing page. Tiers without a callout omit the field.
- Prices are dollars (Dodo returns cents; the handler divides by 100). Currency is implicit USD for the published catalog.
### `DELETE /api/product-catalog`
Purges the cached catalog. Requires `Authorization: Bearer $RELAY_SHARED_SECRET`. Internal.
## Referrals
### `GET /api/referral/me`
Returns the caller's deterministic referral code (an 8-char HMAC of the Clerk userId, stable for the life of the account) and a pre-built share URL. Clerk bearer required. The handler also fires a best-effort `ctx.waitUntil` Convex binding so future `/pro?ref=<code>` signups can attribute — this never blocks the response.
```json
{
"code": "a1b2c3d4",
"shareUrl": "https://worldmonitor.app/pro?ref=a1b2c3d4"
}
```
Errors:
- `401 UNAUTHENTICATED` — missing or invalid Clerk JWT.
- `503 service_unavailable` — `BRIEF_URL_SIGNING_SECRET` not configured (the referral-code HMAC reuses that secret).
No `referrals` count or `rewardMonths` is returned today — Dodo's `affonso_referral` attribution doesn't yet flow into Convex, and exposing only the waitlist-side count would mislead.
`affonso_referral` is the vendor-contracted metadata key Dodo forwards to Affonso's referral-tracking webhook. The key name is load-bearing — renaming it (to `wm_referral`, `ref`, etc.) silently breaks Dodo→Affonso attribution. See `convex/payments/checkout.ts` and `convex/payments/subscriptionHelpers.ts` for the writer/reader call sites.
## Waitlist
### `POST /api/leads/v1/register-interest`
Captures an email into the Convex waitlist table. Turnstile-verified (desktop sources bypass), rate-limited per IP. Part of `LeadsService`; see [Platform endpoints](/api-platform) for the request shape.
|