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:
- Browser sends
OPTIONSrequest withOriginandAccess-Control-Request-Methodheaders - The origin is checked against the endpoint's
allow_originslist - If the origin is allowed, appropriate CORS headers are returned
- 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_originscontains*, all origins are allowed (only valid whenallow_credentialsisfalse) - 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:
{
"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:
{
"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:
{
"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.
{
"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 -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
}'
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 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:
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
*forallow_originsmeans 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_credentialsdefaults totrue, which causes the server to emitAccess-Control-Allow-Credentials: true. The router rejects the combination ofallow_origins: ["*"]andallow_credentials: truewith HTTP 400 (the CORS spec forbids it). To use a wildcard origin you must explicitly setallow_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-TypeandAuthorization) 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
- Open browser DevTools and check the Network tab for the failed request
- Look for the OPTIONS preflight request, check its response headers
- Verify the
Originheader in your request matches an entry inallow_originsexactly (protocol, domain, and port must match) - Test with curl to confirm the endpoint works without CORS:
curl -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-X OPTIONS \
"https://api.xerotier.ai/v1/chat/completions" -v
- Check the response for
Access-Control-Allow-OriginandAccess-Control-Allow-Methodsheaders
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:
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
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 (likeapplication/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.