Authentication

Project scope has two tiers of credentials and a third "mode" that's just the second tier with a header.

The three modes

Mode Authorization X-USER-ID What it represents
Owner JWT Bearer <jwt> (ignored) The human who owns the project — full control plane
Project key (no user) Bearer jg_p_... absent Backend automation: the project itself, no per-user partition
Project key (passthrough user) Bearer jg_p_... <your-internal-id> The company calling on behalf of one of its end-users

Many endpoints accept either Mode 1 or Mode 2/3 via the ProjectAccess extractor — those are flagged per-endpoint as Either (ProjectAccess). Settings-mutation endpoints are JWT-only on purpose: a leaked project key must not be able to redirect webhooks or disable rate limits.

Mode 1: Owner JWT

Obtained from POST /api/auth/login/google (see Account & Platform → Authentication). Use for everything in the dashboard / admin surface: creating projects, minting keys, inviting humans, editing settings.

curl https://api.juglans.ai/api/projects \
  -H "Authorization: Bearer eyJhbGc...your.jwt.here"

Tokens are short-lived; refresh via POST /api/auth/refresh.

Mode 2: Project key (no user)

Mint a project key in API → API keys in the dashboard (the API tab's sub-tab), or via POST /api/projects/{id}/api-keys (see Webhooks & Rate Limits). The plaintext is shown once at creation; the server stores only a SHA-256 hash. Lose it, re-issue.

curl https://api.juglans.ai/api/projects/3a7b5c1d-.../conversations \
  -H "Authorization: Bearer jg_p_a1b2c3d4..."

Use for backend automation that doesn't represent any specific end-user (cron jobs, internal tools). Conversations created in this mode have external_user_id = NULL and are only visible to other "no user" callers.

Mode 3: Project key + X-USER-ID

Same key as Mode 2, plus the X-USER-ID header. The header value is opaque to Juglans — whatever stable identifier you use internally (database UUID, email, anything ≤256 chars).

curl -X POST https://api.juglans.ai/api/projects/3a7b5c1d-.../chat \
  -H "Authorization: Bearer jg_p_a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -H "X-USER-ID: customer_47291" \
  -d '{"agent_id":"...","message":"hi"}'

On first use of a given X-USER-ID for a project, the server auto-upserts a row in project_external_users keyed by (project_id, external_id). Subsequent calls reuse it and bump last_seen_at. You don't need to pre-provision your users.

The external user partitions:

  • Conversations — a Mode-3 caller cannot see Mode-2 threads, and vice versa.
  • The agent runtime's per-thread state, via a chat_id derived from the principal (Mode 2 → project:{pid}:key:{api_key_id}:conv:{cid}, Mode 3 → project:{pid}:user:{shadow_user_id}:conv:{cid}). Anything the agent runtime persists keyed on chat_id (history, in-memory state) is therefore implicitly partitioned per end-user.

Audit-log partitioning by external user is planned but not yet implemented for project-scope calls — the audit_log table only has rows for jg_a_* agent-scope requests today.

Authorization header reference

Authorization: Bearer <token>

The extractor inspects the token shape: jg_p_* routes through the project-key validator, anything else through the JWT validator. There's no separate X-API-Key header.

Revocation

Action Effect
Delete a project API key (DELETE /api/projects/{id}/api-keys/{key_id}) All future calls bearing that key — Mode 2 and Mode 3 — are rejected. Stored conversations remain.
Delete an external user (DELETE /api/projects/{id}/external-users/{user_id}) The shadow row is gone. Any subsequent call carrying that X-USER-ID will recreate a fresh external user with a different UUID; conversations from before the delete are orphaned (no longer reachable through the API). Use this for GDPR-style "forget this user".
Delete the project Cascades: all keys, members, invites, and external-user rows go.
Owner JWT compromised Use the platform-level session revocation (POST /api/auth/logout + the sessions endpoints in account-management) — the JWT itself is not stored, but the parent session is.

Common errors

All errors are {"error": "<message>"} — no code field. Branch on the status.

Status When Example message
401 Missing / unparseable Authorization, or jg_p_* key not found / revoked. Reasons are deliberately conflated. "Invalid API key"
403 Key is for a different project than the URL path. "project API key not valid for this project"
(none) X-USER-ID header sent as blank/whitespace. Silently treated as absent — you fall through to Mode 2 without an error. If you intended Mode 3, this is a bug on your side; check that you're not sending an empty header by mistake.