solarOSsolarOS Docs
Api

The error envelope

Every non-2xx response has this shape:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "firstName is required",
    "details": { "issues": ["..."] }
  },
  "meta": {
    "requestId": "req_...",
    "version": "v1",
    "timestamp": "2026-09-21T12:00:00.000Z",
    "rateLimit": { "limit": 5000, "remaining": 4999, "reset": 1758456000 }
  }
}

error.details is optional and, when present, usually carries validation issue details. meta.rateLimit is included on every response, success or error, so you can track your remaining quota without a separate call.

Every error code

This table is generated from OrgApiErrorCode in apps/web/src/lib/org-api/responses.ts; it can't drift out of sync with the API. It's also published on its own at Reference → Error codes.

CodeHTTP statusMeaning
MISSING_API_KEY401No Authorization/x-api-key header supplied.
INVALID_API_KEY401Key malformed or no bcrypt match.
REVOKED_API_KEY401Key revoked, outside rotation grace window.
EXPIRED_API_KEY401Key is past its expiresAt.
IP_NOT_ALLOWED403Caller IP isn't in the key's IP allowlist.
BILLING_ACCESS_DENIED402Org's billing/access level blocks this route.
INSUFFICIENT_SCOPE403Key is missing a required scope for this operation.
FORBIDDEN403Generic authorization failure (e.g. author-only edit).
RATE_LIMITED429Rate-limit bucket exceeded; see Retry-After header.
QUOTA_EXCEEDED402A route-specific quota check failed.
VALIDATION_FAILED400Request body failed Zod/shape validation.
NOT_FOUND404Resource not found, or belongs to another organization.
CONFLICT409Generic conflict with existing state.
IDEMPOTENCY_REQUIRED400Idempotency-Key header missing on a mutating request that requires it.
IDEMPOTENCY_CONFLICT409Same Idempotency-Key replayed with a different request body.
OWNERSHIP_REQUIRED400Missing or invalid owner assignment on a created resource.
EXTERNAL_REFERENCE_CONFLICT409An External Reference tuple already points at a different resource.
SEAT_LIMIT_REACHED409Creating a schedulable team member would exceed the included technician seats; requires an interactive org-admin action.
AURORA_IDENTIFIERS_REQUIRED400Aurora sync request is missing design/project identifiers.
CUSTOM_FIELD_VALIDATION_FAILED400customFields payload fails the tenant Field contract.
UNSUPPORTED_PROJECT_TEMPLATE400Template can't safely apply (e.g. FROM_QUOTE-only fields).
UNSUPPORTED_SORT400Invalid sortBy/sortDir combination.
UNSUPPORTED_FILTER400Invalid or unsupported query filter.
INTERNAL_ERROR500Uncaught exception — treat as a solarOS bug, retry with backoff.

Rate limit headers

Every response that has rate-limit info carries three headers, set alongside meta.rateLimit:

  • X-RateLimit-Limit: the request cap for the current window.
  • X-RateLimit-Remaining: requests left in the current window.
  • X-RateLimit-Reset: when the window resets (Unix timestamp, seconds).

A 429 RATE_LIMITED response additionally carries Retry-After (seconds to wait before retrying), and its error message states the same number: Rate limit exceeded. Retry after <n> seconds.

IDEMPOTENCY_CONFLICT vs. a safe retry

Most POST operations require an Idempotency-Key header: a string you generate per logical operation (e.g. derived from the source record's ID). What happens when you send the same key again depends on whether the request body matches:

  • Same key, same body → solarOS recognizes the replay and returns the original response again (with an Idempotency-Replayed: true header). This is the safe case: retry a timed-out request with the exact same key and body, and you'll never create a duplicate.
  • Same key, different body409 IDEMPOTENCY_CONFLICT. This means you reused a key for what solarOS sees as a different logical request. Don't retry as-is; generate a new key for the new body, or investigate why the same key was reused with different data (a common cause: a script deriving the key from a timestamp instead of a stable source-record ID).

Idempotency-Key protects against accidental duplicate requests (safe retries). It's a separate mechanism from External References (externalReferences on create calls, and the /external-references/* endpoints), which is how you reconcile solarOS records against IDs from another system across separate calls. See Migrate from another CRM for both used together.

Was this page helpful?

On this page