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_idderived 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 onchat_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_logtable only has rows forjg_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. |
— |