// Platform

Endpoint CORS

Per-endpoint CORS lets browser apps call inference directly without a proxy. Each endpoint carries its own origin allowlist, method set, header set, credentials posture, and preflight cache; defaults are restrictive on purpose.

Overview

Each Xerotier inference endpoint carries its own CORS policy. This is useful when:

  • Your web application calls inference endpoints directly from the browser
  • You have multiple frontend applications on different domains that need endpoint access
  • You want to restrict which origins can access specific endpoints

CORS configuration is applied on a per-endpoint basis and enforced on every request to the endpoint.

Minimum required body. Only allow_origins and allow_methods are required on PUT. Every other field has a server default. Send a wildcard origin only with allow_credentials: false; any other combination of "*" with credentials is rejected with HTTP 400.

Configuration

CORS settings are configured when creating or updating an endpoint. The following fields control CORS behavior. Required fields are marked.

Field Type Default Description
allow_originsrequired string[] ["*"] List of origins permitted to make requests. Use * for any origin (only valid when allow_credentials is false). The alias allowed_origins is also accepted; if both are present, allow_origins wins.
allow_methodsrequired string[] ["GET", "POST", "OPTIONS"] HTTP methods the client is allowed to use. Valid values: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD. Alias: allowed_methods.
allow_headersoptional string[] ["Content-Type", "Authorization"] Request headers the client is allowed to send. Alias: allowed_headers.
max_ageoptional integer 86400 How long (in seconds) browsers should cache the preflight response. Must be non-negative. Alias: max_age_seconds.
allow_credentialsoptional boolean true Whether Access-Control-Allow-Credentials: true is emitted. Setting it to true together with allow_origins: ["*"] is rejected with HTTP 400 (the CORS spec forbids that combination).
expose_headersoptional string[] [] Response headers the browser is permitted to expose to client JavaScript.

How It Works

Preflight Requests

When a browser makes a cross-origin request that is not "simple" (e.g., uses custom headers or non-standard methods), it first sends an OPTIONS preflight request. Xerotier handles this automatically:

  1. Browser sends OPTIONS request with Origin and Access-Control-Request-Method headers
  2. The origin is checked against the endpoint's allow_origins list
  3. If the origin is allowed, appropriate CORS headers are returned
  4. Browser proceeds with the actual request

Simple Requests

For simple requests (GET/POST with standard headers), the browser sends the request directly. The Access-Control-Allow-Origin header is added to the response if the origin is allowed.

Origin Validation

The Origin header is validated against the configured allow_origins list:

  • If allow_origins contains *, all origins are allowed (only valid when allow_credentials is false)
  • Otherwise, the origin must exactly match one of the listed origins
  • If the origin is not allowed, no CORS headers are added and the browser blocks the response

Examples

Minimum Body

The smallest accepted PUT body. Carries only the two required fields; the server fills in defaults for the rest:

JSON
{ "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST", "OPTIONS"] }

Single Origin

Allow requests only from your production web application. Send this as the request body to PUT /{project_id}/v1/management/endpoints/:id/cors:

