Skip to main content
Browser requests are checked against your organization’s origin allow-list before they reach an endpoint. Requests from your own backend are not - CORS is a browser mechanism, so server-side calls are unaffected by everything on this page. You need this if your page calls the API directly. Most commonly that means:
  • Exchanging a tap for a session token with POST /v1/taps (a verify page with no backend of its own).
  • Introspecting or revoking a session token with GET/DELETE /v1/session-tokens/current.
  • Reading a unit, or polling claim or transfer status, with a session token.
The other half of browser setup is the publishable key that identifies your organization.

Allow-list your origins

Manage the list with your API key. PUT replaces it wholesale - send the complete list, not a delta.
Send an empty array to remove every origin. GET /v1/settings/cors-origins returns the current list.
Changes take effect within about a minute. The allow-list is cached for 60 seconds, so a newly added origin may be rejected briefly after you save it.

Entry formats

Up to 50 entries, each at most 255 characters, with one wildcard per entry. An origin is scheme://host[:port] - no path, query, or fragment. Entries are stored in canonical form and returned as saved, so https://Brand.Example/ comes back as https://brand.example. A subdomain wildcard needs at least two labels after the *: https://*.brand.example is accepted, https://*.com is not. The wildcard does not match the bare domain - allow-list https://brand.example separately if you serve from the apex.
Every listed origin can call the API from a browser on your organization’s behalf. Port wildcards are meant for local development - remove http://localhost:* and any other development origin before you go live.

What the API expects

Send the Authorization header, with the default credentials mode. Do not set credentials: "include" - the API never returns Access-Control-Allow-Credentials, so the browser will block the response.
Preflight OPTIONS requests are handled for you; no configuration is needed.
@endstate-sdk/core sends requests this way already
  • it never sets credentials, so a browser client works as long as the origin is allow-listed. The raw fetch above is what the SDK does for you.

Reading response headers

Browser JS can read these cross-origin: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, Idempotent-Replayed. Include X-Request-Id in any support request - it identifies the exact call in our logs.

When an origin is rejected

A request from an origin that is not on your list is rejected with 403 and auth.forbidden before it reaches the endpoint. The rejection is still CORS-decorated, which means your JavaScript can read the error envelope and show something useful instead of an opaque network failure. Echoing the origin on a rejection grants nothing - the request was already refused. Authentication failures are decorated too, so a 401 from a missing or invalid credential is readable in the browser rather than opaque. One case is not decorated, because there is no valid origin to echo: a malformed Origin, or one that resolves to null (file://, a sandboxed iframe, a data: URL). That surfaces as a generic network error. If a call fails and you see nothing in the response, check the browser console for the CORS message, then confirm the exact origin - scheme, host, and port - is on the list.

Next steps

Publishable keys

The client-safe credential that identifies your organization.

Host your own verify page

Build the post-tap experience on your domain, with or without a backend.