Skip to content

Authentication

Every request carries one header:

curl https://api.trilocore.ai/api/v1/workspaces \
  -H "Authorization: Bearer $TRILOCORE_API_KEY"
import os
import httpx

client = httpx.Client(
    base_url="https://api.trilocore.ai",
    headers={"Authorization": f"Bearer {os.environ['TRILOCORE_API_KEY']}"},
)
r = client.get("/api/v1/workspaces")
r.raise_for_status()
const auth = { Authorization: `Bearer ${process.env.TRILOCORE_API_KEY}` };

const r = await fetch("https://api.trilocore.ai/api/v1/workspaces", {
  headers: auth,
});

There is one authentication scheme — Authorization: Bearer — and two kinds of token that fit it. Everything else about identity is derived by the front door, not sent by you.

The two credentials

API key Browser session
Looks like jns_… a signed session token
Sent as Authorization: Bearer jns_… Authorization: Bearer …, or the trilocore_sso cookie
Obtained from the API keys screen in the Trilocore app signing in to the Trilocore app
Intended for servers, CI, scripts, SDKs the product's own web UI
CSRF token needed never yes, when the cookie is used on a writing method

Both resolve to the same identity model, so an endpoint that works with a session works with a key. The reference states Authorization: Bearer (jns_ key or SSO session) on every authenticated endpoint for exactly this reason.

An API key is a bearer credential

Anyone holding a jns_… key can act as you, up to your plan's limits, until the key is revoked. Keep keys in environment variables or a secret manager, never in a repository, a frontend bundle, or a URL query string. Rotate on exposure — the value is unrecoverable after issue, so a lost key is replaced, not looked up.

Unauthenticated endpoints

Four endpoints answer without a credential, because they exist to let a third party verify work you have published:

# Fetch the public key set used to verify attestation signatures.
curl https://api.trilocore.ai/api/v1/public/attestations/jwks.json

The others are POST /api/v1/public/attestations/verify, GET /api/v1/public/reports/{report_id} and GET /api/v1/public/reports/link/{link_id}. They are rate limited per client IP rather than per key, and they cap request bodies more tightly than the authenticated surface — an oversized body is rejected with 413 payload_too_large.

The specification document at /api/v1/meta/openapi.json is also public.

Browser sessions and CSRF

Skip this section if you are using an API key; it does not apply to you.

Signing in to the Trilocore app sets three cookies:

Cookie Purpose
trilocore_sso the session token the API reads
__Host-trilocore_csrf the CSRF double-submit token
trilocore_refresh used only to renew a session; the API never reads it

When a request is authenticated by cookie and uses a writing method — POST, PUT, PATCH or DELETE — you must mirror the CSRF cookie's value into an x-csrf-token header:

// Browser: read the double-submit cookie and echo it on unsafe methods.
const csrf = document.cookie
  .split("; ")
  .find((c) => c.startsWith("__Host-trilocore_csrf="))
  ?.split("=")[1];

await fetch("https://api.trilocore.ai/api/v1/notes", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json", "x-csrf-token": csrf },
  body: JSON.stringify({ title: "Findings triage" }),
});

Omit it and the request is rejected with 403 before it reaches any service.

Branch on 403, not on one code string

The code differs by surface. On a proxied prefix the body carries "code": "csrf_required". On a gateway-native prefix — /api/v1/notes/*, as in the example above — the body carries "code": "forbidden" with "detail": "csrf_required". A client that keys its refresh-and-retry on code === "csrf_required" will therefore never fire on the notes surface. Match status === 403 and treat csrf_required appearing in either code or detail as the signal.

Two things to note. First, Authorization: Bearer is exempt — a bearer token is not sent automatically by the browser, so there is nothing for a cross-site request to forge. Second, the requirement is on the authentication method, not the endpoint: the same endpoint needs a CSRF token from a cookie-authenticated caller and does not need one from a key-authenticated caller.

Headers the front door controls

The front door does not pass your request through unchanged. It establishes who you are once, then forwards a deliberately small set of headers to the service that owns the resource:

content-type      accept
if-match          if-none-match
if-modified-since if-unmodified-since
idempotency-key

Everything else is dropped and, where the backend needs it, re-issued by the front door from the identity it just verified. That includes your Authorization header, your Cookie header, every x-trilocore-* header, and X-Forwarded-For.

Never set an x-trilocore-* header yourself

These headers carry verified identity — key id, user id, organisation id, persona, organisation role, teams, client IP — from the front door to the service behind it. A client-supplied value is stripped and replaced, so setting one cannot elevate your access. It also cannot achieve anything: if you are relying on one to make a call work, the call is wrong. The same applies to X-Forwarded-For; the client IP used for logging and per-IP limits is determined by the edge, not by you.

A small number of additional request headers are read by the front door itself to scope a call rather than forwarded verbatim. Where one applies, it is documented alongside the endpoints that use it.

Failure modes

Status code Cause
401 unauthorized missing, malformed, expired or revoked credential
403 csrf_required cookie-authenticated writing method with no x-csrf-token
403 forbidden authenticated, but not permitted to perform this operation
403 persona_forbidden the credential's persona may not use this endpoint
404 not_found the resource does not exist or is not yours

A 401 does not mean the endpoint exists

Authentication is checked before the endpoint is looked up. An unauthenticated request to a path that does not exist answers 401, not 404, so that the API never confirms which routes are real to an anonymous caller. Only an authenticated caller sees 404 for a path that is genuinely absent — so when you are probing an unfamiliar path, authenticate first or you will misread the result.

See Errors for the full catalogue and the response shape.