JSON
{ "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "max_age": 86400 }

Multiple Origins

Allow requests from both production and staging environments:

JSON
{ "allow_origins": [ "https://app.example.com", "https://staging.example.com", "http://localhost:3000" ], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "max_age": 3600 }

Wildcard Origin

Allow requests from any origin. The server rejects allow_origins: ["*"] together with allow_credentials: true (HTTP 400, "allow_origins cannot be '*' when allow_credentials is true (CORS spec)."), so the wildcard form must explicitly disable credentials:

Pitfall. allow_credentials defaults to true. Posting {"allow_origins":["*"]} without overriding it is rejected with HTTP 400. Always pair the wildcard with "allow_credentials": false as shown below.

JSON
{ "allow_origins": ["*"], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "allow_credentials": false, "max_age": 86400 }

CORS Management Routes

CORS is managed via two per-endpoint routes on the router public API. There is no separate DELETE for CORS configuration, deleting the parent endpoint (DELETE /{project_id}/v1/management/endpoints/:id) removes its CORS row as part of the soft-delete.

Method Path Description
GET /{project_id}/v1/management/endpoints/:id/cors Retrieve the CORS configuration for an endpoint. Returns the platform defaults when no row exists.
PUT /{project_id}/v1/management/endpoints/:id/cors Create or replace the CORS configuration for an endpoint. allow_origins and allow_methods are required.

Response aliases. The GET response carries every field under both canonical (allow_*, max_age) and alias (allowed_*, max_age_seconds) keys for dashboard compatibility. Write either form on PUT; when both are present, the canonical form wins.

Setting CORS via API

Configure CORS on an endpoint using the dedicated CORS route. Substitute your project id (e.g. proj_abc123) for {project_id} and the endpoint UUID for :id:

curl
curl -X PUT https://api.xerotier.ai/{project_id}/v1/management/endpoints/ENDPOINT-UUID/cors \ -H "Authorization: Bearer xero_my-project_abc123" \ -H "Content-Type: application/json" \ -d '{ "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "max_age": 86400 }'
Python
import requests headers = { "Authorization": "Bearer xero_my-project_abc123", "Content-Type": "application/json" } response = requests.put( "https://api.xerotier.ai/{project_id}/v1/management/endpoints/ENDPOINT-UUID/cors", headers=headers, json={ "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "max_age": 86400 } ) print(response.json())

Retrieving CORS Configuration

curl
curl https://api.xerotier.ai/{project_id}/v1/management/endpoints/ENDPOINT-UUID/cors \ -H "Authorization: Bearer xero_my-project_abc123"

Example Response

The response carries the canonical allow_* keys and also emits allowed_* and max_age_seconds aliases for dashboard compatibility:

{ "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "allow_credentials": true, "expose_headers": [], "max_age": 86400, "allowed_origins": ["https://app.example.com"], "allowed_methods": ["GET", "POST", "OPTIONS"], "allowed_headers": ["Content-Type", "Authorization"], "max_age_seconds": 86400 }

JavaScript Fetch Example

Once CORS is configured on your endpoint, you can make direct browser requests:

JavaScript
const response = await fetch( "https://api.xerotier.ai/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer xero_my-project_abc123" }, body: JSON.stringify({ model: "my-model", messages: [ { role: "user", content: "Hello!" } ] }) } ); const data = await response.json(); console.log(data.choices[0].message.content);

Security Considerations

  • Avoid wildcard origins in production: Using * for allow_origins means any website can make requests to your endpoint. This is acceptable for public APIs but not recommended for endpoints with sensitive data.
  • Credentials default to enabled: allow_credentials defaults to true, which causes the server to emit Access-Control-Allow-Credentials: true. The router rejects the combination of allow_origins: ["*"] and allow_credentials: true with HTTP 400 (the CORS spec forbids it). To use a wildcard origin you must explicitly set allow_credentials: false; to use credentials you must enumerate specific origins. Server-to-server requests are unaffected because CORS is a browser-enforced mechanism.
  • API keys in browser code: When calling endpoints from browser JavaScript, your API key is visible to anyone who inspects the page source. Consider using scoped API keys with minimal permissions and endpoint restrictions.
  • Restrict allowed headers: Only allow the headers your application actually needs. The defaults (Content-Type and Authorization) are sufficient for most inference use cases.
  • Use short max_age during development: A shorter max_age (e.g., 300 seconds) during development means CORS policy changes take effect faster in browsers.

Important: CORS is a browser-enforced security mechanism. Server-to-server requests (curl, backend services, SDKs) are not affected by CORS settings and will always work regardless of configuration.

Troubleshooting

Common CORS Errors

Error Cause Fix
"No 'Access-Control-Allow-Origin' header" Origin not in allow_origins list Add your origin to the endpoint's allow_origins
"Method not allowed by CORS" HTTP method not in allow_methods Add the method (e.g., POST) to allow_methods
"Request header not allowed" Custom header not in allow_headers Add the header name to allow_headers
HTTP 400 "allow_origins cannot be '*' when allow_credentials is true (CORS spec)." The request body combined allow_origins: ["*"] with allow_credentials: true (the default), which the router rejects per the CORS spec Either enumerate specific origins (e.g., ["https://app.example.com"]) or set allow_credentials: false alongside the wildcard
HTTP 400 "allow_origins (or allowed_origins) is required." PUT body omitted the required allow_origins field (or the equivalent allow_methods error if methods are missing) Send a full body containing at least allow_origins and allow_methods

Diagnostic Steps

  1. Open browser DevTools and check the Network tab for the failed request
  2. Look for the OPTIONS preflight request, check its response headers
  3. Verify the Origin header in your request matches an entry in allow_origins exactly (protocol, domain, and port must match)
  4. Test with curl to confirm the endpoint works without CORS:
curl
curl -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -X OPTIONS \ "https://api.xerotier.ai/v1/chat/completions" -v
  1. Check the response for Access-Control-Allow-Origin and Access-Control-Allow-Methods headers

A correctly configured endpoint returns 204 No Content with these headers; a misconfigured one returns 200 OK with the same body but no Access-Control-* headers:

Allowed
HTTP/2 204 access-control-allow-origin: https://app.example.com access-control-allow-methods: GET, POST, OPTIONS access-control-allow-headers: Content-Type, Authorization access-control-max-age: 86400
Blocked
HTTP/2 200 content-type: text/plain # Note the absence of any access-control-* response header. # The browser will block the response from script.

Simple vs Preflighted Requests

Browsers classify cross-origin requests into two categories:

  • Simple requests: GET or POST with standard Content-Types (text/plain, multipart/form-data, application/x-www-form-urlencoded) and no custom headers. These skip the preflight step.
  • Preflighted requests: Requests with custom headers (like Authorization), non-standard Content-Types (like application/json), or non-simple methods. These trigger an OPTIONS preflight first.

Most inference API calls use application/json and Authorization headers, so they are preflighted. Make sure your CORS configuration includes OPTIONS in allow_methods.