# Blackpearl Developer Documentation — Full Bundle This file is generated from canonical public Markdown in manifest order. OpenAPI: Curated index: Documentation MCP: --- # Quickstart > Create your first job, poll it safely, and print a useful sales result. Canonical guide: Raw Markdown: OpenAPI: This guide takes you from a project API key to a business summary and sales angle. You do not need another guide to complete the flow. It uses [Playbooks](https://api.blackpearl.com/docs/playbooks) as the example capability. Every capability in the Build section follows the same pattern shown here — authenticate with a project key, create a job, poll it to a terminal state — so this flow transfers directly. > [!NOTE] > Working from an AI coding agent such as Claude Code, Codex, or Cursor? Connect the read-only documentation MCP server and hand your agent the [Playbooks API primer](https://api.blackpearl.com/docs/playbooks-api-primer) — setup commands are in [AI and machine-readable documentation](https://api.blackpearl.com/docs/agent-resources). ## Prerequisites 1. In the console, create or select a **development project**. A project owns its keys, usage, quotas, and budget. 2. Open **API keys**, create a key for that project, and copy the secret when it is shown. It cannot be retrieved later. 3. Install `curl` and `jq`. 4. Export the configured API URL and your key: ```bash export BLACKPEARL_API_BASE_URL="https://api.blackpearl.com" export BLACKPEARL_API_KEY="replace-with-your-project-key" export BLACKPEARL_IDEMPOTENCY_KEY="playbook-$(date +%s)-$$" ``` > [!WARNING] > Keep keys in a server-side secret manager. Do not commit them or embed them in browser or mobile code. A hosted development-project request performs real work and records real usage; a key prefix does not create a simulation mode. ## 1. Create the smallest useful Playbooks job The only input requirement is a non-empty `target_company`, a valid `target_company_domain`, or both. This request uses the domain: ```bash CREATE_RESPONSE=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/playbooks" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: $BLACKPEARL_IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data '{"target_company_domain":"acme.example"}') printf '%s\n' "$CREATE_RESPONSE" | jq . ``` The API returns `202 Accepted`. The response has this exact field structure; IDs and timestamps are generated for your request: ```json { "id": "job_example", "status": "queued", "progress": 0, "project_id": "proj_example", "campaign_id": null, "type": "playbooks", "input": { "target_company": null, "target_company_domain": "acme.example", "anchor_company": null, "product_info": null, "tone": "consultative", "sections": null, "model": "default", "brand_profile_id": null, "brand_profile_key": null }, "result": null, "error": null, "usage": { "tokens": 0, "cost_usd": 0.0, "backend_requests": 0, "backend_runtime_ms": 0, "candidates": null }, "stages": null, "created_at": "2026-07-13T12:00:00Z", "expires_at": "2026-08-12T12:00:00Z", "started_at": null, "finished_at": null } ``` - `id` is the durable job identifier. Persist it before polling. - `Idempotency-Key` makes an ambiguous create safe to repeat with the exact same input for 24 hours. - `status` is `queued` or `running` while active, then `succeeded`, `failed`, or `canceled`. - `progress` is a coarse percentage from 0 through 100. - `project_id` identifies the project charged for the work; Playbooks has no `campaign_id`. - `input` is the normalized request snapshot. Defaults appear even when you omit them. - `result` remains `null` until success. `error` remains `null` unless the job fails. - `usage` is the work attributed to this job. `stages` is `null` for Playbooks. - The timestamps show when the job was accepted, when its 30-day retention window ends, when execution started, and when it reached a terminal state. The response conforms to OpenAPI schema `PublicPlaybookJobResponse`; the request conforms to OpenAPI schema `PlaybookInput`. Its OpenAPI operation is `create_playbook_v1_playbooks_post`. ## 2. Persist and poll with a deadline This shell flow resumes an existing job from `.blackpearl-playbooks-job` and creates one only when no saved ID exists. It polls with capped backoff, stops at every terminal state, and leaves the ID on disk when the local deadline expires. ```bash STATE_FILE="${BLACKPEARL_JOB_STATE_FILE:-.blackpearl-playbooks-job}" IDEMPOTENCY_FILE="${STATE_FILE}.idempotency" DEADLINE_SECONDS="${BLACKPEARL_POLL_DEADLINE_SECONDS:-120}" if [ -s "$STATE_FILE" ]; then JOB_ID=$(tr -d '\r\n' < "$STATE_FILE") else if [ ! -s "$IDEMPOTENCY_FILE" ]; then printf 'playbook-%s-%s\n' "$(date +%s)" "$$" > "$IDEMPOTENCY_FILE" fi IDEMPOTENCY_KEY=$(tr -d '\r\n' < "$IDEMPOTENCY_FILE") CREATE_RESPONSE=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/playbooks" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data '{"target_company_domain":"acme.example"}') || exit 1 JOB_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.id') || exit 1 printf '%s\n' "$JOB_ID" > "$STATE_FILE" fi deadline=$((SECONDS + DEADLINE_SECONDS)) delay=2 while [ "$SECONDS" -lt "$deadline" ]; do JOB=$(curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/jobs/$JOB_ID" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY") || exit 1 STATUS=$(printf '%s' "$JOB" | jq -er '.status') || exit 1 printf 'status=%s progress=%s%%\n' "$STATUS" "$(printf '%s' "$JOB" | jq -r '.progress')" case "$STATUS" in succeeded) printf 'Business summary: %s\n' "$(printf '%s' "$JOB" | jq -er '.result.business_summary')" printf 'First sales angle: %s\n' "$(printf '%s' "$JOB" | jq -er '.result.sales_angles[0]')" rm -f "$STATE_FILE" rm -f "$IDEMPOTENCY_FILE" exit 0 ;; failed) printf 'Playbooks job failed: %s\n' "$(printf '%s' "$JOB" | jq -r '.error // "No failure message returned"')" >&2 rm -f "$STATE_FILE" rm -f "$IDEMPOTENCY_FILE" exit 1 ;; canceled) printf 'Playbooks job was canceled.\n' >&2 rm -f "$STATE_FILE" rm -f "$IDEMPOTENCY_FILE" exit 1 ;; queued|running) ;; *) printf 'Unknown job status: %s\n' "$STATUS" >&2; exit 1 ;; esac sleep "$delay" if [ "$delay" -lt 10 ]; then delay=$((delay * 2)); fi if [ "$delay" -gt 10 ]; then delay=10; fi done printf 'Polling deadline reached; resume job %s using %s.\n' "$JOB_ID" "$STATE_FILE" >&2 exit 2 ``` > [!IMPORTANT] > A client timeout does not cancel server work. Keep the job ID and resume later. Job polling is rate-limited but is not billed as another Playbooks generation request. The API retains the job and result through `expires_at`, 30 days after creation. Store any result your application needs beyond that timestamp. After expiry, job and candidate reads return `410 job_expired`; expiry does not remove usage, cost, or audit records. If the create connection is lost before you receive the job ID, repeat the exact POST with the key saved in `IDEMPOTENCY_FILE`. A matching request returns the original `202` body and does not create or bill another job. Do not change the input while reusing a key, and do not expect replay after its 24-hour window. ## Expected output A successfully configured Playbooks backend produces output with this structure; generated content varies by request and backend: ```text status=running progress=55% status=succeeded progress=100% Business summary: First sales angle: ``` An unconfigured backend fails the Job instead of returning synthetic content. ## Next steps - Run the tested [Python or TypeScript quickstart](#python-and-typescript-versions) when moving this flow into an application. - Learn how to store and resume jobs in [Jobs and polling](https://api.blackpearl.com/docs/jobs). - Add seller context and choose sections in [Playbooks](https://api.blackpearl.com/docs/playbooks). - Handle API failures by `error.code` using the [error and retry matrix](https://api.blackpearl.com/docs/errors). ## Python and TypeScript versions The same flow ships as tested, dependency-light programs. The complete sources are embedded below; in the repository they live under `docs/examples/`. Both read only `BLACKPEARL_API_BASE_URL` and `BLACKPEARL_API_KEY` for credentials and connection details. Run the standard-library-only script: ```bash python3 docs/examples/python/playbooks_quickstart.py ``` ```python #!/usr/bin/env python3 """Minimal, dependency-free Playbooks quickstart. Required environment: BLACKPEARL_API_BASE_URL Configured gateway URL, for example http://localhost:8400 BLACKPEARL_API_KEY Project-scoped API key """ from __future__ import annotations import json import os import random import sys import time import uuid from pathlib import Path from typing import Any from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen TERMINAL = {"succeeded", "failed", "canceled"} TRANSIENT_READ_CODES = { "rate_limit_exceeded", "internal_error", "audiences_backend_error", "audiences_backend_invalid_response", "audiences_backend_unavailable", } class APIError(RuntimeError): def __init__(self, status: int, code: str, message: str, retry_after: str | None = None): super().__init__(f"API error {status} {code}: {message}") self.status = status self.code = code self.retry_after = retry_after def required_env(name: str) -> str: value = os.environ.get(name, "").strip() if not value: raise RuntimeError(f"Set {name} before running this example") return value def request_json( method: str, url: str, api_key: str, body: dict[str, Any] | None = None, extra_headers: dict[str, str] | None = None, ) -> tuple[dict[str, Any], Any]: payload = json.dumps(body).encode() if body is not None else None request = Request( url, data=payload, method=method, headers={ "Authorization": f"Bearer {api_key}", "Accept": "application/json", **({"Content-Type": "application/json"} if body is not None else {}), **(extra_headers or {}), }, ) try: with urlopen(request, timeout=30) as response: return json.load(response), response.headers except HTTPError as exc: try: payload = json.loads(exc.read()) error = payload.get("error", {}) except (json.JSONDecodeError, AttributeError): error = {} raise APIError( exc.code, str(error.get("code", "http_error")), str(error.get("message", exc.reason)), exc.headers.get("Retry-After"), ) from exc except URLError as exc: raise RuntimeError(f"Could not reach the configured API: {exc.reason}") from exc def persist_job_id(path: Path, job_id: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(f"{job_id}\n", encoding="utf-8") def persistent_idempotency_key(path: Path) -> str: if path.exists() and path.read_text(encoding="utf-8").strip(): return path.read_text(encoding="utf-8").strip() key = f"playbook-{uuid.uuid4()}" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(f"{key}\n", encoding="utf-8") return key def create_or_resume(base_url: str, api_key: str, state_path: Path) -> str: if state_path.exists() and state_path.read_text(encoding="utf-8").strip(): job_id = state_path.read_text(encoding="utf-8").strip() print(f"Resuming Playbooks job {job_id}") return job_id # Persist the key before sending. If the connection fails ambiguously, rerun # with this same request/key during the 24-hour replay window. idempotency_key = persistent_idempotency_key( state_path.with_name(f"{state_path.name}.idempotency") ) job, _ = request_json( "POST", f"{base_url}/v1/playbooks", api_key, {"target_company_domain": "acme.example"}, {"Idempotency-Key": idempotency_key}, ) job_id = job.get("id") if job.get("type") != "playbooks" or not isinstance(job_id, str) or not job_id: raise RuntimeError("Create response did not contain a typed Playbooks job ID") persist_job_id(state_path, job_id) print(f"Created Playbooks job {job_id}") return job_id def poll_job(base_url: str, api_key: str, job_id: str) -> dict[str, Any]: timeout = float(os.environ.get("BLACKPEARL_POLL_DEADLINE_SECONDS", "120")) delay = float(os.environ.get("BLACKPEARL_POLL_INITIAL_SECONDS", "2")) max_delay = float(os.environ.get("BLACKPEARL_POLL_MAX_SECONDS", "10")) deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: job, _ = request_json("GET", f"{base_url}/v1/jobs/{job_id}", api_key) except APIError as exc: if exc.code not in TRANSIENT_READ_CODES: raise if exc.code == "rate_limit_exceeded" and exc.retry_after: delay = max(delay, float(exc.retry_after)) time.sleep(min(delay, max(0.0, deadline - time.monotonic()))) delay = min(max_delay, max(delay * 2, 0.01)) continue status = job.get("status") if status not in {"queued", "running", *TERMINAL}: raise RuntimeError(f"Unknown job status: {status!r}") print(f"status={status} progress={job.get('progress')}%") if status in TERMINAL: return job jittered = delay * random.uniform(0.8, 1.2) time.sleep(min(jittered, max(0.0, deadline - time.monotonic()))) delay = min(max_delay, max(delay * 2, 0.01)) raise TimeoutError(f"Polling deadline reached; resume job {job_id} from the saved state file") def render_result(job: dict[str, Any]) -> None: status = job["status"] if status == "failed": raise RuntimeError(f"Playbooks job failed: {job.get('error') or 'No failure message returned'}") if status == "canceled": raise RuntimeError("Playbooks job was canceled") if job.get("type") != "playbooks" or status != "succeeded": raise RuntimeError("Expected a successful Playbooks job") result = job.get("result") if not isinstance(result, dict): raise RuntimeError("Successful Playbooks job did not contain a typed result") summary = result.get("business_summary") angles = result.get("sales_angles") if not isinstance(summary, str) or not summary or not isinstance(angles, list) or not angles or not isinstance(angles[0], str): raise RuntimeError("Playbooks result did not contain a useful business summary and sales angle") print(f"Business summary: {summary}") print(f"First sales angle: {angles[0]}") def main() -> int: base_url = required_env("BLACKPEARL_API_BASE_URL").rstrip("/") api_key = required_env("BLACKPEARL_API_KEY") state_path = Path(os.environ.get("BLACKPEARL_JOB_STATE_FILE", ".blackpearl-playbooks-job")) try: job_id = create_or_resume(base_url, api_key, state_path) job = poll_job(base_url, api_key, job_id) render_result(job) state_path.unlink(missing_ok=True) state_path.with_name(f"{state_path.name}.idempotency").unlink(missing_ok=True) return 0 except Exception as exc: print(str(exc), file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main()) ``` Install and run with npm: ```bash npm --prefix docs/examples/typescript install npm --prefix docs/examples/typescript start ``` ```typescript import { randomUUID } from "node:crypto"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled"; interface ErrorResponse { error?: { code?: string; message?: string }; } interface PlaybookJob { id: string; type: "playbooks"; status: JobStatus; progress: number; error: string | null; result: null | { business_summary: string | null; sales_angles: string[] }; } const terminal = new Set(["succeeded", "failed", "canceled"]); const transientReadCodes = new Set([ "rate_limit_exceeded", "internal_error", "audiences_backend_error", "audiences_backend_invalid_response", "audiences_backend_unavailable", ]); class ApiError extends Error { constructor( readonly status: number, readonly code: string, message: string, readonly retryAfter: string | null, ) { super(`API error ${status} ${code}: ${message}`); } } function requiredEnv(name: string): string { const value = process.env[name]?.trim(); if (!value) throw new Error(`Set ${name} before running this example`); return value; } async function requestJson( method: "GET" | "POST", url: string, apiKey: string, body?: object, extraHeaders: Record = {}, ): Promise<{ body: unknown; headers: Headers }> { const response = await fetch(url, { method, headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}), ...extraHeaders, }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(30_000), }); const payload: unknown = await response.json(); if (!response.ok) { const error = (payload as ErrorResponse).error; throw new ApiError( response.status, error?.code ?? "http_error", error?.message ?? response.statusText, response.headers.get("Retry-After"), ); } return { body: payload, headers: response.headers }; } function isPlaybookJob(value: unknown): value is PlaybookJob { if (!value || typeof value !== "object") return false; const job = value as Partial; return typeof job.id === "string" && job.type === "playbooks" && ["queued", "running", "succeeded", "failed", "canceled"].includes(job.status ?? ""); } const sleep = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); async function saveJobId(path: string, jobId: string): Promise { await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${jobId}\n`, "utf8"); } async function persistentIdempotencyKey(path: string): Promise { try { const key = (await readFile(path, "utf8")).trim(); if (key) return key; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } const key = `playbook-${randomUUID()}`; await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${key}\n`, "utf8"); return key; } async function createOrResume(baseUrl: string, apiKey: string, statePath: string): Promise { try { const jobId = (await readFile(statePath, "utf8")).trim(); if (jobId) { console.log(`Resuming Playbooks job ${jobId}`); return jobId; } } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } // Persist the key before sending so an ambiguous failure is safely retried // with the same request and key during the 24-hour replay window. const idempotencyKey = await persistentIdempotencyKey(`${statePath}.idempotency`); const { body } = await requestJson("POST", `${baseUrl}/v1/playbooks`, apiKey, { target_company_domain: "acme.example", }, { "Idempotency-Key": idempotencyKey }); if (!isPlaybookJob(body)) throw new Error("Create response did not contain a typed Playbooks job ID"); await saveJobId(statePath, body.id); console.log(`Created Playbooks job ${body.id}`); return body.id; } async function pollJob(baseUrl: string, apiKey: string, jobId: string): Promise { const timeoutMs = Number(process.env.BLACKPEARL_POLL_DEADLINE_SECONDS ?? "120") * 1000; let delayMs = Number(process.env.BLACKPEARL_POLL_INITIAL_SECONDS ?? "2") * 1000; const maxDelayMs = Number(process.env.BLACKPEARL_POLL_MAX_SECONDS ?? "10") * 1000; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const { body } = await requestJson("GET", `${baseUrl}/v1/jobs/${jobId}`, apiKey); if (!isPlaybookJob(body)) throw new Error("Polling response was not a typed Playbooks job"); console.log(`status=${body.status} progress=${body.progress}%`); if (terminal.has(body.status)) return body; } catch (error) { if (!(error instanceof ApiError) || !transientReadCodes.has(error.code)) throw error; if (error.code === "rate_limit_exceeded" && error.retryAfter) { delayMs = Math.max(delayMs, Number(error.retryAfter) * 1000); } } const jitteredMs = delayMs * (0.8 + Math.random() * 0.4); await sleep(Math.min(jitteredMs, Math.max(0, deadline - Date.now()))); delayMs = Math.min(maxDelayMs, Math.max(delayMs * 2, 10)); } throw new Error(`Polling deadline reached; resume job ${jobId} from the saved state file`); } function renderResult(job: PlaybookJob): void { if (job.status === "failed") throw new Error(`Playbooks job failed: ${job.error ?? "No failure message returned"}`); if (job.status === "canceled") throw new Error("Playbooks job was canceled"); const summary = job.result?.business_summary; const firstAngle = job.result?.sales_angles[0]; if (job.status !== "succeeded" || !summary || !firstAngle) { throw new Error("Playbooks result did not contain a useful business summary and sales angle"); } console.log(`Business summary: ${summary}`); console.log(`First sales angle: ${firstAngle}`); } async function main(): Promise { const baseUrl = requiredEnv("BLACKPEARL_API_BASE_URL").replace(/\/$/, ""); const apiKey = requiredEnv("BLACKPEARL_API_KEY"); const statePath = process.env.BLACKPEARL_JOB_STATE_FILE ?? ".blackpearl-playbooks-job"; const jobId = await createOrResume(baseUrl, apiKey, statePath); const job = await pollJob(baseUrl, apiKey, jobId); renderResult(job); await rm(statePath, { force: true }); await rm(`${statePath}.idempotency`, { force: true }); } main().catch((error: unknown) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }); ``` --- # Brands > Turn a company name or website into a versioned, evidence-backed brand guideline. Canonical guide: Raw Markdown: OpenAPI: Brands creates a reusable identity and messaging foundation for downstream documents, presentations, Playbooks, campaigns, and templates. Supply a company name, an official website, or both. The platform enqueues web research and stores the completed result as a strict, versioned guideline rather than an unstructured report. ## Create and poll a brand Operation `create_brand_v1_brands_post` creates a Brand and starts its first research Job. Send an `Idempotency-Key` with the first request and retain it until the returned job ID is stored. Matching retries within 24 hours replay the original response and do not create a duplicate profile. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/brands" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: brand-20260716-acme" \ --header "Content-Type: application/json" \ --data '{ "company_name": "Acme Corporation", "website_url": "https://acme.example", "is_default": false }' ``` At least one of `company_name` and `website_url` is required. The response is a typed asynchronous job with `type: "brands"`. Poll `GET /v1/jobs/{job_id}` until `status` is `succeeded`, `failed`, or `canceled`. A successful `result` contains `brand_profile_id`, `schema_version`, `revision`, `guideline_hash`, `source_count`, and `guideline_path`. ## Read brand state Operation `list_brands_v1_brands_get` uses `GET /v1/brands` to list active brands. Operation `get_brand_v1_brands__brand_id__get` uses `GET /v1/brands/{brand_id}` to inspect one. Both expose safe state metadata such as `research_status`, `research_job_id`, `guideline_available`, `guideline_revision`, and `source_count`. They do not expose raw provider responses, traces, credentials, Server.Bebop identifiers, or the internal branding snapshot. `research_status` is one of `not_started`, `queued`, `researching`, `ready`, `failed`, or `canceled`. If a refresh fails, the previous successful guideline remains stored and readable. ## Consume the guideline After the job succeeds, use operation `get_brand_guideline_v1_brands__brand_id__guideline_get` at the path returned in `result.guideline_path`: ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/brands/$BRAND_ID/guideline" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" ``` The response has stable sections for `identity`, `foundations`, `audience`, `positioning`, `voice`, `messaging`, `visual`, `applications`, `governance`, and `research`. Evidence-bearing sections reference entries in `research.sources` and carry a confidence level. Unknown facts remain null or empty rather than being invented. Bind `brand_profile_id`, `revision`, and `guideline_hash` to each downstream artifact. Do not silently change guideline revisions in the middle of a generation job. Treat generated claims and external material as reviewable context: a guideline does not authorize publishing, outreach, or writes to another system. ## Refresh and errors Operation `research_brand_v1_brands__brand_id__research_post` starts a new revision with `POST /v1/brands/{brand_id}/research`. When a run is already active, the endpoint returns that job instead of enqueuing another. Common failures include `invalid_request`, `brand_profile_not_found`, `brand_profile_archived`, `not_ready`, `capability_disabled`, `insufficient_credits`, `quota_exceeded`, and `budget_exceeded`. Follow the [error and retry guide](https://api.blackpearl.com/docs/errors); retry an ambiguous POST only with its original body and idempotency key. The complete runtime configuration, schema inventory, research safeguards, and downstream invariants are documented in the repository's `docs/brands-api.md` operator reference. --- # Offers > Reuse structured value propositions, proof points, pricing shape, ICP hints, and deal-stage fit across GTM work. Canonical guide: Raw Markdown: OpenAPI: An Offer is the organization-scoped catalog object for what a brand sells. It gives Playbooks, Campaigns, templates, and Outcome attribution one structured source of selling context. Creating or reading an Offer performs no research, pricing calculation, provider call, or outbound action. ## Create an offer Operation `create_offer_v1_offers_post` requires an active `brand_profile_id` and `name`. An optional stable `key` is derived from the name when omitted. ```bash OFFER=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/offers" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"brand_profile_id\": \"$BRAND_ID\", \"name\": \"AI SDR co-pilot\", \"key\": \"ai_sdr_copilot\", \"description\": \"Evidence-backed research, qualification, and drafting for SDR teams.\", \"value_props\": [\"Cut account research from hours to minutes\"], \"proof_points\": [\"Customer-approved proof point\"], \"pricing_model\": \"Per-seat subscription\", \"icp\": { \"industries\": [\"B2B SaaS\"], \"company_sizes\": [\"51-200\"], \"regions\": [\"ANZ\"], \"personas\": [\"VP Sales\"], \"signals\": [\"Hiring SDRs\"] }, \"stage_fit\": [\"cold_outreach\", \"discovery\"], \"status\": \"active\" }") OFFER_ID=$(printf '%s' "$OFFER" | jq -er '.id') ``` Allowed stages are `cold_outreach`, `discovery`, `evaluation`, `proposal`, `negotiation`, `closing`, `onboarding`, `expansion`, `renewal`, and `winback`. `icp` is a closed object; unknown fields and credentials are rejected. `pricing_model` is descriptive text, not executable quote or discount logic. Create is not an idempotent replay operation. A duplicate key returns `offer_key_exists`; after an ambiguous network failure, list and match by key before retrying. ## Read and reuse offers Operation `list_offers_v1_offers_get` returns up to 200 organization offers. `include_archived=true` includes catalog entries retained for historical attribution. Operation `get_offer_v1_offers__offer_id__get` returns one Offer in the caller's organization. Pass an active `offer_id` to Playbooks or bind it to a Campaign. Record the same ID on Outcomes and group insights by offer to measure which selling proposition is landing. Archived Offers remain readable historical attribution targets but cannot be selected for new generation work. Common failures include `brand_profile_not_found`, `brand_profile_archived`, `offer_key_exists`, `offer_limit_exceeded`, invalid field or stage values, and `offer_not_found`. Follow [Errors and safe retries](https://api.blackpearl.com/docs/errors). --- # Campaigns > Save repeatable targeting, bind GTM context, and run asynchronous audience cycles without granting delivery authority. Canonical guide: Raw Markdown: OpenAPI: A Campaign is the project-scoped organizing object for repeatable Audience targeting. It saves an `AudienceInput`, records cycle history, and can bind the brand, offer, Playbook, message-template revision, and dossier specification used by a caller's external GTM pipeline. Blackpearl does not enroll recipients, schedule sequences, or send messages. ## Create and find campaigns Operation `create_campaign_v1_campaigns_post` creates a campaign without starting a run. The request uses the same typed targeting fields as direct Audience creation. ```bash CAMPAIGN=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/campaigns" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "key": "anz-revops-2026q3", "name": "ANZ RevOps leaders", "objective": "Find revenue operations leaders at B2B SaaS companies in Australia and New Zealand.", "target_size": 100, "model": "default" }') CAMPAIGN_ID=$(printf '%s' "$CAMPAIGN" | jq -er '.id') ``` Campaign keys are optional and stable within the project. Repeating a create with the same normalized key and targeting input returns the existing campaign; changed targeting input under that key returns `campaign_key_exists`. Operation `list_campaigns_v1_campaigns_get` lists non-archived campaigns newest first and accepts `limit` plus an opaque `cursor`. Operation `get_campaign_v1_campaigns__campaign_id__get` reads one campaign. ## Bind the GTM graph Operation `update_campaign_v1_campaigns__campaign_id__patch` updates only supplied fields. Use it to change `name`, `key`, `objective`, lifecycle `status`, scalar `metadata`, or these optional relations: `brand_profile_id`, `offer_id`, `playbook_job_id`, `message_template_revision_id`, and `company_dossier_specification_id`. ```bash curl --fail-with-body --silent --show-error \ --request PATCH "$BLACKPEARL_API_BASE_URL/v1/campaigns/$CAMPAIGN_ID" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"status\": \"active\", \"brand_profile_id\": \"$BRAND_ID\", \"offer_id\": \"$OFFER_ID\" }" ``` Omit a relation to preserve it, send an empty string to clear it, or send an ID to rebind it. The complete merged relation set is validated. In particular, an offer must belong to the bound brand. Archived or out-of-scope relations fail closed. Lifecycle transitions are `draft` to `active` or `archived`; `active` to `paused`, `completed`, or `archived`; `paused` to `active`, `completed`, or `archived`; and `completed` to `archived`. Archived is terminal. These states describe your pipeline, never Blackpearl delivery activity. ## Run cycles Operation `run_campaign_cycle_v1_campaigns__campaign_id__cycles_post` starts one asynchronous Audience cycle. Supply `Idempotency-Key` and retain it until the Job ID is stored. Only one queued or running cycle is allowed per campaign. ```bash JOB=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/campaigns/$CAMPAIGN_ID/cycles" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: cycle-20260717-anz-revops") ``` Poll the returned Job as described in [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs). Operation `list_campaign_cycles_v1_campaigns__campaign_id__cycles_get` returns the most recent non-expired cycle Jobs in descending creation order. Each retained Job remains readable for 30 days. Use [Outcomes](https://api.blackpearl.com/docs/outcomes) to record external results against `campaign_id`. A Campaign organizes targeting and attribution; it is not proof of consent and grants no outbound authority. --- # Audiences > Run evidence-backed audience discovery, then materialize immutable review snapshots from real prospects. Canonical guide: Raw Markdown: OpenAPI: Audiences has two complementary workflows. The RTSA workflow runs asynchronous discovery and returns scored candidates. The snapshot workflow freezes selected prospects from a successful Prospecting job so people can review and export a stable list. Neither workflow authorizes or sends outreach. ## Run one audience discovery Operation `create_audience_v1_audiences_post` accepts an `AudienceInput` at `POST /v1/audiences`. The required `objective` describes the companies and buyers to find. `target_size` is 1 through 2,000, `max_contacts_per_company` is 1 through 10, and optional seller, product, ICP, exclusion, and intent context refines the run. ```bash JOB=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/audiences" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: audience-run-20260717-anz-revops" \ --header "Content-Type: application/json" \ --data '{ "objective": "Find revenue operations leaders at B2B SaaS companies in Australia and New Zealand.", "target_size": 100, "max_contacts_per_company": 3, "model": "default" }') JOB_ID=$(printf '%s' "$JOB" | jq -er '.id') ``` The response is an asynchronous Audience job. Poll it using [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs). Each pipeline stage is `pending`, `running`, `completed`, `skipped`, or `failed`. After the Job succeeds, operation `list_audience_candidates_v1_audiences__job_id__candidates_get` pages `GET /v1/audiences/{job_id}/candidates`. Pass each opaque `next_cursor` back unchanged and stop when it is null. Optional `q` filtering is applied before pagination. ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/audiences/$JOB_ID/candidates?limit=50" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` Candidate rows are discriminated by `result_shape`: a `company` row has company scoring plus contacts, while a `contact` row is one scored contact with company context. Results remain available until the job's `expires_at`; save anything needed beyond the 30-day retention window. ## Materialize an immutable snapshot Use a snapshot when a successful, non-synthetic Prospecting result should become a durable review set. Operation `create_audience_v1_audience_snapshots_post` accepts a caller-owned `key`, a display `name`, the source `prospecting_job_id`, and an optional subset of `prospect_ids`. ```bash SNAPSHOT=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/audience-snapshots" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"key\": \"anz-revops-2026q3\", \"name\": \"ANZ RevOps leaders\", \"prospecting_job_id\": \"$PROSPECTING_JOB_ID\" }") AUDIENCE_ID=$(printf '%s' "$SNAPSHOT" | jq -er '.id') ``` The create namespace is project, API-key environment, and `key`. An exact replay returns the original resource with HTTP 200; different normalized input under the same key returns `audience_idempotency_conflict`. The server stores a hash of the normalized source result and copies only validated prospect fields. A later provider or Job change cannot rewrite the snapshot. Operation `list_audiences_v1_audience_snapshots_get` lists snapshots newest first with cursor pagination. Operation `get_audience_v1_audience_snapshots__audience_id__get` reads one snapshot. Resources in another project or `live`/`test` environment resolve as not found. ## Review members Operation `list_audience_members_v1_audience_snapshots__audience_id__members_get` pages the frozen members. Filters include text query, review, qualification, enrichment and outreach states, minimum score, evidence or work-email presence, and exact prospect ID. Cursors are bound to the filter set; restart pagination when a filter changes. Operation `review_audience_members_v1_audience_snapshots__audience_id__reviews_post` records an append-only `included` or `excluded` decision for 1 through 100 member IDs. Generate one `review_key` per logical decision and retain it for retries. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/audience-snapshots/$AUDIENCE_ID/reviews" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"review_key\": \"review-20260717-batch-1\", \"member_ids\": [\"$AUDIENCE_MEMBER_ID\"], \"decision\": \"included\", \"reason_code\": \"verified-fit\" }" ``` Review means curation, not consent or delivery permission. Every snapshot and member returns `outbound_authorized: false`, including included members. ## Export and safety Operation `export_audience_csv_v1_audience_snapshots__audience_id__export_csv_get` exports the current filtered snapshot as CSV. Treat names, emails, profile details, generated drafts, and evidence as personal or untrusted application data. Apply your own lawful-basis, suppression, consent, access-control, retention, and dispatch-time checks outside Blackpearl. Common failures are `not_ready`, `job_expired`, `audiences_result_unavailable`, `prospecting_job_not_found`, `synthetic_prospecting_result`, `audience_idempotency_conflict`, `invalid_cursor`, entitlement, quota, and budget errors. Follow [Errors and safe retries](https://api.blackpearl.com/docs/errors). --- # Playbooks > Configure a Playbooks request and interpret every part of its typed result. Canonical guide: Raw Markdown: OpenAPI: Playbooks creates a tailored sales playbook for one target account. Start with the complete [quickstart](https://api.blackpearl.com/docs/getting-started), then use this guide to choose inputs and render more of the result. ## Playbook templates The catalog is OpenAPI operation `list_playbook_templates_v1_playbook_templates_get`. `playbook_template_key` selects a playbook template: either one of your organization's own templates (created in the console under **Playbook Templates**, or via `POST /api/orgs/{org_id}/playbook-templates`) or a curated catalog entry. `GET /v1/playbook-templates` lists both — your templates first — along with the merged stage vocabulary. An organization template fixes the offer, brand profile, deal stage (your own stage names are accepted — any short lowercase slug), sales approach guidance, and an offer canon that the generation backend treats as human-approved seller facts (seller research is skipped when a canon exists). Unknown keys fail the run with `playbook_template_not_found`. ### The template lifecycle A template is created from a name, an offer/brand binding, and one natural-language **brief** (stage, motion, audience, approach — anything), plus optional uploaded supporting documents. A one-time research job then produces the template's artifact set: 1. **Offer research pack** — markdown documents (canon, competitors, objections, benchmarks, plus scraped sources and your uploads) reused by every run, so the seller is never re-researched per prospect. 2. **Document template** — the Jinja HTML template runs render through. 3. **Payload schema** — a per-template JSON Schema whose field descriptions guide generation. There is no approval step: research (or an artifact edit in the console) finishing is the release. Each org template exposes `template_state`: `ready` and `draft` templates run; `researching` and `research_failed` fail the run with `409 playbook_template_not_ready` (not retryable — wait for research or retry it) only while nothing is live yet. Re-running research over a live template requires an explicit refresh because research is paid work; the live version keeps serving until the new one lands. ## Request inputs Provide at least one of `target_company` or `target_company_domain`. A domain must be a hostname such as `acme.example`, without a scheme or path. | Field | Requirement and allowed values | |---|---| | `target_company` | Optional non-empty company name; required when the domain is omitted. | | `target_company_domain` | Optional valid hostname; required when the name is omitted. | | `anchor_company` | Optional seller name, up to 160 characters. Defaults from the selected brand profile. | | `product_info` | Optional seller/product context, up to 12,000 characters. | | `tone` | `consultative` (default), `direct`, or `technical`. | | `model` | `default`; this is the only current value. | | `intelligence` | Optional model tier for the generation agents: `low`, `medium`, `high`, or `ultra`. Defaults from the selected playbook template, else `high`. Higher tiers cost more and research/write better. | | `sections` | One to ten unique section values. Omit it for the complete result. | | `brand_profile_id` | Optional organization brand-profile ID. | | `brand_profile_key` | Optional stable brand-profile key. If both selectors are present, they must identify the same profile. | Allowed `sections` values are `summary`, `objective`, `value_props`, `sales_angles`, `personas`, `services`, `tech_stack`, `integrations`, `objections`, and `meeting_note`. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/playbooks" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: playbook-20260714-acme" \ --header "Content-Type: application/json" \ --data '{ "target_company_domain": "acme.example", "anchor_company": "Northstar Analytics", "product_info": "Revenue-intelligence software for enterprise sales teams.", "tone": "consultative", "sections": ["summary", "sales_angles", "personas", "objections"], "model": "default" }' ``` This is OpenAPI operation `create_playbook_v1_playbooks_post` with request schema `PlaybookInput` and response schema `PublicPlaybookJobResponse`. Generate a unique `Idempotency-Key` before the first attempt and retain it until the job ID is safely stored. The same key and normalized input replay the original response for 24 hours. Reusing it with different input returns `idempotency_key_reused`. ## Interpret a successful result When `status` is `succeeded`, `result` is a `PublicPlaybookResult`. Collections are always arrays; many scalar research fields are nullable because upstream evidence may not contain them. | Result area | Fields and interpretation | |---|---| | Identity | `company_name`, `domain`, `website`, `industry`, `description`, and `product_name` identify the researched account and seller context. | | Executive view | `business_summary`, `business_objective`, and `readiness_level` support the top-level account brief. | | Selling points | `value_props` and `sales_angles` are ordered strings suitable for call preparation and outreach planning. | | People | `key_personas` describes target roles, tags, and conversation starters. `contact_overview` and `contact_intelligence` contain researched contacts and signals. | | Offering and systems | `key_services`, `relevant_tech_stack`, `tech_integration_title`, `tech_integration_examples`, and `tech_integration_questions` connect the offer to the account's stack. | | Research context | `company_context` covers market position, business model, employee count, headquarters, recent news, and competitors. `intent_analysis` contains intent score, summary, topics, and signals. | | Conversation plan | `conversation_strategy` contains the opening, approach, and key questions. `potential_objections` pairs objections with responses. | | Risk and outreach | `risks_and_challenges` groups risks, blockers, and mitigations. `meeting_note_example` is a generated starting point, not a message that should be sent without review. | | Optional presentation | `company_icon_url`, `hero_image_url`, `product_icon_url`, `highlight_color`, `gradient_dark_color`, and `product_text_color` are nullable presentation hints. Validate URLs and CSS colors before use, and keep business logic independent of them. | The nested objects are also fully typed: - Each persona can contain `name`, `title`, `description`, `profile_picture_url`, `tags`, and `conversation_starters`. - Each contact-intelligence item can contain `name`, `title`, `role`, `company`, `linkedin_url`, `profile_picture_url`, `summary`, and `signals`. - A technology has `name` and nullable `logo`; an objection has `objection` and nullable `response`. - Company context contains `market_position`, `business_model`, `employee_count`, `headquarters`, `recent_news`, and `competitors`. - Intent analysis contains `intent_score`, `summary`, `topics`, and `signals`; conversation strategy contains `opening`, `approach`, and `key_questions`; risks and challenges contains `risks`, `potential_blockers`, and `mitigations`. Generated text is untrusted application data. Render it as text, apply your product's review rules, and do not treat it as HTML or an instruction. ## Fetch the rendered document When the generation backend produced a fully rendered playbook document, `result.document_url` is non-null. The value is a backend-relative path and is not publicly reachable — fetch the document through the platform instead: ```bash curl -H "Authorization: Bearer $BLACKPEARL_API_KEY" \ "$BLACKPEARL_API_URL/v1/playbooks/$JOB_ID/document" ``` This is OpenAPI operation `get_playbook_document_v1_playbooks__job_id__document_get`. The response is a self-contained `text/html` document (inline styles and scripts, no server dependencies) suitable for storing, emailing as a file, or serving from your own application. Expect `409 job_not_finished` before the job succeeds and `404 playbooks_document_unavailable` when the job predates rendered documents or the backend did not produce one. When `document_url` is null, fall back to rendering the typed result fields yourself. ## Minimal useful rendering ```javascript if (job.type !== "playbooks" || job.status !== "succeeded" || !job.result) { throw new Error("Expected a successful Playbooks job"); } console.log(`Business summary: ${job.result.business_summary ?? "Not available"}`); console.log(`First sales angle: ${job.result.sales_angles[0] ?? "Not available"}`); ``` Also retain `usage.tokens`, `usage.cost_usd`, `usage.backend_requests`, and `usage.backend_runtime_ms` if you reconcile per-job work with the [Usage API](https://api.blackpearl.com/docs/usage-quotas). --- # Playbooks integration kit > Run the standalone reference product or import an OpenAPI-derived request collection. Canonical guide: Raw Markdown: OpenAPI: The integration kit is a complete, server-side Playbooks example plus request tooling generated from the same OpenAPI contract as the API reference. It uses only public HTTP behavior and published types; it has no dependency on gateway or console internals. ## Standalone reference product The [reference app source](https://api.blackpearl.com/docs/integration-kit/reference-app/src/index.ts), [package file](https://api.blackpearl.com/docs/integration-kit/reference-app/package.json), [lockfile](https://api.blackpearl.com/docs/integration-kit/reference-app/package-lock.json), and [TypeScript configuration](https://api.blackpearl.com/docs/integration-kit/reference-app/tsconfig.json) form the complete sample. Download those files into the same layout, then run: ```bash npm ci export BLACKPEARL_API_BASE_URL="https://api.blackpearl.com" export BLACKPEARL_API_KEY="replace-with-your-development-project-key" npm run serve ``` Open `http://127.0.0.1:8787` after the command reports success. The browser receives rendered result text, never the project key. The app persists its exact request, idempotency key, job ID, latest typed job, and successful result in `.blackpearl-reference/state.json`. Restarting resumes an active job or renders the stored result without creating new work. Use `npm run reset` only when you intentionally want a new job. The implementation demonstrates: - Bearer authentication read only from the server environment. - An idempotency key persisted before the first POST and reused with identical input after an ambiguous transport failure. - A job ID persisted before bounded polling, with restart-safe resume. - Two-second initial polling and exponential backoff with jitter capped at ten seconds. - Separate handling for transient `rate_limit_exceeded`, non-retryable `quota_exceeded` and `budget_exceeded`, authentication errors, failed/canceled jobs, and `410 job_expired`. - Runtime validation before rendering `business_summary`, `sales_angles`, and `value_props`; generated text is HTML-escaped. The same `run` command targets any gateway whose Playbooks backend is configured, including an isolated local service or a hosted development project. Calls perform real capability work and record real usage; an unconfigured backend fails closed. ## Postman collection Download the [Blackpearl Public API Postman collection](https://api.blackpearl.com/docs/integration-kit/blackpearl-public-api.postman_collection.json), import it, and set its `baseUrl` and `apiKey` collection variables. Set `idempotencyKey` to a unique retained value before a job-creation request. If a create response is ambiguous, resend the unchanged request with that same value. The collection contains every current public OpenAPI operation, HTTP method, path/query/header parameter, JSON request example, and documented success status. Its metadata records the exact OpenAPI document version and SHA-256 digest. `make integration-kit-check` regenerates it in memory and fails on contract drift. ## Client support policy The [Python and TypeScript quickstarts](https://api.blackpearl.com/docs/getting-started#python-and-typescript-versions) and this product are deliberately dependency-light reference clients, not supported SDK packages. Broad official SDK generation remains deferred until API stability and adoption justify the compatibility and release burden. Their automated tests use explicit test-only HTTP fixtures; the shipped clients contain no simulation path. --- # Documents > Turn a brief, Brand Pack, and optional Offer into downloadable PPTX, DOCX, or PDF files. Canonical guide: Raw Markdown: OpenAPI: Documents is a GA generation capability for finished sales collateral. It runs the Doc Maker managed agent asynchronously and stores the resulting files with the Job. It does not create account dossiers: the separate Company Dossier Specifications operations remain Preview planning contracts. ## Create a document Job Operation `create_document_job_v1_documents_post` accepts `POST /v1/documents`. `brief` is required and describes the artifact, audience, and content. The other inputs constrain or enrich the run: | Field | Behavior | |---|---| | `document_type` | Optional `proposal`, `playbook`, `deck`, `gtm_strategy`, `report`, or `one_pager`. The agent chooses when omitted. | | `format` | Optional `pptx`, `docx`, or `pdf`. The agent chooses when omitted. | | `target_domain` | Optional focus-account domain. Schemes, `www.`, and paths are normalized to a bare domain. | | `brand` | Optional Brand profile ID or key. The organization's default Brand is used when omitted. | | `offer` | Optional Offer ID or key whose value propositions, proof points, pricing, ICP, and stage fit are attached. | | `template_urls` | Up to three HTTP(S) reference documents whose structure, tone, and layout should be followed. | | `title` | Optional title retained with the Job and result. | | `external_ref` | Optional caller correlation value echoed in the result. | The selected Brand must have a completed guideline so the platform can freeze its Brand Pack into the run. An attached Offer must be active and belong to the same organization. ```bash JOB=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/documents" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: document-20260720-acme-proposal" \ --header "Content-Type: application/json" \ --data '{ "brief": "Create a first-meeting proposal for Acme RevOps. Lead with pipeline visibility, explain our 90-day rollout, and close with pricing.", "document_type": "proposal", "format": "docx", "target_domain": "acme.example", "brand": "northstar", "offer": "analytics_suite", "template_urls": ["https://cdn.example/templates/enterprise-proposal.docx"], "title": "Acme RevOps proposal", "external_ref": "crm-account-123" }') JOB_ID=$(printf '%s' "$JOB" | jq -er '.id') ``` The endpoint returns `202 Accepted` with a typed `DocumentJobResponse`. Generate and persist the `Idempotency-Key` before the first attempt. Matching retries within 24 hours replay the original response; using the key with changed input returns `idempotency_key_reused`. ## Poll and interpret the result Poll `GET /v1/jobs/{job_id}` using the bounded flow in [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs). The `documents` Job variant retains the normalized input, three execution stages, usage, and a typed result after success. The result contains: - `files`: stored file references with `file_id`, `filename`, `format`, `size_bytes`, and an informational Console download path; - `brand` and optional `offer`: the exact selling context resolved for the run; - `progress`: up to 50 curated agent milestones; - `model`, `duration_seconds`, `request_echo`, `title`, `target_domain`, and `external_ref` for traceability. Real runs ledger the managed session's authoritative token usage and cost in the Job `usage` object. A succeeded Job can contain more than one file. Treat generated claims and downloaded content as reviewable application data, not publishing or outreach authority. If the managed Doc Maker integration is not configured, the Job fails closed with a configuration error and produces no synthetic file. ## Download files Operation `download_document_job_file_v1_documents__job_id__files__file_id__get` returns `GET /v1/documents/{job_id}/files/{file_id}`. Use the IDs from the successful Job result: ```bash COMPLETED_JOB=$(curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/jobs/$JOB_ID" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY") FILE_ID=$(printf '%s' "$COMPLETED_JOB" | jq -er '.result.files[0].file_id') curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/documents/$JOB_ID/files/$FILE_ID" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --output acme-proposal.docx ``` The response is the raw file with a format-specific `Content-Type` and an attachment filename. Unknown formats use `application/octet-stream`. Files share the Job's 30-day retention window and are visible only to the owning project. Common create failures are `capability_disabled`, `insufficient_credits`, `quota_exceeded`, `budget_exceeded`, `brand_not_found`, `brand_guideline_missing`, `offer_not_found`, and `offer_archived`. A missing or cross-project Job or file returns `not_found`. Follow [Errors and retry decisions](https://api.blackpearl.com/docs/errors) and never repeat an ambiguous create request without its original idempotency key. ## Preview dossier planning contracts Company Dossier Specifications remain Preview and contract-only. They freeze a target company, connector revision, requested sections, collection budgets, purpose, and policy version; they perform no provider request, research, model call, or rendering. Preview operation `create_company_dossier_specification_v1_company_dossier_specifications_post` creates an immutable plan. Preview operation `list_company_dossier_specifications_v1_company_dossier_specifications_get` lists plans with cursor pagination. Preview operation `get_company_dossier_specification_v1_company_dossier_specifications__specification_id__get` reads one plan. Do not interpret a stored dossier specification as collected evidence, generated collateral, permission to crawl a site, or authorization to publish. --- # Message Templates > Turn approved GTM context and prospect evidence into quality-gated, approval-only outreach copy. Canonical guide: Raw Markdown: OpenAPI: Message Templates creates reusable structural email revisions and personalized prospect drafts. It is deliberately approval-only: every response reports zero messages enqueued and sent, and the capability has no provider-delivery, tracking, attachment, or CRM-write authority. ## Generate a template revision `generate_message_template_revision_v1_message_template_revisions_generate_post` starts `POST /v1/message-template-revisions/generate`. Send an `Idempotency-Key`, a focused audience and objective, an approved offer or offer summary, and the desired next step. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/message-template-revisions/generate" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: revops-template-20260717" \ --header "Content-Type: application/json" \ --data '{ "template_key": "revops-first-touch", "name": "RevOps first touch", "language": "en-nz", "audience": "Revenue operations leaders at B2B software companies", "objective": "Start a relevant discovery conversation", "offer_summary": "Reduce manual forecasting work and surface account risk earlier", "desired_next_step": "a short comparison", "options": { "tone": "direct", "locale": "New Zealand English", "max_words": 90, "max_links": 0, "forbidden_claims": ["guaranteed outcomes"] }, "reason": "Create the governed first-touch baseline for review." }' ``` The response is a typed asynchronous job with `type: "message_templates"`. Poll it as described in the [Jobs guide](https://api.blackpearl.com/docs/jobs). A successful result has `status: "DRAFT_READY"`, the next immutable revision, and a 100-point rubric covering relevance, evidence, clarity, recipient value, CTA, trust, and mobile readability. The server converts only its allowlisted placeholders into the closed template structure and saves nothing below the quality threshold. You can also create an already-reviewed closed structure through `create_message_template_revision_v1_message_template_revisions_post`, list revisions with `list_message_template_revisions_v1_message_template_revisions_get`, inspect one with `get_message_template_revision_v1_message_template_revisions__revision_id__get`, and render exact escaped text/HTML through `render_message_template_revision_v1_message_template_revisions__revision_id__render_post`. ## Personalize a retained prospect `generate_prospect_message_v1_message_template_revisions__revision_id__prospect_message_post` starts `POST /v1/message-template-revisions/{revision_id}/prospect-message`. Select one prospect from an unexpired succeeded Prospecting job and provide sender/reply/postal identity plus the exact retained evidence and policy references. The caller cannot declare policy approval or suppression state. For real-recipient copy, the platform requires the exact policy reference and version in its operator-controlled allow-list, verifies basis and address-provenance IDs against the prospect's retained qualification/enrichment evidence, derives the address-source class from that evidence, and checks the project/environment Outcome ledger for an unsubscribe. It returns those facts in `compliance_verification`. Identity, evidence, sender, offer, next-step, company/jurisdiction, and privacy-notice gates still fail closed. The result status is: - `DRAFT_READY` when every real-recipient gate and quality check passes. - `NEEDS_INPUT` when required context is incomplete. - `BLOCKED` for a platform suppression match, rejected qualification, or non-demo synthetic use. - `DEMONSTRATION_ONLY` for any explicitly requested preview; synthetic data always requires it. Personalization makes no OpenAI request. `NEEDS_INPUT` and `BLOCKED` return no subject or body. A passing request maps governed retained values to the selected revision's declared variables and uses the deterministic renderer, producing exactly one selected-revision subject. The server validates the complete returned subject/body for evidence and approved-value anchors, recipient relevance, numeric claims, word and link limits, one CTA, mobile structure, prohibited language, and the canonical final opt-out notice. Missing or unsupported required variables return `NEEDS_INPUT`. ## Usage, cost, and safe downstream handling Each template-generation OpenAI Responses call is captured in a durable billing outbox. The job becomes successful only after all cost rows, customer debit, usage, result, and immutable revision are durably settled; scheduler retries do not repeat the provider call. Job usage and the Costs dashboard report input, cached-input, cache-write, output, reasoning, and total tokens plus true USD cost. Personalization has zero provider usage. See [Usage, quotas, credits, and budgets](https://api.blackpearl.com/docs/usage-quotas). A `DRAFT_READY` message is still only a human-review artifact. Store its template and approval hashes with any downstream workflow, revalidate consent and suppression at that system's effect boundary, and never infer delivery authority from generation success. The initial POST returns `202`; asynchronous failure appears when polling as `status: "failed"` with `error: {"code": "...", "message": "..."}`. Branch on the stable code and follow the [error and retry guide](https://api.blackpearl.com/docs/errors) for idempotent retries and budget, quota, or provider failures. --- # Prospecting > Run turbo or agentic prospect discovery and poll the append-only live event thread. Canonical guide: Raw Markdown: OpenAPI: Prospecting discovers and qualifies real people against a typed targeting brief. It returns an asynchronous Job with evidence, qualification, enrichment, and draft-only outreach state. Provider credentials never belong in the public request. ## Start a run Operation `create_prospecting_job_v1_prospects_post` accepts `POST /v1/prospects`. Required fields are `campaign_name`, `product_info`, and `objective`. The optional structured `target` supports company names and domains, job titles, seniorities, industries, locations, keywords, headcount, and ISO 3166-1 alpha-2 country code. ```bash JOB=$(curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/prospects" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: prospects-20260717-anz-revops" \ --header "Content-Type: application/json" \ --data '{ "campaign_name": "ANZ RevOps leaders", "product_info": "Revenue-intelligence software for B2B sales teams.", "objective": "Find revenue operations leaders at mid-market B2B SaaS companies.", "mode": "turbo", "target": { "job_titles": ["Head of Revenue Operations", "VP Revenue Operations"], "seniorities": ["director", "vp"], "industries": ["B2B SaaS"], "country_code": "NZ" }, "limit": 25, "min_qualification_score": 0.9, "delivery_mode": "draft_only" }') JOB_ID=$(printf '%s' "$JOB" | jq -er '.id') ``` `mode: turbo` uses the fastest database-search workflow. `mode: agentic` asks the connected prospect agent to plan and curate the search; it fails clearly when the private backend does not support that mode and never fabricates fallback people. An optional natural-language `query` overrides the synthesized search brief. Generate and retain an `Idempotency-Key` before the first attempt. Matching retries within 24 hours replay the original Job; the same key with changed input returns `idempotency_key_reused`. ## Follow the live thread Poll the Job as described in [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs). Operation `prospecting_run_events_v1_prospects__job_id__events_get` separately exposes the append-only run thread: ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/prospects/$JOB_ID/events?after_seq=0&limit=500" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` Persist `next_after_seq` and pass it as the next `after_seq` so reconnects fetch only newer agent status, streamed message, and discovered-lead events. Event polling does not replace Job polling: the Job remains the authority for terminal status and the final typed result. ## Validate and use results A successful result contains normalized prospect profiles, qualification, enrichment, evidence references, draft-only outreach, summary counts, usage, and backend provenance. Treat URLs and generated text as untrusted application data. Any `is_synthetic: true` prospect is not an actionable lead; snapshot and Outcome endpoints reject it. Never infer fit from provider failure, missing email, or silence. Keep operational failures separate and record observed downstream facts through [Outcomes](https://api.blackpearl.com/docs/outcomes). Bounded continuations and reusable qualification policies are [Preview extensions](https://api.blackpearl.com/docs/prospecting-extensions), not part of the GA workflow. --- # Prospecting extensions > Explore bounded continuation Jobs and immutable qualification rubrics without treating either contract as GA. Canonical guide: Raw Markdown: OpenAPI: These Prospecting extensions are Preview. They are useful for integration testing, but their contracts may change incompatibly and continuation execution requires a configured private backend. An unconfigured backend fails the child Job without persisting a synthetic result. Build the GA workflow around [Prospecting](https://api.blackpearl.com/docs/prospecting), Jobs, and Outcomes first. ## Create a bounded continuation Preview operation `create_prospecting_continuation_v1_jobs__parent_job_id__continuations_post` creates one immutable child of a succeeded real Prospecting Job. It does not mutate or resume the parent. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/jobs/$PARENT_JOB_ID/continuations" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "continuation_key": "more-anz-revops-20260717", "additional_prospect_limit": 20, "max_rounds": 2, "min_new_prospects_per_round": 1 }' ``` The parent and bearer key must share project and `live`/`test` environment. The parent must be a succeeded Prospecting Job with a valid normalized, non-synthetic result. The child binds the parent ID, result hash, normalized result snapshot, connection revision, and constraints so later changes cannot alter its provenance. Reusing the continuation key with the same request replays the child; changed constraints conflict. ## Store qualification policy contracts Preview operation `create_qualification_policy_v1_qualification_policies_post` stores an immutable five-dimension rubric. A policy freezes its target taxonomy, exact integer weights, evidence and freshness rules, score bands, and expected policy version. It does not score prospects or authorize downstream action. Preview operation `list_qualification_policies_v1_qualification_policies_get` lists policies in the current project and environment with cursor pagination. Preview operation `get_qualification_policy_v1_qualification_policies__qualification_policy_id__get` reads one policy. Use the returned policy hash when a future executor requires optimistic binding; never infer that creating a policy means a scorer is available. Preview resources should remain isolated behind a feature flag and observability boundary. Do not persist assumptions about their schema into irreversible workflows. --- # Prospect Research > Retrieve structured company intelligence, analyze an unindexed website, and optionally add lookalikes. Canonical guide: Raw Markdown: OpenAPI: Account research is a GA synchronous lookup. It returns one typed website-intelligence record for a normalized company domain. The Prospect Research Specifications operations on this page remain separate Preview planning contracts and do not execute research. ## Retrieve an account record Operation `public_account_research_v1_account_research_post` accepts `POST /v1/account-research`: ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/account-research" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "domain": "hubspot.com", "analyze": true, "include_similar": true }' | jq . ``` `domain` is required. Schemes, `www.`, paths, queries, fragments, and a trailing dot are removed before lookup. Invalid hostnames return `invalid_domain` without recording usage. By default, an unindexed domain returns `404 account_not_found` and consumes no request unit. Set `analyze: true` to invoke the provider's on-miss website analysis, then retrieve the newly indexed record. A successful on-demand analysis consumes one additional `prospect_research` request and sets `analyzed: true`. A website that cannot be fetched returns `account_unanalyzable`; upstream analysis quota exhaustion returns `account_research_backend_quota`. Set `include_similar: true` to add up to ten `similar_companies` with normalized identity details and a similarity score from 0 to 1. Lookalikes are best-effort enrichment: provider failure produces an empty list and does not fail the account lookup. The requested domain is removed from its own lookalikes. ## Interpret and meter the result The response groups fields by purpose: | Area | Contents | |---|---| | `identity` | Name, domain, URL, summary, history, industry, structure, founders, values, and descriptive language. | | `scale` | Employee and office counts, stated users, revenue, TAM, and primary location. | | `market` | B2B/B2C posture, target customers, products, services, customers, competitors, and testimonial presence. | | `digital` | Important site URLs, social links, integrations, detected technology, languages, support, API, trial, app, content, commerce, and subscription signals. | | `signals` | AI, compliance, awards, CSR, inclusion, environmental, geographic, IP, IPO, disruption, and hiring signals. | | `contacts` | Up to ten provider-surfaced names, titles, business emails, and professional profile URLs. Fields may be null. | | `similar_companies` | Optional lookalikes requested with `include_similar`. | Also retain `domain`, `retrieved_at`, `schema_version`, `analyzed`, and `provenance`. Successful responses come from the configured research backend; an unconfigured integration fails closed instead of returning synthetic company evidence. Every successful lookup consumes one `prospect_research` request. An on-miss analysis consumes one additional request before the successful lookup. Validation, entitlement, quota, and not-found failures do not consume a successful lookup unit. Similar-company enrichment does not add a unit. Provider-derived facts can be incomplete, stale, or incorrect. Preserve provenance and retrieval time, render values as untrusted data, and review material claims before using them in generated collateral or decisions about people. ## Errors and retry behavior `capability_disabled`, `invalid_domain`, `account_not_found`, and `account_unanalyzable` require a caller or source change. `quota_exceeded` and `account_research_backend_quota` require a reset or capacity change. `account_research_not_configured` and `account_research_unauthorized` require an operator configuration fix. Retry `account_research_unavailable` or `account_research_backend_error` only with bounded backoff. See [Errors and retry decisions](https://api.blackpearl.com/docs/errors). ## Preview research-planning contracts Prospect Research Specifications are Preview, contract-only plans. They bind reviewed Audience members to closed business-research lenses, evidence policy, and hard collection budgets. Creating one performs no web search, provider request, model call, enrichment, scoring, or generation. Preview operation `create_prospect_research_specification_v1_prospect_research_specifications_post` creates an immutable plan. Preview operation `list_prospect_research_specifications_v1_prospect_research_specifications_get` lists plans with cursor pagination. Preview operation `get_prospect_research_specification_v1_prospect_research_specifications__specification_id__get` reads one plan. Do not interpret a specification as researched evidence or use it to make claims about a person. Any future person-level executor must retain source attribution, freshness, confidence, lawful purpose, and review controls. --- # Professional Network > Bind reviewed Audience members to an official Sales Navigator connection contract without scraping or execution. Canonical guide: Raw Markdown: OpenAPI: Professional Network Specifications are Preview, contract-only plans for future official-API access. They bind reviewed Audience members, an approved connector revision, display fields, purpose, and policy version. They prohibit scraping, copied cookies, password automation, private endpoints, messaging, and profile mutation. Preview operation `create_professional_network_specification_v1_professional_network_specifications_post` requires a caller key, Audience ID and exact result hash, member selection, connection binding, allow-listed display components, purpose, expected policy version, and reason. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/professional-network-specifications" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data @professional-network-specification.json ``` Preview operation `list_professional_network_specifications_v1_professional_network_specifications_get` lists specifications in the current project and environment. Preview operation `get_professional_network_specification_v1_professional_network_specifications__specification_id__get` reads one. Responses report provider execution unavailable and zero access counters. A stored connector or specification does not establish LinkedIn approval, product tier, member consent, organization role, or permission to reuse data. --- # Account Coordination > Derive an immutable account-level preflight from a reviewed Audience and its Outcome evidence. Canonical guide: Raw Markdown: OpenAPI: Account Coordination is a Preview, deterministic assessment over existing Blackpearl data. It detects risks such as contacting too many people at one account, stale evidence, restrictive feedback, and retry timing. It does not generate copy, assign a sender, create a task, mutate a CRM, or authorize contact. Preview operation `create_account_coordination_assessment_v1_account_coordination_assessments_post` binds a caller key, `audience_id`, exact Audience result hash, company domain, expected policy version, and audit reason. The Audience must belong to the current project and API-key environment. Its immutable member snapshot and relevant append-only Outcomes are evaluated atomically. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/account-coordination-assessments" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"key\": \"acme-preflight-20260717\", \"audience_id\": \"$AUDIENCE_ID\", \"expected_audience_result_hash\": \"$AUDIENCE_RESULT_HASH\", \"company_domain\": \"acme.example\", \"expected_policy_version\": \"2026-07-15\", \"reason\": \"Preflight before external campaign review\" }" ``` An exact key/request replay returns the existing assessment; a changed request under the same key conflicts. The result preserves evidence references, derived flags, decision, and any `retry_after` bound to the assessment policy version. Preview operation `get_account_coordination_assessment_v1_account_coordination_assessments__assessment_id__get` reads one immutable assessment. Treat the decision as a preflight signal for a separately governed delivery system, never as consent or send authority. --- # Enrichment > Bind reviewed Audience members to a closed enrichment field plan without calling a provider. Canonical guide: Raw Markdown: OpenAPI: Enrichment Specifications are Preview, contract-only plans over exact members of an immutable Audience snapshot. They are the single planning surface for contact and company completion, including business-email discovery through `work_email` fields. No current public operation executes enrichment. Preview operation `create_enrichment_specification_v1_enrichment_specifications_post` requires a caller key, `audience_id`, expected Audience result hash, bounded member selection, requested field contracts, expected policy version, and reason. It fails closed if the snapshot, member IDs, hash, environment, or policy version do not match. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/enrichment-specifications" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data @enrichment-specification.json ``` Preview operation `list_enrichment_specifications_v1_enrichment_specifications_get` lists project/environment specifications with cursor pagination. Preview operation `get_enrichment_specification_v1_enrichment_specifications__specification_id__get` retrieves one immutable record. Every record reports provider execution unavailable and zero processed or enriched rows. Creating a plan does not discover an email, verify deliverability, grant consent, export data, update a CRM, or authorize outbound contact. --- # Data Intake > Store immutable source, selection, field, deduplication, purpose, and retention contracts without executing intake. Canonical guide: Raw Markdown: OpenAPI: Data Intake Specifications are Preview planning records. They freeze an allow-listed source and exact version, bounded selection, requested fields, deduplication policy, declared purpose, and retention before a source executor exists. They do not upload, download, process, export, or sync records. Preview operation `create_data_intake_specification_v1_data_intake_specifications_post` creates an immutable, project- and environment-scoped specification. The request requires a caller key, source, selection, fields, deduplication rules, purpose, `retention_days`, expected policy version, and auditable reason. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/data-intake-specifications" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data @data-intake-specification.json ``` Preview operation `list_data_intake_specifications_v1_data_intake_specifications_get` lists specifications with opaque cursor pagination. Preview operation `get_data_intake_specification_v1_data_intake_specifications__specification_id__get` reads one. Responses explicitly report execution unavailable and zero I/O or processing counters. A valid specification does not establish source rights, data-subject consent, data quality, or the availability of a connector. Keep Preview resources out of production ingestion paths. --- # Site Intelligence > Store a bounded HTTPS-origin and check-profile contract without performing network or evaluation work. Canonical guide: Raw Markdown: OpenAPI: Site Audit Specifications are Preview, contract-only Site Intelligence resources. They freeze an authorized HTTPS target and allow-listed check profile for a future readiness audit. The gateway does not resolve DNS, fetch pages, execute scripts, scan infrastructure, or evaluate the site. Preview operation `create_site_audit_specification_v1_site_audit_specifications_post` requires a caller key, target, check profile, expected policy version, and auditable reason. Target validation rejects credentials, fragments, unsupported schemes, unsafe addressing, and scope outside the declared origin. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/site-audit-specifications" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data @site-audit-specification.json ``` Preview operation `list_site_audit_specifications_v1_site_audit_specifications_get` lists project/environment specifications with cursor pagination. Preview operation `get_site_audit_specification_v1_site_audit_specifications__specification_id__get` reads one. Every response states that execution is unavailable and reports zero fetch, evaluation, and finding counts. A stored plan does not prove site ownership or permission; callers remain responsible for explicit authorization and a future executor must revalidate the target at execution time. --- # Feedback and outcomes > Append external prospect, account, or campaign results and aggregate them across the GTM graph. Canonical guide: Raw Markdown: OpenAPI: Outcomes is the append-only feedback ledger. Your delivery, CRM, or calendar tooling records what happened; Blackpearl validates its attribution and returns deterministic aggregates. Recording an Outcome does not verify the assertion, send a message, update a CRM, or train a model. ## Record an outcome Operation `create_outcome_v1_outcomes_post` accepts exactly one logical event at `POST /v1/outcomes`. ```bash curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/outcomes" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Content-Type: application/json" \ --data "{ \"source\": \"manual\", \"external_event_id\": \"meeting-20260717-018f6f\", \"type\": \"meeting_booked\", \"occurred_at\": \"2026-07-17T02:15:00Z\", \"prospecting_job_id\": \"$PROSPECTING_JOB_ID\", \"prospect_id\": \"$PROSPECT_ID\", \"campaign_id\": \"$CAMPAIGN_ID\", \"offer_id\": \"$OFFER_ID\", \"channel\": \"email\" }" ``` At least one subject is required: | Subject | Required fields | Boundary | |---|---|---| | Prospect | `prospecting_job_id` and `prospect_id` together | The Job must be a succeeded, non-synthetic Prospecting run and the prospect a real row in its result. | | Account | `account_domain` | A valid bare domain, normalized to lowercase. | | Campaign | `campaign_id` | A Campaign in the API key's project. | Optional `campaign_id`, `offer_id`, `brand_profile_id`, `audience_id`, `playbook_job_id`, and `message_template_revision_id` references are validated before insertion. A prospect subject can also carry these attribution fields. For manual events, omit `connector_id`. A non-manual `source` requires a project Connection whose provider key matches the source. Connections are currently Preview; see [Connector inventory](https://api.blackpearl.com/docs/connectors) before depending on provider ingestion. ## Taxonomy and learning safety Preference types are `qualification_approved`, `qualification_rejected`, `draft_approved`, and `draft_rejected`. Commercial types are `positive_reply`, `negative_reply`, `meeting_booked`, `sales_qualified`, `opportunity_created`, `closed_won`, `closed_lost`, and `ghosted`. Operational types are `delivered`, `bounced`, `unsubscribed`, `email_not_found`, and `provider_failed`. The server derives `classification` and whether the type is a possible `learning_candidate`. Every currently accepted row is still `verification_status: unverified` and `eligible_for_learning: false`. Operational events and `ghosted` are never learning candidates; provider availability and silence must not be mistaken for poor fit. `amount_usd_cents` is a strict integer accepted only for `closed_won`. Raw webhook bodies, credentials, prompts, message content, and arbitrary metadata are rejected. ## Idempotency and listing The idempotency namespace combines project, API-key environment, source connection or manual namespace, and `external_event_id`. An exact retry returns HTTP 200 with `idempotent_replay: true`; different normalized data under the same namespace returns `outcome_idempotency_conflict`. Operation `list_outcomes_v1_outcomes_get` lists rows newest first. Filter by type, classification, subject kind, prospecting Job, prospect, account domain, Campaign, or Offer. Use `next_cursor` unchanged and restart pagination when filters change. ## Aggregate insights Operation `outcome_insights_v1_outcomes_insights_get` groups the ledger by `campaign`, `offer`, `brand_profile`, `audience`, `playbook_job`, `message_template_revision`, `type`, `channel`, or `account_domain`. Optional `since` is inclusive and `until` is exclusive. ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/outcomes/insights?group_by=campaign&campaign_id=$CAMPAIGN_ID" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` Aggregates report counts, classification distribution, positive replies, meetings, wins, losses, win rate, closed-won revenue, and learning-candidate count. They summarize unverified submitted facts; they do not make them verified. --- # Authentication and project environments > Protect project-scoped Bearer keys and isolate development, staging, and production work. Canonical guide: Raw Markdown: OpenAPI: ## Authenticate public API calls Every `/v1` operation requires a project-scoped API key as an HTTP Bearer credential: ```bash curl "$BLACKPEARL_API_BASE_URL/v1/capabilities" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" ``` The API's OpenAPI document defines one global HTTP Bearer security scheme. Do not send the key as a query parameter or JSON field. ## Projects are environments A project is the isolation boundary for keys, ownership, usage attribution, quotas, and budgets. Create separate development, staging, and production projects when those workloads must not affect each other. There is one supported key type. A prefix does not change execution behavior: every hosted request performs real capability work and records real usage. Automated tests should inject explicit test doubles at integration boundaries or target an isolated gateway configured with test services. ## Create, store, and rotate keys - Create a key from **API keys** in the console and copy it immediately; the secret is shown once and stored hashed. - Store the value in a server-side secrets manager and expose it to the process as `BLACKPEARL_API_KEY`. - Never commit a key, log it, paste it into support tickets, or embed it in browser or mobile code. - Rotate without downtime by creating and deploying a replacement before revoking the old key. - A revoked or expired key stops authenticating immediately. ## Authentication failures | Code | Meaning | Action | |---|---|---| | `missing_api_key` | The Bearer header is absent. | Add `Authorization: Bearer ...`. | | `invalid_api_key` | The credential is malformed, unknown, or revoked. | Load an active key from the correct secret. | | `expired_api_key` | The key passed its configured expiry. | Rotate to a new active key. | | `project_archived` | The credential belongs to an archived project. | Use a key from an active project. | These are configuration failures, not transient failures. Do not retry them without changing the credential or project. See the complete [error and retry matrix](https://api.blackpearl.com/docs/errors) and discover enabled features with [Capabilities and entitlements](https://api.blackpearl.com/docs/feature-flags). The capabilities request uses OpenAPI operation `capabilities_v1_capabilities_get` and schema `PublicCapabilitiesResponse`. --- # Jobs and resumable polling > Persist asynchronous job IDs, poll with bounded backoff, and stop on every terminal status. Canonical guide: Raw Markdown: OpenAPI: Playbooks, Audiences, Brand research, Prospecting, and Documents return asynchronous jobs. The shared `type` field selects the typed `input` and `result`: `playbooks` uses `PublicPlaybookJobResponse`, `audiences` uses `PublicAudienceJobResponse`, `brands` uses `PublicBrandJobResponse`, `prospecting` uses `ProspectingJobResponse`, and `documents` uses `DocumentJobResponse`. ## Lifecycle | Status | Terminal | Client behavior | |---|---:|---| | `queued` | No | Wait before polling again. | | `running` | No | Report `progress` if useful and keep polling. | | `succeeded` | Yes | Validate and render the typed `result`. | | `failed` | Yes | Stop and handle `error_code` when present, then surface the job's `error` message. | | `canceled` | Yes | Stop; there is no public cancel operation in the initial contract. | Do not wait only for `succeeded`, because that creates infinite loops after failures or cancellation. ## Resumable polling algorithm 1. After a successful create response, persist `id` before the first poll. 2. Poll `GET /v1/jobs/{job_id}` after 2 seconds. 3. Back off exponentially with jitter, capped at 10 seconds. 4. Set a local deadline appropriate to the caller. A deadline stops the client, not server work. 5. Keep the ID when the deadline expires and resume it later rather than creating duplicate work. 6. Stop on `succeeded`, `failed`, or `canceled`. Before the create request, persist an `Idempotency-Key`. If the connection fails before the response arrives, repeat the exact job-creation operation and input with that key during its 24-hour replay window. This recovers the original response instead of creating duplicate work. The [Quickstart](https://api.blackpearl.com/docs/getting-started) includes a complete cURL implementation. Tested Python and TypeScript implementations under `docs/examples` add jitter and use the same terminal-state rules. ## Ownership and billing A job is visible only to a key for its owning project. A missing or cross-project ID returns `not_found`. Creating a job records accepted work and its eventual capability usage. Poll requests are subject to request rate limits but are not billed as new generation requests. Job creation requires a positive spendable credit balance, but does not reserve or estimate the run's price. Settlement uses the actual provider cost after execution. If that amount exceeds the remaining balance, the platform consumes the balance without taking it negative, triggers configured auto recharge, and terminates the job as `failed` with `error_code` `insufficient_credits`; `result` remains null. `progress` is coarse and is not a completion-time promise. Do not build an SLA from it. Every public job includes `expires_at`, exactly 30 days after `created_at`. Job metadata, input, and results remain readable through that timestamp. Store any result your application needs longer. After `expires_at`, direct job and Audience candidate reads return `410` with `error.code` `job_expired`. Cleanup removes retained input, result, failure detail, stages, and private diagnostics, but preserves the minimal ownership tombstone plus usage, cost, credit, and audit ledgers. Expiry does not cancel work, reverse billed usage, or erase accounting history. ## Poll errors Safe GET requests may be retried for `rate_limit_exceeded` after `Retry-After` and for documented transient `5xx` failures with bounded exponential backoff. Fix authentication, entitlement, quota, and budget errors before retrying. See [Errors](https://api.blackpearl.com/docs/errors). Job retrieval is OpenAPI operation `get_job_v1_jobs__job_id__get`. Its response is the same `type`-discriminated union of Playbooks, Audiences, Brands, Prospecting, and Documents Job schemas. Documents results contain file references; download their bytes through the authenticated endpoint documented in [Documents](https://api.blackpearl.com/docs/documents) before the Job expires. --- # Errors and retry decisions > Branch on stable error codes and retry only operations that are safe and transient. Canonical guide: Raw Markdown: OpenAPI: 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. --- # Rate limits > Recover from transient request-rate limits without confusing them with quota or budget exhaustion. Canonical guide: Raw Markdown: OpenAPI: Rate limits control request pace for one project API key. They are separate from monthly usage quotas, hard cost budgets, credits, and billed capability work. Every request that successfully authenticates to `/v1` consumes one place in that key's rolling 60-second window. This includes reads, polling, validation failures, idempotent replays, and calls to Usage. Missing, invalid, revoked, and expired credentials do not enter a key's window because no key can be identified. The deployment default is 120 authenticated requests per 60 seconds per API key. A deployment can configure another limit, so use the policy and live window values returned by the API: | Response value | Meaning | |---|---| | `RateLimit-Limit` | Requests allowed in the current 60-second window. | | `RateLimit-Remaining` | Requests still available after the current request. | | `RateLimit-Reset` | Whole seconds until the oldest request leaves the current window. | | `Retry-After` | On `rate_limit_exceeded` only, the minimum whole seconds to wait before retrying. | The three `RateLimit-*` headers appear on authenticated success and error responses. `GET /v1/usage` also returns `rate_limit.limit` and `rate_limit.window_seconds` for policy discovery. The per-minute limit is set by the organization's **usage tier**, which grows automatically with cumulative paid usage and account age. `GET /v1/usage` returns the current tier as `usage_tier` (`key`, `name`, `rank`, `rate_limit_per_minute`, `monthly_cost_cap_usd`). Each tier also carries a monthly spend cap: when it is reached, job creation fails `429 budget_exceeded` with `error.details.scope` of `"usage_tier"` — like budget exhaustion, this is not transient and resets at `error.details.resets_at`. When request pace is too high, the API returns `429` with `error.code` set to `rate_limit_exceeded`. Wait at least the larger of `Retry-After` and `RateLimit-Reset`, then retry with bounded exponential backoff and jitter. Polling consumes request-rate capacity but does not create or bill another generation run. `quota_exceeded` and `budget_exceeded` also return `429`, but neither is a transient request-rate failure and neither carries `Retry-After`. Use `error.details.resets_at` and the counters in [Limits, budgets, and usage](https://api.blackpearl.com/docs/usage-quotas) to decide whether to wait for the next UTC month or ask an administrator to change the control. The complete decision table is in [Errors](https://api.blackpearl.com/docs/errors). --- # Limits, budgets, and usage > Distinguish request rate, monthly quota, hard budget, credits, and Usage API counters. Canonical guide: Raw Markdown: OpenAPI: Five controls can affect whether work starts: | Control | Purpose | Failure and recovery | |---|---|---| | Request rate | Limits request pace for a project key. | `rate_limit_exceeded`; retry after `Retry-After`. | | Usage quota | Limits metered requests, tokens, or cost at organization, project, API-key, and optional capability scope for a UTC calendar month. | `quota_exceeded`; use `error.details.resets_at`, wait for reset, or ask an administrator to change the quota. | | Hard cost budget | Stops new capability work after an organization or project monthly cost ceiling is reached. | `budget_exceeded`; use `error.details`, wait for reset, or ask an administrator to change the budget. | | Usage-tier cost cap | Stops new capability work at the organization's tier-based monthly spend ceiling. | `budget_exceeded` with `error.details.scope: usage_tier`; wait for reset or a tier change. | | Prepaid credits | Requires a positive spendable balance when billing is enabled; no future job price is estimated or reserved. | `insufficient_credits`; add credits or let auto-recharge complete. | Only the rate-limit case is a transient automatic retry. See [Errors](https://api.blackpearl.com/docs/errors). All quota and budget periods begin at `00:00:00 UTC` on the first day of a calendar month and reset at the next month's boundary. An error's structured `details` reports the applicable scope, limit, current value, remaining value, and `resets_at`. Do not automatically retry either code merely because its HTTP status is `429`: an administrator may change a control before reset, but only `rate_limit_exceeded` is transient by itself. ## What each counter records One newly accepted Playbooks job or Audience cycle adds exactly `1` to Usage `requests`. Idempotent replay returns the original job without adding another request. Rejected job creation, campaign setup, reads, polling, candidate retrieval, and Usage calls add `0` to this monthly counter. They can still consume the API-key request-rate window. When a job finishes, its capability execution adds attributed `tokens` and downstream `cost_usd`; it does not add a second request. A request quota checks whether the next new job would exceed its limit. Token and cost quotas stop new jobs once recorded usage has reached the limit. Work already accepted continues even if a later completion crosses a token, cost, or budget threshold. Budgets use the append-only downstream cost ledger, independently of Usage quotas. A soft budget records alerts but does not reject work. A hard budget rejects new work at or above its monthly ceiling. Prepaid billing is separate: any positive spendable balance permits work to start, with no admission estimate or price reserve. At completion, actual downstream cost with the organization's billing markup is charged. If that actual price exceeds the remaining balance, the balance is consumed and the job fails without exposing its result; auto-recharge is then triggered. `credits.balance_usd` is the current ledger balance, not a monthly counter. Usage records are append-only. The public Usage API returns the authenticated project's current monthly rollup and capability breakdown: ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/usage" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` ```json { "project_id": "proj_example", "period": "2026-07", "period_start": "2026-07-01T00:00:00Z", "period_end": "2026-08-01T00:00:00Z", "rollup": { "requests": 12, "tokens": 45200, "cost_usd": 1.243 }, "by_capability": [ { "capability_key": "playbooks", "requests": 12, "tokens": 45200, "cost_usd": 1.243 } ], "usage_tier": { "key": "growth", "name": "Growth", "rank": 2, "rate_limit_per_minute": 120, "monthly_cost_cap_usd": 250.0 }, "rate_limit": { "limit": 120, "window_seconds": 60 }, "quotas": [ { "metric": "requests", "period": "month", "scope": "project", "capability_key": "playbooks", "limit_value": 1000, "used": 12, "remaining": 988, "resets_at": "2026-08-01T00:00:00Z" } ], "budgets": [ { "period": "month", "scope": "project", "capability_key": null, "limit_usd": 50, "spend_usd": 1.243, "remaining_usd": 48.757, "hard_limit": true, "resets_at": "2026-08-01T00:00:00Z" } ], "credits": { "billing_enabled": true, "balance_usd": 98.757, "topup_increment_usd": 25.0 } } ``` `period_start` is inclusive and `period_end` is exclusive. `rollup` totals the project, while `by_capability` reconciles those counters by capability key. `usage_tier` is nullable and reports the organization's resolved rate and monthly cost cap when tiering applies. `quotas` includes every organization, project, or API-key quota that can affect this key; `budgets` includes every organization or project budget that can affect this project. Empty arrays mean no matching control is configured. Compare a completed job's `usage` with these rollups while allowing for other jobs in the same period. Usage retrieval is OpenAPI operation `usage_v1_usage_get` with schema `PublicUsageResponse`. --- # Troubleshooting and request diagnostics > Correlate a failed call, inspect organization request records, and prepare a support-safe diagnostic package. Canonical guide: Raw Markdown: OpenAPI: ## Capture a request ID Every response includes `X-Request-ID`; every public error repeats the same value as `error.request_id`. The gateway generates a value when the client does not send one. To correlate your own logs, send an ASCII identifier of 1-128 characters that starts with a letter or digit and then uses only letters, digits, `.`, `_`, `:`, or `-`: ```bash REQUEST_ID="checkout:20260714:7f20" curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/capabilities" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "X-Request-ID: $REQUEST_ID" \ --dump-header - ``` The response echoes a valid client value. An invalid value is rejected as `422 invalid_request`, and the response carries a newly generated safe ID. Request IDs are correlation values, not authentication or idempotency controls; use `Idempotency-Key` separately on job creation. For accepted or rejected generation work, an organization console user can open **Logs**, paste the exact request ID into the search box, and inspect method, path, status, capability, time, tokens, cost, and latency. Completion accounting can appear as another record correlated to the job ID. Read-only polling and authentication failures do not create Usage log rows, so retain the response ID for the support workflow below. ## Diagnose by failure class | Symptom or code | Check | Next action | |---|---|---| | `missing_api_key` | The request has no Bearer header. | Add `Authorization: Bearer $BLACKPEARL_API_KEY` from the server-side secret store. | | `invalid_api_key` or `expired_api_key` | Key was copied incorrectly, revoked, or expired. | Rotate to an active key; do not retry the unchanged credential. | | `project_archived` or `forbidden` | Key project is archived or does not own the resource. | Use an active owning-project key. Cross-project IDs deliberately look missing. | | `capability_disabled` | `GET /v1/capabilities` does not list the requested capability. | Ask an organization administrator to enable the entitlement. | | `rate_limit_exceeded` | `RateLimit-Remaining` is `0`. | Wait at least `Retry-After`/`RateLimit-Reset`, then retry with jitter. | | `quota_exceeded` | `error.details` identifies metric, scope, used, limit, and `resets_at`. | Wait for the UTC month reset or ask an administrator to change the quota. | | `budget_exceeded` | `error.details` identifies spend, limit, hard-limit state, and `resets_at`. | Wait for reset or ask an administrator to change the hard budget. Do not use a rate-limit retry loop. | | `insufficient_credits` | The billed organization had no positive balance at admission, or a completed run's actual charge exceeded the remaining balance. | Let auto-recharge complete or add prepaid credit before creating new work. | | Job status `failed` | The job's public `error` and request ID are safe to record. | Stop polling, preserve the job ID, and correct non-transient input/configuration causes before creating new work. | | Audience `502` or `503` | Capability service returned an invalid response/error or was unavailable. | Retry a safe read with bounded backoff. For ambiguous creation, repeat the exact request with its persisted `Idempotency-Key`. | Never infer failure from a local polling timeout: it stops only the client. Keep the job ID and resume `GET /v1/jobs/{job_id}`. See [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs), [Errors](https://api.blackpearl.com/docs/errors), and [Limits, budgets, and usage](https://api.blackpearl.com/docs/usage-quotas). ## Prepare a support-safe package If the documented action does not resolve the problem, send the account's established support contact these values: - `error.request_id` or the `X-Request-ID` response value; - UTC timestamp, HTTP method, and path without sensitive query values; - HTTP status and `error.code`; - organization/project ID and API-key prefix (never the secret key); - job/campaign ID and the non-secret `Idempotency-Key`, when relevant; and - a short reproduction sequence and whether the failure is repeatable. Do not include the Bearer credential, cookies, full request bodies, candidate exports, or other confidential data. Support can correlate the request ID with platform request records; the package does not require you to access internal logs or source code. No public status-page or generic support URL is configured in the current deployment metadata. Use the support channel named in your account agreement; public documentation will link dedicated destinations when they exist. --- # Connections > Read the provider catalog and redacted project connection metadata while live connector operations remain unavailable. Canonical guide: Raw Markdown: OpenAPI: Connectors is a Preview inventory surface, not a finished integration runtime. The catalog describes provider manifests and credential fields. Connections are encrypted, project-scoped configuration records created and rotated by organization administrators in the Console. The public API is read-only. There are currently no public operations that verify a provider credential, read provider records, write provider records, execute a sync, send a message, receive a webhook, or complete an OAuth callback. Catalog entries report verification as `unavailable`, and many are not bound to any executor. Do not treat a configured Connection as proof that a provider is reachable, authorized, funded, or operational. ## Read the catalog Preview operation `connector_catalog_list_v1_connectors_get` returns `GET /v1/connectors`. Each entry describes a stable provider key, version, display metadata, credential field names, capabilities, limits, provenance, and verification mode. It never returns credential values. ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/connectors" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` Treat declared capabilities as reviewed inventory only. A future adapter must have its own execution, authorization, rate-limit, retry, audit, and data-rights review before a capability can become operational. ## Read configured connections Preview operation `connection_list_v1_connections_get` returns redacted `GET /v1/connections` rows for the API key's project. A row identifies the provider, display label, status, catalog version, credential revision, configured field names, and timestamps. Secret values, ciphertext, key IDs, OAuth tokens, and raw verification responses are never returned. ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/connections" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` The Console stores provider credential documents encrypted with a dedicated rotation keyring. Provider secrets are distinct from the Blackpearl bearer key and must never appear in public Job inputs, URLs, free-text reasons, metadata, logs, or Outcome payloads. Because the surface is Preview, response fields and provider inventory can change incompatibly. Pin only opaque connection IDs when testing a Preview capability, and design a safe fallback when the associated executor is unavailable. --- # Inspect the CRM schema preview boundary > Understand the Console-only HubSpot and Salesforce metadata snapshot contract and its fail-closed service boundary. Canonical guide: Raw Markdown: OpenAPI: CRM Schema is a Console-only Preview capability; it has no public `/v1` operation. It is included here so Console documentation links resolve and integrators do not mistake a private adapter boundary for a supported public connector. The capability accepts only HubSpot or Salesforce Connection IDs owned by the same organization and project. Its intended scope is read-only object, field, enumeration, and relationship metadata. It cannot read CRM records, mutate a CRM, send messages, or broaden a provider token's permissions. The capability calls a private service adapter only when its service token is configured. Without that configuration the Job fails closed, makes no provider request, and persists no schema snapshot. A configured private adapter does not make the public Connector catalog operational. Treat typed issues literally, and never use incomplete or failed discovery data to configure irreversible production mappings. Provider credentials are decrypted only inside the private adapter path, must match the bound Connection revision, and must not appear in Job input, result, logs, audit metadata, or client responses. Until the capability gains a stable public contract and verified live implementation, keep it behind Console access controls and a Preview feature flag. --- # Capabilities and entitlements > Discover which capability keys the authenticated organization can use before creating work. Canonical guide: Raw Markdown: OpenAPI: Call capabilities before presenting a capability-specific workflow: ```bash curl --fail-with-body --silent --show-error \ "$BLACKPEARL_API_BASE_URL/v1/capabilities" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" | jq . ``` ```json { "capabilities": [ { "key": "playbooks", "name": "Playbooks", "description": "Generate AI sales playbooks for a target account.", "category": "Generation", "enabled": true, "status": "ga", "docs_slug": "playbooks" } ] } ``` The endpoint returns enabled capabilities for the key's organization. Use `key` as the stable programmatic identifier and `docs_slug` to link to the corresponding guide. `status` is `ga` or `coming_soon`; the latter maps to Preview documentation, not a GA compatibility promise. Clients must still tolerate future additive enum values and optional response fields. Console visibility and execution entitlement are independent internal controls. The public response includes only executable capabilities. A create request for unavailable functionality returns `capability_disabled`; obtain access before retrying. This is OpenAPI operation `capabilities_v1_capabilities_get` with schema `PublicCapabilitiesResponse`. Continue with [Prospecting](https://api.blackpearl.com/docs/prospecting), [Account research](https://api.blackpearl.com/docs/prospect-research), [Documents](https://api.blackpearl.com/docs/documents), [Audiences](https://api.blackpearl.com/docs/audiences), or [Playbooks](https://api.blackpearl.com/docs/playbooks). --- # AI and machine-readable documentation > Fetch raw guides, documentation bundles, OpenAPI, or the read-only documentation MCP server without scraping the web application. Canonical guide: Raw Markdown: OpenAPI: All resources on this page are public and read-only. They contain developer documentation and the public API contract, never project API keys or private repository content. ## Direct sources | Resource | URL | Use | |---|---|---| | Playbooks agent primer | [Raw Markdown](https://api.blackpearl.com/docs/raw/playbooks-api-primer.md) | Small, complete context for the golden Playbooks flow. | | Documentation index | [`llms.txt`](https://api.blackpearl.com/llms.txt) | Curated guide descriptions and direct Markdown links. | | Full documentation | [`llms-full.txt`](https://api.blackpearl.com/llms-full.txt) | Deterministic bundle of every public canonical guide. | | OpenAPI | [Download JSON](https://api.blackpearl.com/v1/openapi.json) | Exact paths, authentication, fields, enums, constraints, and response schemas. | | Documentation manifest | [Download JSON](https://api.blackpearl.com/docs/manifest.json) | Ordered public page metadata and stable source URLs. | | Documentation MCP | `https://api.blackpearl.com/docs/mcp/` | Search and retrieve canonical public pages with citation URLs. | Every guide is also plain Markdown: append `.md` to its canonical URL (for example `https://api.blackpearl.com/docs/getting-started.md`), or use the equivalent `https://api.blackpearl.com/docs/raw/{slug}.md` path. Raw files and bundles are generated from canonical Markdown; they are not separately maintained prose. ## Codex Add the Streamable HTTP server to `~/.codex/config.toml`, or use `.codex/config.toml` in a trusted project: ```toml [mcp_servers.blackpearl_docs] url = "https://api.blackpearl.com/docs/mcp/" ``` The Codex app, CLI, and IDE extension share this configuration. Restart or begin a new session, then ask Codex to search `blackpearl_docs` before implementing a Blackpearl integration. ## Claude Code ```bash claude mcp add --transport http blackpearl-docs https://api.blackpearl.com/docs/mcp/ ``` Run `/mcp` in Claude Code to confirm the server and its `search_documentation` and `get_documentation` tools are available. ## Claude.ai and Claude Cowork The documentation MCP server also works as a claude.ai custom connector, which makes it available in chats, projects, and Cowork sessions: 1. In claude.ai, open **Settings → Connectors** and choose **Add custom connector**. 2. Enter `https://api.blackpearl.com/docs/mcp/` as the remote MCP server URL. The server is public and read-only, so no OAuth or API key is required. 3. In a chat or Cowork session, enable the connector from the tools menu, then ask Claude to search the Blackpearl documentation before writing integration code. Cowork sessions that run Claude Code can instead use the `claude mcp add` command above; both paths expose the same `search_documentation` and `get_documentation` tools. ## Cursor Create `.cursor/mcp.json` for a project or `~/.cursor/mcp.json` globally: ```json { "mcpServers": { "blackpearl-docs": { "type": "http", "url": "https://api.blackpearl.com/docs/mcp/" } } } ``` Enable the server under Cursor's tools settings. Cursor Agent can then search and retrieve the same Markdown published at the raw URLs. ## VS Code agent mode Create `.vscode/mcp.json` in the workspace: ```json { "servers": { "blackpearl-docs": { "type": "http", "url": "https://api.blackpearl.com/docs/mcp/" } } } ``` Start the server from the editor, open GitHub Copilot Chat in agent mode, and enable the Blackpearl documentation tools. No credentials or request headers are required because this server exposes only public documentation. ## Suggested agent instruction ```text Before changing a Blackpearl integration, search the blackpearl_docs MCP server. Use get_documentation for the canonical guide and consult the published OpenAPI for exact request and response schemas. Cite the source_url returned by the tool. Do not infer undocumented retry, retention, idempotency, or sandbox behavior. ``` Client configuration formats follow the current official setup references for [Codex](https://learn.chatgpt.com/docs/extend/mcp), [Claude Code](https://docs.anthropic.com/en/docs/claude-code/mcp), [Cursor](https://docs.cursor.com/context/model-context-protocol), and [VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers). --- # Playbooks API primer for agents > Give a coding agent the minimum complete contract for creating, polling, and consuming a Playbooks job safely. Canonical guide: Raw Markdown: OpenAPI: Use this page with the canonical [OpenAPI document](https://api.blackpearl.com/v1/openapi.json). OpenAPI is authoritative for exact schemas; this primer is authoritative for the integration sequence and operational decisions. ## Contract at a glance - Base URL: `BLACKPEARL_API_BASE_URL`, configured for the deployment. - Authentication: `Authorization: Bearer $BLACKPEARL_API_KEY` on every `/v1` request. - Create: `POST /v1/playbooks` using operation `create_playbook_v1_playbooks_post` and schema `PlaybookInput`. - Poll: `GET /v1/jobs/{job_id}` using operation `get_job_v1_jobs__job_id__get`. - Create response: `202 Accepted` with schema `PublicPlaybookJobResponse`. - Create retry safety: send `Idempotency-Key`; matching retries replay the original response for 24 hours. - Result: when the job succeeds, `result` is schema `PublicPlaybookResult`. - Retention: `expires_at` is 30 days after creation; store results needed after that time. - Errors: schema `ErrorResponse`; branch on `error.code`, not HTTP status alone. Create separate projects and keys for development, staging, and production. Hosted development calls still perform real work and record real usage. ## Minimal request Set the configured API URL and a server-side project key: ```bash export BLACKPEARL_API_BASE_URL="https://api.blackpearl.com" export BLACKPEARL_API_KEY="replace-with-your-project-key" export BLACKPEARL_IDEMPOTENCY_KEY="playbook-$(date +%s)-$$" curl --fail-with-body --silent --show-error \ --request POST "$BLACKPEARL_API_BASE_URL/v1/playbooks" \ --header "Authorization: Bearer $BLACKPEARL_API_KEY" \ --header "Idempotency-Key: $BLACKPEARL_IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data '{"target_company_domain":"acme.example"}' ``` Provide at least one of `target_company` or `target_company_domain`. A domain is a hostname without a scheme or path. Optional `tone` values are `consultative`, `direct`, and `technical`; `model` is currently `default`. Omit `sections` for the complete result. Persist the idempotency key before the first request and the returned `id` immediately after success. If the connection is lost or a server response is ambiguous, repeat the exact POST with the same key within 24 hours. A matching request returns the original response without creating or billing another job. A changed request returns `idempotency_key_reused`; `idempotency_in_progress` means retry the same request and key after a short delay. ## Poll every terminal state Wait 2 seconds before the first GET, then use exponential backoff with jitter capped at 10 seconds. Apply a caller deadline and retain the job ID so polling can resume later. A client timeout does not cancel server work. | Status | Terminal | Action | |---|---:|---| | `queued` | No | Wait and poll again. | | `running` | No | Optionally show `progress`, then poll again. | | `succeeded` | Yes | Validate and consume the typed `result`. | | `failed` | Yes | Stop and surface the job `error`. | | `canceled` | Yes | Stop; there is no public cancel operation. | On success, useful first outputs are `result.business_summary` and the first item in `result.sales_angles`. Collections are arrays. Research-derived scalar fields can be null; follow `PublicPlaybookResult` rather than inventing fallback fields. ## Errors, retries, and usage - `rate_limit_exceeded` is transient. Wait at least the `Retry-After` value, then retry with bounded backoff and jitter. - `quota_exceeded` and `budget_exceeded` also use status `429`, but require a reset or configuration change. Do not automatically retry them. - Authentication, entitlement, and validation codes require the caller to change credentials, project state, access, or input. - A safe GET may be retried for a documented transient `5xx`. Repeat an ambiguous create POST only with its original idempotency key and exact input. - Polling is rate-limited but does not start another billed Playbooks generation. Inspect the completed job's `usage` and reconcile project totals through `GET /v1/usage`. - A job or candidate read after `expires_at` returns `410 job_expired`. Create a new job if no client-stored result exists. ## Do not assume - Do not assume a key prefix creates a sandbox. - Do not assume a fixed completion-time SLA, fixed request-per-minute limit, or cancellation endpoint. - Do not assume an idempotency key remains replayable after 24 hours or can be reused with different input. - Do not wait only for `succeeded`; all three terminal states stop polling. - Do not treat every `429` or `5xx` as retryable. - Do not expose a project key in browser or mobile code. For complete examples, use the [Quickstart](https://api.blackpearl.com/docs/getting-started). For field interpretation and recovery details, use [Playbooks](https://api.blackpearl.com/docs/playbooks), [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs), [Errors and retry decisions](https://api.blackpearl.com/docs/errors), and [Limits, budgets, and usage](https://api.blackpearl.com/docs/usage-quotas). --- # Roles and console access > Understand console roles while keeping public API authorization project-scoped. Canonical guide: Raw Markdown: OpenAPI: Console users belong to an organization as `owner`, `admin`, `member`, `viewer`, or `billing`. Owners and administrators manage projects, keys, members, capabilities, and limits; members operate enabled capabilities; viewers are read-only; billing users focus on billing data. The console authenticates with an HTTP-only session cookie. Public `/v1` integrations do not use that cookie or inherit a user's console session: they use a project-scoped Bearer key as described in [Authentication](https://api.blackpearl.com/docs/authentication). Organization invitations are deliberate and role-specific. Do not share personal credentials or API keys to work around missing membership. Administrators should issue the appropriate membership and create separate project keys for deployed services. --- # Workbench guide > Explore Playbooks inputs and results in the console before automating the public API. Canonical guide: Raw Markdown: OpenAPI: The Workbench calls the same capability implementation as the public API, handles job polling, and renders the result. Use it to explore inputs before automating them. 1. Select the project that should own usage. 2. Enter a target company or domain and optional seller context. 3. Choose a documented tone and section set. 4. Submit, watch job status and progress, then review the rendered result and usage. Workbench requests perform real work on hosted deployments. A development project isolates that work but does not simulate it. For automation, start with the [Quickstart](https://api.blackpearl.com/docs/getting-started). The exact request values are in [Playbooks](https://api.blackpearl.com/docs/playbooks), job behavior is in [Jobs and resumable polling](https://api.blackpearl.com/docs/jobs), and failures are handled with [Errors and retry decisions](https://api.blackpearl.com/docs/errors). --- # Security and data handling > Review credential boundaries, project isolation, capability data flow, retention, and logging behavior. Canonical guide: Raw Markdown: OpenAPI: ## Credential boundary Public API keys are project-scoped secrets. The console displays a new secret once; the gateway stores only its hash for later authentication. Keep the key in a server-side secret manager, inject it into the calling process at runtime, and send it only in the `Authorization: Bearer ...` header over HTTPS. Never commit it, put it in a URL, paste it into a support request, or embed it in browser, desktop, or mobile code where an end user can extract it. Rotate without downtime by creating a replacement key in the same project, deploying it to every server-side caller, confirming traffic on the replacement, and then revoking the old key. Revocation and configured expiry stop new authentication immediately. The platform's private capability-service credentials remain at the gateway and are not returned to API clients or forwarded from the client's Bearer key. ## Tenant and ownership boundaries Authentication resolves one organization and one project from the key. Public campaign and job operations enforce that project boundary: an ID owned by another project is returned as `404 not_found`, including when the caller knows the ID. Usage is attributed to the key's project, while matching organization-, project-, key-, and capability-scoped controls can govern new work. Use separate projects and keys for development, staging, production, or customers whose jobs and accounting must be isolated. A key is not a user session and does not inherit console membership roles. See [Authentication and project environments](https://api.blackpearl.com/docs/authentication) for the lifecycle and [Roles and API-key authority](https://api.blackpearl.com/docs/rbac) for the authorization model. ## Data sent for capability work The gateway stores the validated public input and sends the fields needed to the configured private capability service: | Capability | Data sent upstream | Data returned and retained by the platform | |---|---|---| | Playbooks | Target company name or domain, anchor-company and product context, requested tone/sections, a job correlation value, and the selected brand profile's branding snapshot when present. | Generated account research, personas, sales guidance, attributed usage, and private backend correlation diagnostics. | | Audiences | Campaign name and objective, website/product/ICP/additional context, target size and workflow settings, excluded domains, reusable campaign setup artifacts, and organization/job correlation values. | Candidate summary, candidate pages fetched from the capability service, reusable setup artifacts, delivered-domain deduplication data, stage state, usage, cost, and private backend correlation diagnostics. | | Message Templates | Template generation sends the validated audience, objective, offer summary, desired next step, copy controls, and selected Brand/Offer/Campaign snapshots to OpenAI Responses with provider storage disabled. Prospect personalization sends no prospect identity, work email, retained evidence, sender identity, or compliance data to OpenAI; it verifies platform ledgers and renders the selected immutable revision locally. | Immutable structural revisions, generation input/result, OpenAI token/cost attribution and private response correlation IDs, plus personalization inputs and approval artifacts. Personalization job data can include prospect and sender identity, work email, retained evidence references, and compliance attestations/verifications. | Do not submit secrets or regulated personal data in free-text targeting, product, or additional-context fields unless your agreement explicitly permits it. Candidate output can include business contact data; apply your own access, lawful-use, export, and deletion controls after retrieval. The platform uses separately configured service credentials for upstream calls—your project API key is not sent to capability services. ## Retention and deletion Every public job publishes `expires_at`, exactly 30 days after creation. Until then, the gateway exposes owned job metadata, input, result, and Audience candidates. Client timeout does not cancel execution; retain the job ID and resume polling. Store any result your application must keep beyond `expires_at` in a system with your required security and deletion policy. Message personalization refuses a Prospecting source job as soon as its `expires_at` boundary is reached, both when accepting the request and again before execution. This prevents a queued job from reusing prospect PII after the source retention window closes. After expiry, public job and candidate reads return `410 job_expired`. Scheduled cleanup removes the platform-retained job input, result, error detail, stages, and private job diagnostics, while keeping a minimal ownership/status tombstone. Append-only usage, downstream cost, credit, and audit ledgers remain for accounting and security requirements. The 30-day gateway cleanup is not a promise that a configured upstream capability service has deleted its own operational records; upstream deletion obligations are governed by the applicable service agreement. ## Logs, errors, and request IDs The gateway request log records request ID, method, path, status, latency, and client IP. It does not write the `Authorization` header, query string, or request/response body in that log. Public errors replace unexpected internal failures with a stable safe message. Usage and audit records can store request correlation metadata, principal IDs, operation names, status, and accounting values; job payload and private backend diagnostics are retained as described above. Send a valid `X-Request-ID` to correlate a call, or use the generated ID returned in the `X-Request-ID` response header and `error.request_id`. A request ID is not a secret, but it must never be used as a substitute for authentication. Follow [Troubleshooting and request diagnostics](https://api.blackpearl.com/docs/troubleshooting) to prepare a support-safe diagnostic package. --- # API compatibility and deprecation > Build tolerant v1 clients and understand additive changes, previews, deprecation notices, and support windows. Canonical guide: Raw Markdown: OpenAPI: ## The v1 compatibility boundary `/v1` is the public API version. Every OpenAPI operation carries `x-stability: ga` or `x-stability: preview`; stability is classified per operation rather than inferred from the URL version. The checked-in contract is generated, semantically linted, and compared with the prior contract in CI using the same rules described here. The platform can make these compatible additive changes within `/v1`: - add an operation or optional request field; - add an optional response field or response header; - add a new documented error code or optional structured error detail; - add a value to an enum; and - add a preview capability or operation without changing a GA operation. Clients must ignore unknown response object fields and unknown response headers. Treat enum values as open sets at runtime: preserve or surface an unknown value instead of crashing, even if generated types model the known values. Use a default branch for unknown `error.code` values and decide retry behavior conservatively from HTTP semantics and documentation. ## Changes that require another version The platform will use a new URL version rather than silently making a GA `/v1` change that removes or renames an operation or field, newly requires previously optional input, changes a field's type or established meaning, removes or narrows enum values, removes a successful response representation, or changes authentication and project-ownership semantics incompatibly. A new version can run alongside `/v1` during migration. Contract-diff CI automatically rejects mechanically detectable examples such as removed paths/operations/properties, newly required fields, narrowed enums, incompatible types, required parameters, required request bodies, and removed successful response codes or media types. Review is still required for semantic changes that a schema diff cannot identify. ## Preview and GA expectations GA operations receive the compatibility and deprecation protections on this page. Preview operations are identified by `x-stability: preview` in OpenAPI and a Preview label in documentation. Preview shapes can change or be withdrawn before GA, with changes recorded in the changelog, and do not receive the GA support window. Moving a preview operation to GA establishes its compatibility baseline; moving a GA operation back to preview is not compatible. ## Deprecation and removal An incompatible GA replacement is introduced under a new API version. The platform will support the superseded GA version for at least 12 months after its replacement reaches GA and will publish a deprecation notice at least 180 days before shutdown. The notice identifies affected operations, the replacement, migration guidance, and the earliest shutdown date. If the two windows imply different dates, the later date applies. Security or legal emergencies can require faster protective action. When feasible, the platform will prefer a compatible mitigation and communicate the exception through the same documentation channels. Review the published [API changelog](https://api.blackpearl.com/docs/api-changelog.md) for observable changes and the generated [OpenAPI contract](https://api.blackpearl.com/v1/openapi.json) for exact current shapes.