PAMbaseDocs
Reference

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

json
{
"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

HTTPcodeCauseRecoverySDK class
400bad_requestMalformed request, invalid scope in manifest, or code mismatchFix the request / manifestValidationError
400validation_failedBody failed Zod validationRead details (issues array) and fix the fieldsValidationError
401unauthorizedMissing, invalid, expired, or revoked tokenRe-run the OAuth connect flowUnauthorizedError
402limit_exceededPlan cap reachedUpgrade — see LimitsPAMbaseError
403forbiddenApp suspended or client mismatchContact support / check credentialsPAMbaseError
403scope_deniedToken lacks the required scopeRequest it in your manifest / consentScopeDeniedError
404not_foundResource does not exist or is out of scopeVerify the idNotFoundError
409conflictEmail or slug already takenChoose a different valuePAMbaseError
429rate_limitedToo many requestsBack off — SDK exposes retryAfterMsRateLimitError
500internalServer errorRetry with backoff; report if persistentServerError
402 happens at the write/connect point

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.

scope_denied is not unauthorized

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

403 scope_denied
{
"error": {
"code": "scope_denied",
"message": "Scope not granted: memory:write:fitness"
}
}
400 validation_failed
{
"error": {
"code": "validation_failed",
"message": "Invalid request body",
"details": [
{ "path": ["fireAt"], "message": "Invalid datetime", "code": "invalid_string" }
]
}
}
429 rate_limited
{
"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:

typescript
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 / revoked
return redirectToConnect();
}
if (err instanceof ScopeDeniedError) {
// 403 — token lacks a scope; .scope tells you which
console.warn("Add this scope to your manifest:", err.scope);
return;
}
if (err instanceof RateLimitError) {
// 429 — back off the suggested amount, then retry
await new Promise((r) => setTimeout(r, err.retryAfterMs));
return;
}
if (err instanceof ValidationError) {
console.error("Fix these fields:", err.details);
return;
}
// Anything else: normalize and rethrow
throw classifyError(err);
}
Retries are automatic

The SDK already retries NetworkError, RateLimitError, and 5xx with backoff. You usually only need to handle UnauthorizedError and ScopeDeniedError yourself — see Retry and Troubleshooting.