Product Release

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.

21 March 2026

Improvement
The FishDog organisation API Keys management page, showing per-key scopes and the MCP server access section.
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)

Key Takeaways

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

---

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.

Frequently Asked Questions

Can my web app now call the Ditto API directly?

Yes, provided your origin is on the allowlist. The new CORS policy supports both literal allowlist entries and regex patterns, so a single regex covers a whole subdomain space. Drop us a line if you need an origin added.

Why is /v1/billing/* excluded?

Billing endpoints touch Stripe and should never be called directly from the browser. Customer-facing billing UIs that need this data should call /v1/billing/* from their own server, not from the browser.

Is preflight cached?

Yes — preflight responses include Max-Age: 86400, so browsers cache the preflight for 24 hours. After the first preflight in a session, subsequent calls to the same endpoint don't pay the round-trip cost.

Which headers can my browser client read?

Standard response headers plus rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and the request-ID header. These are exposed via Access-Control-Expose-Headers so JavaScript can inspect them — useful for displaying remaining-quota state in a UI.

What if my request is from a denied origin?

The request is refused cleanly with the appropriate CORS error. The API does not silently process requests from non-allowlisted origins. If you have a legitimate origin that needs to be added, contact us.

More Releases