Errors.
Every failing request returns the same JSON envelope with a stable, machine-readable code. The SDK turns each one into a typed exception you can branch on — see the mapping below.
Envelope
{"error": {"code": "scope_denied","message": "Scope not granted: memory:write:fitness","details": null}}
code is the contract — match on it, not on message (which is human-facing and may change). details is optional and code-specific; for validation_failed it carries the Zod issues array.
Status → code → recovery
| HTTP | code | Cause | Recovery | SDK class |
|---|---|---|---|---|
| 400 | bad_request | Malformed request, invalid scope in manifest, or code mismatch | Fix the request / manifest | ValidationError |
| 400 | validation_failed | Body failed Zod validation | Read details (issues array) and fix the fields | ValidationError |
| 401 | unauthorized | Missing, invalid, expired, or revoked token | Re-run the OAuth connect flow | UnauthorizedError |
| 402 | limit_exceeded | Plan cap reached | Upgrade — see Limits | PAMbaseError |
| 403 | forbidden | App suspended or client mismatch | Contact support / check credentials | PAMbaseError |
| 403 | scope_denied | Token lacks the required scope | Request it in your manifest / consent | ScopeDeniedError |
| 404 | not_found | Resource does not exist or is out of scope | Verify the id | NotFoundError |
| 409 | conflict | Email or slug already taken | Choose a different value | PAMbaseError |
| 429 | rate_limited | Too many requests | Back off — SDK exposes retryAfterMs | RateLimitError |
| 500 | internal | Server error | Retry with backoff; report if persistent | ServerError |
A plan cap (Limits) only errors when you cross it — e.g. the memory write that exceeds the store cap, or the connect that exceeds the connected-apps cap. Reads keep working.
401 unauthorized means the token itself is bad — send the user back through connect. 403 scope_denied means the token is valid but lacks a specific scope — add that scope to your manifest and have the user re-consent.
Example error responses
{"error": {"code": "scope_denied","message": "Scope not granted: memory:write:fitness"}}
{"error": {"code": "validation_failed","message": "Invalid request body","details": [{ "path": ["fireAt"], "message": "Invalid datetime", "code": "invalid_string" }]}}
{"error": {"code": "rate_limited","message": "Too many requests","details": { "retryAfterMs": 1200 }}}
Handling errors in the SDK
Each code maps to a PAMbaseError subclass. Catch the specific class and read its properties:
import {PAMbaseApp,UnauthorizedError,ScopeDeniedError,RateLimitError,ValidationError,classifyError,} from "@pambase/sdk";const pam = new PAMbaseApp({ baseUrl, connectionToken });try {await pam.remember({ content: "Ran 5k this morning.", kind: "event", scope: "fitness" });} catch (err) {if (err instanceof UnauthorizedError) {// 401 — token missing / expired / revokedreturn redirectToConnect();}if (err instanceof ScopeDeniedError) {// 403 — token lacks a scope; .scope tells you whichconsole.warn("Add this scope to your manifest:", err.scope);return;}if (err instanceof RateLimitError) {// 429 — back off the suggested amount, then retryawait new Promise((r) => setTimeout(r, err.retryAfterMs));return;}if (err instanceof ValidationError) {console.error("Fix these fields:", err.details);return;}// Anything else: normalize and rethrowthrow classifyError(err);}
The SDK already retries NetworkError, RateLimitError, and 5xx with backoff. You usually only need to handle UnauthorizedError and ScopeDeniedError yourself — see Retry and Troubleshooting.