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#

CodesStatusRetry?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_stage400 or 422NoCorrect the request.
missing_api_key, invalid_api_key, expired_api_key, authentication_required401NoSupply an active project key.
insufficient_credits402 or terminal job failureNoLet 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_disabled403NoChange 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_found404NoCheck the ID or domain, project ownership, environment, and retained data.
job_expired410NoUse the result stored by the client or create a new job.
not_ready, job_not_finished, audiences_result_unavailable, idempotency_in_progress409Yes, after delayContinue 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_ready409NoRead 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_unanalyzable422NoCheck that the domain serves a fetchable public website or continue without on-demand analysis.
rate_limit_exceeded429YesWait at least Retry-After, then retry with backoff and jitter.
quota_exceeded429No automatic retryInspect error.details for scope, counters, and resets_at; wait for reset or change the quota.
budget_exceeded429No automatic retryInspect 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_quota429No automatic retryWait for the provider's analysis capacity to reset or ask an operator to increase it.
audiences_backend_error, audiences_backend_invalid_response, audiences_backend_unavailable502 or 503Bounded retry for safe readsBack off with jitter; report persistent failures with request_id.
account_research_backend_error, account_research_unavailable502 or 503Bounded retryBack off with jitter; report persistent failures with request_id.
account_research_unauthorized502NoAsk an operator to repair the platform's research-backend credential.
account_research_not_configured500NoAsk an operator to configure the organization's research-backend identity.
internal_error500Safe reads onlyRetry 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.
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.