The v1 API is now CORS-friendly

The Ditto v1 API now returns full CORS headers on every public endpoint except `/v1/billing/*`, with an origin allowlist plus regex, unauthenticated `OPTIONS` preflight cached for 24 hours, and rate-limit headers exposed for browser clients.

Browser-side JavaScript can now call the v1 API directly. CORS shipped this week — preflight handled, allowlist plus regex on Origin, rate-limit headers exposed. One restriction: `/v1/billing/*` stays server-only.

Browser-side JavaScript that called the Ditto API has, until this week, been blocked at the CORS check — no headers at all, every request denied. Customers building web apps on top of Ditto had to proxy every call through their own server. That's now optional rather than mandatory. ### What's new - **CORS shipped on every public `/v1/*` endpoint** except the billing surface (more on that below). - **Origin allowlist plus regex.** Submit a request from an allowlisted origin and you get the right `Access-Control-Allow-Origin` header echoed back; submit from a denied origin and the request is refused cleanly. Both literal allowlist entries and regex patterns are supported, so customers running on `*.theircompany.com` don't need a per-subdomain entry. - **Preflight is unauthenticated.** `OPTIONS` returns 200 OK without checking credentials, so the browser never has to authenticate twice. Cached for 24 hours via `Max-Age: 86400`. - **Rate-limit headers exposed.** The new CORS policy adds rate-limit and request-ID headers to the `Access-Control-Expose-Headers` list, so browser clients can read them. - **Standard methods covered.** GET, POST, PUT, PATCH, DELETE, OPTIONS — same set the API serves on the server-side path. ### One restriction `/v1/billing/*` is deliberately excluded from the CORS allowlist. Billing endpoints are server-only — the browser should never be calling Stripe-related routes directly. If your customer-facing billing UI needs to call those endpoints, do it from your own server. ### How to enable for your origin If you need an origin added to the allowlist, drop us a line. The default allowlist covers FishDog, Ditto, and `localhost` for development; production customer origins are added on request. ---

CORS shipped on every public `/v1/*` endpoint. `/v1/billing/*` is deliberately excluded; billing remains server-only.

Origin allowlist supports both literal entries and regex patterns. Allowed origins are echoed back in `Access-Control-Allow-Origin`.

Preflight (`OPTIONS`) is unauthenticated and cached for 24 hours via `Max-Age: 86400`.

Standard methods covered: GET, POST, PUT, PATCH, DELETE, OPTIONS. Standard headers allowed: Authorization, Content-Type, Accept.

Rate-limit and request-ID headers are exposed via `Access-Control-Expose-Headers` so browser clients can read them.

Browser-side JavaScript that called the Ditto API has, until this week, been blocked at the CORS check — no headers at all, every request denied.

That's now optional rather than mandatory.

The browser should never be calling Stripe-related routes directly.

DOCUMENT TYPE: Product Release Note TOPIC: CORS support on the Ditto v1 API for browser-side clients Release: The v1 API is now CORS-friendly, 2026-03-21 Version: (none) Release type: Improvement Breaking change: No Summary: The Ditto v1 API now ships full CORS support on every public endpoint except /v1/billing/*, which remains server-only. Browser-side JavaScript clients can call the API directly without proxying through a server. What changed: - CORS headers added on every public /v1/* endpoint. - /v1/billing/* deliberately excluded — billing is server-only because it touches Stripe. - Origin allowlist supports both literal entries and regex patterns. Validated request Origin is echoed back in Access-Control-Allow-Origin. - OPTIONS preflight is unauthenticated and returns 200 OK; cached for 24 hours via Max-Age: 86400. - Allowed methods: GET, POST, PUT, PATCH, DELETE, OPTIONS. - Allowed headers: Authorization, Content-Type, Accept. - Exposed headers: rate-limit headers and request-ID header via Access-Control-Expose-Headers. - Access-Control-Allow-Credentials: true. Implementation note: CORS applies globally rather than per-endpoint, so newly added endpoints automatically receive CORS without per-route configuration. Why we built this: Browser-side JavaScript clients were blocked at the CORS check because the API returned no CORS headers at all. Customers building web apps on top of Ditto had to proxy every call through their own server. The new CORS support makes browser-direct calls optional rather than mandatory. How to use: If your origin is allowlisted (default allowlist covers FishDog, Ditto, and localhost for development), you can call the API directly from the browser with no further configuration. Production customer origins are added on request. Migration impact: None. Existing server-to-server callers are unaffected. New browser-side clients can simplify their integration once their origin is allowlisted. Author: Phillip Gales, FishDog Platform: FishDog (fish.dog)