# Errors and retry decisions

> Branch on stable error codes and retry only operations that are safe and transient.

Canonical guide: <https://api.blackpearl.com/docs/errors>

Raw Markdown: <https://api.blackpearl.com/docs/raw/errors.md>

OpenAPI: <https://api.blackpearl.com/v1/openapi.json>

Public API failures use OpenAPI schema `ErrorResponse`:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "A readable explanation of what went wrong.",
    "request_id": "req_example",
    "docs_url": "https://api.blackpearl.com/docs/errors"
  }
}
```

Use `error.code`, not HTTP status alone, to decide what happens next. Preserve `request_id` for diagnostics. Validation failures can also include structured `details`.

## Error and retry matrix

| Codes | Status | Retry? | Caller action |
|---|---:|---|---|
| `invalid_request`, `invalid_cursor`, `invalid_domain`, `brand_profile_mismatch`, `invalid_campaign_key`, `invalid_campaign_metadata`, `invalid_campaign_name`, `invalid_campaign_objective`, `invalid_campaign_status`, `campaign_relation_mismatch`, `invalid_stage` | 400 or 422 | No | Correct the request. |
| `missing_api_key`, `invalid_api_key`, `expired_api_key`, `authentication_required` | 401 | No | Supply an active project key. |
| `insufficient_credits` | 402 or terminal job failure | No | Let auto-recharge complete or add credits. Admission only requires a positive balance; a terminal failure means the actual charge exceeded what remained. |
| `forbidden`, `project_archived`, `capability_disabled` | 403 | No | Change project state or obtain access. |
| `not_found`, `result_unavailable`, `playbooks_document_unavailable`, `brand_not_found`, `brand_profile_not_found`, `offer_not_found`, `account_not_found`, `playbook_job_not_found`, `message_template_revision_not_found`, `company_dossier_specification_not_found` | 404 | No | Check the ID or domain, project ownership, environment, and retained data. |
| `job_expired` | 410 | No | Use the result stored by the client or create a new job. |
| `not_ready`, `job_not_finished`, `audiences_result_unavailable`, `idempotency_in_progress` | 409 | Yes, after delay | Continue the documented read/poll flow or repeat the same idempotent request. |
| `conflict`, `idempotency_key_reused`, `campaign_archived`, `campaign_key_exists`, `invalid_campaign_transition`, `cycle_in_progress`, `offer_archived`, `brand_profile_archived`, `brand_profile_not_ready`, `brand_guideline_missing`, `missing_default_brand_profile`, `playbook_template_not_ready` | 409 | No | Read or repair resource state; use a new idempotency key for changed input. For `playbook_template_not_ready`, wait for the template's one-time research to finish (or retry it from the template page). |
| `account_unanalyzable` | 422 | No | Check that the domain serves a fetchable public website or continue without on-demand analysis. |
| `rate_limit_exceeded` | 429 | Yes | Wait at least `Retry-After`, then retry with backoff and jitter. |
| `quota_exceeded` | 429 | No automatic retry | Inspect `error.details` for scope, counters, and `resets_at`; wait for reset or change the quota. |
| `budget_exceeded` | 429 | No automatic retry | Inspect `error.details` for spend, limit, and `resets_at`; increase the hard budget or wait for reset. When `error.details.scope` is `"usage_tier"`, the organization's tier-based monthly cap was reached — the cap rises automatically as paid usage history grows, or an administrator can adjust the tier. |
| `account_research_backend_quota` | 429 | No automatic retry | Wait for the provider's analysis capacity to reset or ask an operator to increase it. |
| `audiences_backend_error`, `audiences_backend_invalid_response`, `audiences_backend_unavailable` | 502 or 503 | Bounded retry for safe reads | Back off with jitter; report persistent failures with `request_id`. |
| `account_research_backend_error`, `account_research_unavailable` | 502 or 503 | Bounded retry | Back off with jitter; report persistent failures with `request_id`. |
| `account_research_unauthorized` | 502 | No | Ask an operator to repair the platform's research-backend credential. |
| `account_research_not_configured` | 500 | No | Ask an operator to configure the organization's research-backend identity. |
| `internal_error` | 500 | Safe reads only | Retry a GET with bounds. Do not automatically repeat job creation. |

## Endpoint-specific codes

The OpenAPI `PublicErrorCode` enum is the authoritative finite vocabulary. The
matrix above describes the shared codes; the more precise endpoint codes below
preserve which resource, snapshot, policy, or idempotency rule failed. These
codes are non-retryable unless the endpoint guide explicitly describes the
state change required before retrying. Clients should still retain a default
branch for forward-compatible additions.

- Capability failures (`403`): `documents_capability_disabled`, `professional_network_capability_disabled`, `prospect_research_capability_disabled`.

- Scoped resource failures (`404`): `assessment_not_found`, `audience_member_not_found`, `audience_not_found`, `campaign_not_found`, `company_dossier_connection_not_found`, `connection_not_found`, `data_intake_specification_not_found`, `enrichment_specification_not_found`, `playbook_template_not_found`, `professional_network_connection_not_found`, `professional_network_specification_not_found`, `prospect_not_found`, `prospect_research_specification_not_found`, `prospecting_job_not_found`, `qualification_policy_not_found`, `site_audit_specification_not_found`.

- State, version, idempotency, and limit conflicts (`409`): `account_coordination_evidence_too_large`, `account_coordination_idempotency_conflict`, `account_coordination_policy_not_current`, `audience_idempotency_conflict`, `audience_result_changed`, `audience_review_idempotency_conflict`, `audience_source_changed`, `company_dossier_connection_credentials_changed`, `company_dossier_connection_incomplete`, `company_dossier_connector_manifest_changed`, `company_dossier_policy_not_current`, `company_dossier_specification_idempotency_conflict`, `company_dossier_specification_limit_exceeded`, `company_dossier_specification_limit_reached`, `data_intake_policy_not_current`, `data_intake_specification_idempotency_conflict`, `data_intake_specification_limit_exceeded`, `data_intake_specification_limit_reached`, `enrichment_policy_not_current`, `enrichment_selection_too_large`, `enrichment_specification_idempotency_conflict`, `enrichment_specification_limit_exceeded`, `enrichment_specification_limit_reached`, `message_template_hash_conflict`, `message_template_key_revision_limit_reached`, `message_template_policy_not_current`, `message_template_previous_hash_conflict`, `message_template_revision_idempotency_conflict`, `message_template_revision_limit_exceeded`, `message_template_revision_limit_reached`, `message_template_revision_renderer_unavailable`, `message_template_revision_sequence_conflict`, `offer_key_exists`, `offer_limit_exceeded`, `outcome_idempotency_conflict`, `professional_network_audience_changed`, `professional_network_audience_source_changed`, `professional_network_audience_source_invalid`, `professional_network_connection_incomplete`, `professional_network_connector_manifest_changed`, `professional_network_connector_unavailable`, `professional_network_credentials_changed`, `professional_network_policy_not_current`, `professional_network_selection_too_large`, `professional_network_specification_idempotency_conflict`, `professional_network_specification_limit_exceeded`, `professional_network_specification_limit_reached`, `prospect_research_audience_changed`, `prospect_research_audience_source_changed`, `prospect_research_audience_source_invalid`, `prospect_research_policy_not_current`, `prospect_research_selection_too_large`, `prospect_research_specification_idempotency_conflict`, `prospect_research_specification_limit_exceeded`, `prospect_research_specification_limit_reached`, `prospecting_continuation_idempotency_conflict`, `prospecting_continuation_unavailable`, `prospecting_job_not_ready`, `prospecting_parent_changed`, `prospecting_result_unavailable`, `qualification_policy_idempotency_conflict`, `qualification_policy_limit_exceeded`, `qualification_policy_limit_reached`, `qualification_policy_not_current`, `site_audit_policy_not_current`, `site_audit_specification_idempotency_conflict`, `site_audit_specification_limit_exceeded`, `site_audit_specification_limit_reached`. `audience_source_unavailable` can be `409` or `422` depending on whether the source changed or is invalid for the requested operation.

- Endpoint validation failures (`422`): `audience_empty`, `enrichment_member_evidence_invalid`, `enrichment_members_not_included`, `enrichment_members_unavailable`, `enrichment_prerequisites_missing`, `enrichment_selection_empty`, `invalid_insight_group`, `invalid_offer_field`, `invalid_offer_key`, `invalid_offer_name`, `invalid_offer_stage`, `invalid_offer_status`, `message_template_body_too_long`, `message_template_empty_body`, `message_template_empty_subject`, `message_template_required_variables_missing`, `message_template_subject_too_long`, `message_template_unknown_variables`, `message_template_variable_too_long`, `outcome_connector_required`, `outcome_predates_prospecting_job`, `outcome_source_mismatch`, `professional_network_audience_source_unavailable`, `professional_network_member_prerequisites_missing`, `professional_network_members_unavailable`, `professional_network_selection_empty`, `prospect_research_audience_source_unavailable`, `prospect_research_member_prerequisites_missing`, `prospect_research_members_unavailable`, `prospect_research_selection_empty`, `prospecting_result_too_large`, `synthetic_prospect`, `synthetic_prospecting_result`.

- Audiences backend failures (`502`, bounded retry for safe reads): `audiences_backend_generation_failed`, `audiences_backend_timeout`.

## Safe retries

- GET operations are read-only and may be retried when the code is transient.
- `rate_limit_exceeded` includes `Retry-After`; the other `429` codes require a state or configuration change.
- Before a job-creation POST, generate and persist a non-sensitive `Idempotency-Key` of at most 255 characters. The key is scoped to the authenticated project and operation.
- If the connection is lost or the response is ambiguous, repeat the exact operation, input, and key. Matching retries within 24 hours return the original response without creating or billing another job.
- `idempotency_key_reused` means the input differs from the original request. Use the exact original input or a new key. `idempotency_in_progress` means the first matching request has not stored its response yet; retry the same request after a short delay.
- After 24 hours, the same key can create new work. Persist the returned job ID and do not use idempotency as long-term job storage.
- Set retry and total-operation limits. Jitter prevents synchronized clients from retrying together.

```javascript
const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
if (response.status === 429 && body.error?.code === "rate_limit_exceeded") {
  await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
```

See [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs) for client deadlines and [Limits, budgets, and usage](https://api.blackpearl.com/docs/usage-quotas) for the three distinct `429` cases.
