Skip to content

API reference — identity service

Base URL: http[s]://<host>:8081

All request bodies are application/json. All error responses carry {"code": "<error_code>"}. Success responses carry {"field": value, ...}.

Most endpoints require a bearer token in the Authorization header:

Authorization: Bearer <access_token>

Endpoints that accept API keys instead of (or in addition to) JWTs are noted per-endpoint. The middleware resolves the credential type automatically — callers do not need to specify which type they are using.

Mutating endpoints (POST/PATCH, excluding auth flows) require an Idempotency-Key header. The same request replayed within the deduplication window returns the original response without re-executing the handler.

Authenticates with email and password. Returns an RFC 6749 §5.1 token pair.

Auth required: no

Request body:

FieldTypeRequiredNotes
emailstringyescase-insensitive (citext)
passwordstringyes
x_device_labelstringnolabel stored on the refresh token row

Response 200:

{
"access_token": "<RS256 JWT>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "pb_live_rt_..."
}

Error codes:

HTTPCodeMeaning
400invalid_requestBody missing or malformed
401unauthenticatedWrong email, password, or membership
500internalServer-side failure

Rotates a refresh token and returns a fresh token pair. Reuse-detection revokes the family.

Auth required: no

Request body:

FieldTypeRequired
refresh_tokenstringyes

Response 200: same shape as /v1/auth/login.

Error codes:

HTTPCodeMeaning
400refresh_token_requiredBody missing refresh_token
401refresh_token_reusedToken already consumed; family revoked
401refresh_token_invalidExpired, revoked, or not found
500internal

On reuse detection the body includes "action": "re_authenticate".


Revokes the refresh token family associated with the given token. Always returns 204; unknown or empty tokens are no-ops (no existence leak).

Auth required: no

Request body:

FieldTypeRequired
refresh_tokenstringno

Response 204: no body.


Revokes every non-revoked refresh token for a target user. Owner role required.

Auth required: JWT, role owner

Request body:

FieldTypeRequired
user_idstringyes

Response 204: no body.

Error codes:

HTTPCodeMeaning
400user_id_requireduser_id absent
400user_id_invaliduser_id not a valid UUID
403insufficient_roleCaller is not an owner

Returns the authenticated user’s profile.

Auth required: JWT or API key

Response 200:

{
"id": "<uuid>",
"email": "user@example.com",
"name": "Alice",
"created_at": "2026-05-24T00:00:00Z"
}

Lists all non-deleted API keys for the authenticated user.

Auth required: JWT or API key

Response 200: array of:

[
{
"id": "<uuid>",
"name": "ci-key",
"key_prefix": "pb_live_",
"env": "live",
"expires_at": null,
"last_used_at": "2026-05-24T00:00:00Z"
}
]

Mints a new API key. The raw key is returned once and never stored — treat it as a secret.

Auth required: JWT only (API keys cannot mint API keys)

Request body:

FieldTypeRequiredNotes
namestringyeshuman label
envstringnolive (default) or test
expires_atstringnoRFC 3339 expiry timestamp

Response 201:

{
"id": "<uuid>",
"key_prefix": "pb_live_",
"raw_key": "pb_live_<random>",
"env": "live",
"expires_at": null
}

Soft-deletes an API key. The key stops authenticating immediately.

Auth required: JWT only

Response 204: no body.


Returns the organization. Cross-tenant: returns 404 if id is not the caller’s org.

Auth required: JWT or API key

Response 200:

{
"id": "<uuid>",
"name": "Acme",
"slug": "acme",
"created_at": "2026-05-24T00:00:00Z",
"updated_at": "2026-05-24T00:00:00Z"
}

Updates org name and/or slug. Owner role required.

Auth required: JWT, role owner

Request body: all fields optional; omitted fields are unchanged.

FieldType
namestring
slugstring

Response 200: same shape as GET /v1/orgs/{id}.


CodeHTTPMeaning
invalid_request400Malformed body or missing required field
unauthenticated401Missing or invalid credential
insufficient_role403Valid credential but wrong role
resource_not_found404Entity missing or cross-tenant isolation
internal500Unexpected server error

Proto source: paper-board/proto/identity/v1/auth.proto

Internal consumers call identity.paper-board.svc.cluster.local:50051 (Phase 1–5 plain gRPC; Phase 5+ mTLS via cert-manager).

// from paper-board/proto/identity/v1/auth.proto
rpc VerifyAPIKey(VerifyAPIKeyRequest) returns (VerifyAPIKeyResponse);

Authenticates a raw API key. Returns an AuthContext on success.

Errors: UNAUTHENTICATED (invalid_credentials, wrong_environment, no_membership), INTERNAL.

// from paper-board/proto/identity/v1/auth.proto
rpc VerifyJWT(VerifyJWTRequest) returns (VerifyJWTResponse);

Validates an RS256 JWT and returns an AuthContext enriched with the current membership role.

Errors: UNAUTHENTICATED (expired, key_retired, invalid_credentials, no_membership), INTERNAL.

// from paper-board/proto/identity/v1/auth.proto
rpc IssueJWT(IssueJWTRequest) returns (IssueJWTResponse);

Issues a signed JWT for a known (user_id, org_id) pair. TTL is clamped to 24 h; zero uses the service default (3600 s). Callers must verify membership exists before calling — the RPC asserts it and returns PERMISSION_DENIED (not_a_member) if not.

// from paper-board/proto/identity/v1/auth.proto
rpc GetPublicKey(GetPublicKeyRequest) returns (GetPublicKeyResponse);

Returns the DER-encoded RSA public key for a given kid. Used by consumers that verify JWTs locally instead of delegating to VerifyJWT. Returns NOT_FOUND (kid_unknown) for retired or unknown key IDs.

All Verify* RPCs return an AuthContext:

FieldTypeNotes
user_idstringUUID
org_idstringUUID
modeAuthModeAUTH_MODE_JWT or AUTH_MODE_API_KEY
roleRoleROLE_OWNER or ROLE_MEMBER
envEnvENV_LIVE or ENV_TEST
auth_key_idstringkid (JWT mode only)
api_key_idstringUUID (API key mode only)
methodstring"jwt_session" or "api_key:<prefix>"

Tokens are RS256-signed with a key from identity.auth_keys (state active). The kid header field identifies the signing key.

ClaimValue
issidentity.paper-board
subuser UUID
aud["agents", "identity"]
iatUnix timestamp — issued at
nbfUnix timestamp — not before (= iat)
expUnix timestamp — expiry
jtiUUID — unique token ID
org_idorg UUID
env"live" or "test"
scope"default"

Source:

// from paper-board/identity/internal/core/jwt/issue.go:35-46
claims := jwt.MapClaims{
"iss": "identity.paper-board",
"sub": userID.String(),
"aud": []string{"agents", "identity"},
"iat": now.Unix(),
"nbf": now.Unix(),
"exp": exp.Unix(),
"jti": uuid.NewString(),
"org_id": orgID.String(),
"env": s.cfg.Env,
"scope": "default",
}