# Errors

The typed error envelope, repair metadata, and caller actions.

Canonical HTML: https://developer.shelfcycle.com/guides/errors

Every error uses the same envelope with a stable code, a message, optional validation and repair details, and the request id.

## Envelope

```json
{
  "error": {
    "type": "validation_error",
    "code": "validation_failed",
    "message": "Request validation failed.",
    "param": "type",
    "errors": [{ "path": "type", "message": "Use types instead." }],
    "suggestedAction": "replace_param",
    "repairCategory": "unknown_param",
    "documentationPath": "/api/v1/openapi.json",
    "doNotRetrySameRequest": true,
    "requestId": "request-id"
  }
}
```

Keep the `requestId` for debugging.

## Repair metadata

v1.21 errors can include machine-readable repair fields, including write-readiness and impact details on supported product and Cost Book writes. Treat them as the next-request plan, not as hints that relax the schema.

| Field | Use it for |
| --- | --- |
| `repairCategory` | Classify the failure, such as `unknown_param`, `invalid_enum`, `unsupported_sync_filter`, `standalone_status`, `invalid_window`, or `rate_limited`. |
| `suggestedAction` | Choose the next action, such as `add_required_filter`, `replace_param`, `choose_valid_value`, `pair_date_window`, `narrow_filter`, `wait_and_retry`, or `stop_and_fix_request`. |
| `validValues` | Use only accepted values for the named parameter. |
| `acceptedFilters` | Pick one of the endpoint's supported narrowing filters instead of inventing a broad sync request. |
| `requiredCombination` | Send the fields together, such as paired date-window parameters. |
| `maxWindowDays` | Split changed-record discovery windows that are too wide. |
| `documentationPath` | Open `/api/v1/openapi.json` or `/llms.txt` for the exact contract guidance. |
| `example` | Shape the corrected request with an endpoint-specific example. |
| `doNotRetrySameRequest` | Change the request before retrying. Do not loop the identical request. |
| `writeReadiness` | Review machine-readable readiness details when a write request fails validation or policy checks. |
| `impactAnalysis` | Inspect product or product-family impact details before retrying a constrained catalog write. |

For example, if `/search` rejects `type`, replace it with `types`. If `/contacts` rejects a broad list request, provide `parentType` and `parentId`. If `/orders` or `/purchase-orders` rejects standalone `status`, pair it with an exact identifier or a bounded date window.

## Common codes

| Code | Caller action |
| --- | --- |
| `invalid_api_key` | Check the environment value and bearer header format. |
| `api_key_revoked` | Stop and request a new key. |
| `api_key_expired` | Stop and request a new key. |
| `api_actor_inactive` | Stop and issue a key to an active user. |
| `missing_scope` | Use a key with the required scope. |
| `missing_permission` | Use an acting user with the required ShelfCycle permission. |
| `rate_limited` | Wait for Retry-After or `retryAfterSeconds`. Retry POSTs with the same idempotency key. |
| `validation_failed` | Fix the field named in `param` or `errors[]`. |
| `finder_filter_required` | Add a supported narrowing predicate before retrying a root finder. |
| `unsupported_sync_filter` | Remove sync-style filters such as `updatedSince`. Root finders are not exports. Reference directory and child-list routes support cursor paging only in v1.21. For sales orders and purchase orders, use paired `updatedAtFrom`/`updatedAtTo` only for bounded changed-record discovery. |
| `not_found` | Re-search and confirm the id. Cross-org, unsupported, archived, or invisible records can also return not_found. |
| `idempotency_key_required` | Send a stable Idempotency-Key for POST requests. |
| `idempotency_key_reused` | Retry with the original body or choose a new key for a new source event. |
| `duplicate_customer` | Use the existing customer or change the source data before retrying. |
| `duplicate_supplier` | Use the existing supplier or change the source data before retrying. |
| `duplicate_product` | Use the existing product or change product setup references before retrying. |
| `duplicate_family` | Use the existing product family or change the family identity before retrying. |
| `duplicate_contact` | Use the existing contact or change the source data before retrying. |
| `duplicate_customer_location`, `duplicate_customer_address`, `duplicate_supplier_ship_from_location` | Use the existing child record or change source identity/address data before retrying. |
| `duplicate_document` | Use the existing attached document or change the source URL before retrying. |
| `stale_record` | Refresh context before retrying. |
| `archived_record` | Do not write to archived records. |
| `unsupported_linked_record` | Remove unsupported records. Orders and purchase orders are context, not note subjects or links. |
| `not_api_created` | Do not patch that note through the public API. |
| `primary_change_unsupported`, `address_role_required` | Remove primary switching from customer location requests, or keep customer addresses marked billing or shipping. |
| `unsupported_product_field`, `unsupported_family_field`, `invalid_variable_qty`, `reference_visibility_blocked` | Keep product setup inside the public contract, use quantity 1 for variable packaging, and verify referenced records are visible. |
| `document_url_invalid`, `document_url_cross_org`, `unsupported_document_field` | Send only `fileName` and an HTTPS URL that belongs to the authenticated org when it points at a ShelfCycle bucket. |
| `blocked_order_sensitive_update` | Change only allowed metadata fields, or leave order-sensitive product identity, packaging, and supplier fields unchanged. |

On `dryRun=true`, HTTP 200 can still return `data.status: "blocked"`. Inspect blocked checks such as `duplicate_candidate_found`, `idempotency_target_missing`, `idempotency_key_required`, `idempotency_key_reused`, `stale_record`, `archived_record`, `target_not_found_or_not_visible`, `unsupported_profile_shape`, `primary_change_unsupported`, `address_role_required`, `product_family_reference_active`, `product_packaging_reference_active`, `supplier_reference_active`, `reference_visibility_blocked`, `order_sensitive_update_blocked`, `unsupported_linked_record`, `not_api_created`, or `validation_failed` before execution.

## Rate limits

Default private v1 limits are 60 read requests per minute per key and endpoint, and 20 write requests per minute per key and endpoint.

On `429 rate_limited`, read `Retry-After` and wait before retrying. Successful authenticated requests and rate-limited requests include `X-RateLimit-Remaining` when the rate-limit check runs.

> **Guardrail**: Stop on stale, duplicate, missing access, unsupported sync, revoked, expired, archived, or rate-limit errors. Do not keep retrying without new information.
