Security

Origin binding

Which origins may create challenges for a site key, how origins are normalised, and how the origin is carried into proofs and tokens.

Rules

POST /api/v1/challenges requires an Origin header (browsers send it automatically for cross-origin fetches). The origin is accepted when any of these hold for the site whose key was used:

  1. It is exactly https://<domain>.
  2. It is https://<anything>.<domain> (any depth of subdomain).
  3. It equals one of the site's explicitly allowed origins after normalisation.

Rules 1 and 2 require https and no explicit port. Development origins such as http://localhost:5173 must be listed under allowed origins (up to 20 per site).

Site domainOriginAllowed
example.comhttps://example.comYes
example.comhttps://shop.example.comYes
example.comhttps://a.b.example.comYes
example.comhttp://example.comOnly if listed explicitly
example.comhttps://example.com:8443Only if listed explicitly
example.comhttps://example.com.evil.netNo
example.comhttps://notexample.comNo
example.comhttp://localhost:5173Only if listed explicitly

Normalisation

An origin is parsed as a URL and reduced to scheme://host[:port]. Values with a path, query, fragment or credentials, or a scheme other than http/https, are invalid and rejected with ORIGIN_REQUIRED (missing/invalid header) or ORIGIN_UNAUTHORIZED (valid but not allowed). Site domains are lowercase hostnames without scheme, path or port; a bare label without a dot is refused unless it is localhost.

Where the origin goes

  • Stored on the challenge as origin and covered by the signature base (@origin).
  • Copied into the token as inh.origin and returned by tokens/verify as verification.origin.
  • Recorded on every verification event.

Creating challenges server-to-server

If your backend creates challenges (for a non-browser flow), send an Origin header yourself with an allowed value. The origin is then what agents and tokens will see.

curl
curl -X POST https://imnotahuman.ai/api/v1/challenges \
  -H 'origin: https://example.com' \
  -H 'content-type: application/json' \
  -d '{"site_key":"site_live_exampleweb2ab3cd4ef5gh6j","action":"api_access","ttl":300}'

CORS

Browser calls send an Origin. Public routes reflect that origin in Access-Control-Allow-Origin when it is https on a normal site, or http on localhost. Operator routes (/me, /sites, /agents, /dashboard, /events) reflect only https://imnotahuman.ai and local development. Infrastructure hosts are never reflected, and credentialed CORS is not enabled. Requests without an Origin — server SDKs and curl — are unchanged, because CORS does not apply to them. Allowed request headers are authorization, content-type, x-request-id, signature, signature-input, signature-agent and content-digest. A reflected CORS origin does not authorize a site; the challenge endpoint still checks the site's allowed origins.