response.status first before parsing the body.
This page covers the canonical error shape, every standard HTTP status code, and a worked example of defensive client code.
Target error shape
The canonical error response the CRM+ API is moving toward is:| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable error code in SCREAMING_SNAKE_CASE. Safe to use in switch statements. |
error.message | string | Human-readable explanation, safe to log. Never contains stack traces or database error text. |
error.details | array | Field-level validation errors. Present as an empty array [] when no field-specific errors apply. |
error.details[].field | string | The camelCase field name that failed validation. |
error.details[].code | string | Machine-readable SCREAMING_SNAKE_CASE code for this specific field failure. |
error.details[].message | string | Human-readable description of this specific validation failure. |
The CRM+ API is in the process of migrating to this canonical shape. Some endpoints currently return plain-text error messages or non-standard JSON structures — particularly for
401 Unauthorized responses, which may return Authorization has been denied for this request. as plain text. Write your error handling to inspect response.status first, then attempt to parse the body. Do not assume the body is always valid JSON or always matches the canonical shape.Standard error codes
| HTTP Status | Code | Meaning | Common causes |
|---|---|---|---|
400 | BAD_REQUEST | Malformed request | Invalid JSON syntax, missing required headers, unparseable request body |
401 | UNAUTHENTICATED | Authentication failed | Missing Authorization header, expired OAuth token, revoked API Key |
403 | FORBIDDEN | Authorization failed | Valid credentials but insufficient permissions for this resource or action |
404 | NOT_FOUND | Resource not found | The ID in the path does not match any existing record |
409 | CONFLICT | State conflict | Duplicate record, uniqueness constraint violation |
422 | VALIDATION_FAILED | Semantic validation failed | Valid JSON syntax but a field value violates a business rule (e.g., negative gift amount, future gift date on a completed gift) |
429 | RATE_LIMITED | Rate limit exceeded | More than 1,500 requests in the current hour — see Rate Limits |
500 | INTERNAL_ERROR | Server error | Unexpected error on the Virtuous side — not caused by the request |
503 | SERVICE_UNAVAILABLE | Service unavailable | Temporary outage — retry after the period indicated in the Retry-After header |
404 means the specific resource does not exist. An empty search result on a list endpoint is not a 404 — it returns 200 with list: [] and total: 0. Code that treats 404 as “no records matched” will misinterpret real missing-resource errors.Handling errors in code
A production-grade CRM+ client should: inspect the status before parsing the body, fall back gracefully when the body is plain text, branch on status for actionable error types, and respect theRetry-After header on 429.
Validation error details
When the API returns422 VALIDATION_FAILED, the error.details array contains one entry per field that failed validation. Use these entries to surface specific error messages to your users or to identify the exact field that needs correction.
error.details is an empty array ([]), the error applies to the request as a whole, not to any specific field.
Retryable vs. non-retryable errors
Not every error is worth retrying. Categorize errors before deciding whether to retry:| Category | Status codes | Retry? | Notes |
|---|---|---|---|
| Transient | 429, 500, 502, 503, 504 | Yes — with backoff | Respect the Retry-After header on 429 and 503. |
| Authentication | 401 | Only after refreshing the credential | For OAuth, attempt one refresh and one retry. For API Keys, do not retry — the key has been revoked. |
| Authorization | 403 | No | The credential lacks the required permission. Retrying with the same credential will fail again. |
| Client error | 400, 404, 409, 422 | No | The request needs to change. Retrying the identical request will produce the identical error. |
429 responses.
Cross-API error handling
If your integration uses both Raise and CRM+, note that the two APIs return different error shapes. CRM+ uses anerror.code / error.message / error.details[] envelope. Raise uses an RFC 7807–style title / status / detail envelope with field-level errors in a flat errors map.
| Product | Top-level error fields | Validation details |
|---|---|---|
| CRM+ | error.code, error.message, error.details[] | error.details[].field, error.details[].code, error.details[].message |
| Raise | title, status, detail | errors.{fieldName}[] |
error (CRM+) or title (Raise). See Raise Error Handling for the Raise-specific details.
Next steps
Rate Limits
The retry-with-backoff pattern for handling
429 Too Many Requests responses.Pagination and Filtering
How to iterate large result sets and how empty-result responses differ from
404.Authentication
How to fix
401 and 403 responses by checking credentials and permission groups.Reconcile Failed Syncs
Patterns for retrying, deduplicating, and recovering from partial failures in bulk operations.