# The v1 API is now CORS-friendly

Canonical HTML: https://fish.dog/product-releases/the-v1-api-is-now-cors-friendly
JSON Feed: https://fish.dog/product-releases/feed.json
RSS Feed: https://fish.dog/product-releases/rss.xml
Published: 21 March 2026
Updated: 4 May 2026
Release Type: Improvement
Breaking Change: No
Author: Phillip Gales

## Primary Claim

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.

## Summary

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.

## LLM Summary

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.

## Full Release

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.

---

## Quotable Insights

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

## FAQ

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