Errors and retry decisions
Branch on stable error codes and retry only operations that are safe and transient.
Public API failures use OpenAPI schema ErrorResponse:
{
"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_unavailablecan be409or422depending 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_exceededincludesRetry-After; the other429codes require a state or configuration change.- Before a job-creation POST, generate and persist a non-sensitive
Idempotency-Keyof 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_reusedmeans the input differs from the original request. Use the exact original input or a new key.idempotency_in_progressmeans 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.
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 for client deadlines and Limits, budgets, and usage for the three distinct 429 cases.