> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wokelo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Peer Comparison

> Generate a comprehensive side-by-side benchmarking report for 2–5 companies — covering firmographics, financials, product offerings, feature matrices, business models, employee sentiment, hiring trends, and online visibility — as structured JSON, PDF, DOCX, or PPT.

## 1. Overview

The Peer Comparison API generates a comprehensive multi-dimensional benchmarking report for 2 to 5 companies simultaneously. Unlike Company Research (which covers a single company in depth), this API produces a purpose-built side-by-side analysis designed to surface competitive differentiation across every major dimension — from financials and product features to employee sentiment and hiring velocity.

This is an **asynchronous POST API** — submitting a request returns a `report_id` immediately. You then poll for completion using the [Report Status](/supporting-apis-doc#report-status) endpoint and retrieve the finished report using the [Download Report](/supporting-apis-doc#download-report) endpoint.

**The three-step workflow:**

```text theme={"system"}
Step 1: POST /api/workflow_manager/start/         → returns report_id
Step 2: GET  /api/assets/get_notebook_status/     → poll until status = "Completed"
Step 3: POST /api/assets/download_report/         → retrieve JSON / PDF / DOCX / PPT
```

Companies are identified by **website URL** rather than permalink. Up to 5 URLs can be passed in a single request.

The completed report is structured into several named sections, each returned as a top-level key in the JSON output:

* **Firmographics** — one structured card per company: firmographic details (industry, HQ, headcount, funding stage, total funding), and a full financial suite including revenue, EBITDA, net income, EPS, market cap, and valuation multiples (EV/Sales, EV/EBITDA, P/E)
* **Product offerings** — two views of the competitive product landscape: a narrative `product_offerings` array (one entry per product capability area per company) and a `key_product_features` feature matrix (yes/no cells per company per feature category, with a structured `company_result_mapping`)
* **Business model** — structured area/details comparison across Revenue & Pricing, Operational Setup, Value Proposition, and Customer Segments — one entry per company per area
* **Additional sections** — depending on data availability, reports may also include Employee Reviews (sentiment and satisfaction data), Hiring Trends (role-level headcount and velocity signals), and Online Presence (SEO and web visibility metrics)

**Common use cases:**

* **Competitive diligence packages** — compare a target company against 3–4 direct peers before an IC presentation, with a single report covering every relevant dimension
* **Market map benchmarking** — identify which players offer which product capabilities using the feature matrix, and surface white-space gaps
* **Buy-side deal screening** — compare multiple acquisition candidates side by side on financials, product scope, and headcount to quickly eliminate or prioritise targets
* **Portfolio company positioning** — benchmark a portfolio company against its peers on hiring velocity, employee satisfaction, and online visibility to identify operational underperformance
* **Investor presentations** — export as PPT or PDF to embed peer comparison tables directly in LP updates, conference decks, or deal memos

<Info>
  This API is asynchronous. The initial POST returns a `report_id` only — not the report content. Report generation typically completes in 3–7 minutes for a 5-company comparison. See [How Async APIs work](/how-async-apis-work) for a full explanation of the polling lifecycle.
</Info>

***

## 2. Quick Start

**Step 1 — Submit the report request**

<CodeGroup>
  ```bash cURL theme={"system"}
  curl --location 'https://api.wokelo.ai/api/workflow_manager/start/' \
    --header 'Authorization: Bearer <YOUR_API_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data '{
      "workflow": "player_comparison",
      "websites": [
        "https://we.are.expensify.com/",
        "https://ramp.com/",
        "https://www.airbase.com/"
      ],
      "workbook_name": "Spend Management Peer Comparison"
    }'
  ```

  ```python Python theme={"system"}
  import requests

  response = requests.post(
      "https://api.wokelo.ai/api/workflow_manager/start/",
      headers={
          "Authorization": "Bearer <YOUR_API_TOKEN>",
          "Content-Type": "application/json"
      },
      json={
          "workflow": "player_comparison",
          "websites": [
              "https://we.are.expensify.com/",
              "https://ramp.com/",
              "https://www.airbase.com/"
          ],
          "workbook_name": "Spend Management Peer Comparison"
      }
  )
  report_id = response.json()["report_id"]
  print(f"Report submitted. report_id: {report_id}")
  ```
</CodeGroup>

**Step 2 — Poll for completion**

```python theme={"system"}
import time, requests

def wait_for_report(report_id, api_key, poll_interval=20, timeout=900):
    headers = {"Authorization": f"Bearer {api_key}"}
    elapsed = 0
    while elapsed < timeout:
        r = requests.get(
            "https://api.wokelo.ai/api/assets/get_notebook_status/",
            headers=headers,
            params={"report_id": report_id}
        )
        status = r.json().get("status", "")
        print(f"[{elapsed}s] Status: {status}")
        if status == "Completed":
            return True
        if status == "Failed":
            raise Exception(f"Report {report_id} failed")
        time.sleep(poll_interval)
        elapsed += poll_interval
    raise TimeoutError(f"Report {report_id} did not complete within {timeout}s")

wait_for_report(report_id, api_key="<YOUR_API_TOKEN>")
```

**Step 3 — Download the report**

```python theme={"system"}
# Fetch as JSON for programmatic processing
result = requests.post(
    "https://api.wokelo.ai/api/assets/download_report/",
    headers={"Authorization": "Bearer <YOUR_API_TOKEN>"},
    json={"report_id": report_id, "file_type": "json"}
)
report = result.json()
print(f"Sections: {list(report.keys())}")

# Or download as a formatted document
ppt_result = requests.post(
    "https://api.wokelo.ai/api/assets/download_report/",
    headers={"Authorization": "Bearer <YOUR_API_TOKEN>"},
    json={"report_id": report_id, "file_type": "ppt"}
)
with open("peer_comparison.pptx", "wb") as f:
    f.write(ppt_result.content)
```

***

## 3. Authentication

All requests must include a **Bearer token** in the `Authorization` HTTP header.

```text theme={"system"}
Authorization: Bearer <YOUR_API_TOKEN>
```

API tokens are issued from your Wokelo account. Navigate to **Account Details → API Credentials** in the Wokelo dashboard to get your client id and client secret. Contact [support@wokelo.ai](mailto:support@wokelo.ai) if you do not yet have API access.

<Warning>
  Never expose your token in client-side code, browser requests, or public repositories. A missing or invalid token returns `401 Unauthorized`. A valid token without sufficient plan permissions returns `403 Forbidden`.
</Warning>

***

## 4. Request Reference

**Endpoint**

```text theme={"system"}
POST https://api.wokelo.ai/api/workflow_manager/start/
```

All parameters are passed as JSON in the request body.

| Parameter       | Type      | Required     | Description                                                                                                                                                                                                                                                                       |
| --------------- | --------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow`      | string    | **Required** | Must always be `"player_comparison"`. This is distinct from `"company_primer"` (Company Research) and `"industry_primer"` (Industry Research). Using the wrong value submits the wrong report type or returns a `400` error.                                                      |
| `websites`      | string\[] | **Required** | Array of company website URLs to compare. Accepts 2–5 entries. Both bare domain format (`"stripe.com"`) and full URL format (`"https://www.stripe.com"`) are accepted. All companies must be web-resolvable — Wokelo uses the URL to identify each company in its knowledge base. |
| `workbook_name` | string    | Optional     | Label for the generated report workbook. Defaults to a generic name if omitted. Using a descriptive name (e.g. `"Payments Competitive Landscape Q2 2026"`) makes the report easier to identify in subsequent Report Status or Download calls.                                     |
| `custom_files`  | object\[] | Optional     | Array of file references to include in the report alongside Wokelo's synthesis. Each object should use the `fileName` value returned by the [File Upload API](/supporting-apis-doc#file-upload).                                                                                  |

<Info>
  The minimum number of websites is 2 and the maximum is 5. Passing a single URL or more than 5 will return a `400 Bad Request`. All URLs must be for different companies — duplicate entries will not produce meaningful comparisons.
</Info>

**Full request example:**

<CodeGroup>
  ```bash cURL theme={"system"}
  curl --location 'https://api.wokelo.ai/api/workflow_manager/start/' \
    --header 'Authorization: Bearer <YOUR_API_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data '{
      "workflow": "player_comparison",
      "websites": [
        "https://we.are.expensify.com/",
        "https://ramp.com/",
        "https://www.airbase.com/",
        "http://www.expensya.com/",
        "https://payhawk.com/"
      ],
      "workbook_name": "Spend Management Peer Comparison"
    }'
  ```

  ```python Python theme={"system"}
  import requests

  response = requests.post(
      "https://api.wokelo.ai/api/workflow_manager/start/",
      headers={
          "Authorization": "Bearer <YOUR_API_TOKEN>",
          "Content-Type": "application/json"
      },
      json={
          "workflow": "player_comparison",
          "websites": [
              "https://we.are.expensify.com/",
              "https://ramp.com/",
              "https://www.airbase.com/",
              "http://www.expensya.com/",
              "https://payhawk.com/"
          ],
          "workbook_name": "Spend Management Peer Comparison"
      }
  )
  print(response.json())   # {"report_id": 1002345}
  ```
</CodeGroup>

***

## 5. Response

### Submission response

The initial POST returns immediately with a single field:

```json theme={"system"}
{
  "report_id": 1002345
}
```

| Field       | Type    | Description                                                                                                                      |
| ----------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `report_id` | integer | Unique identifier for this report job. Store immediately — it is the only handle to your report and cannot be recovered if lost. |

### Report status response

Poll `GET /api/assets/get_notebook_status/?report_id={report_id}` until the status field is `"Completed"`.

| Status value   | Meaning                                                       |
| -------------- | ------------------------------------------------------------- |
| `"Pending"`    | Report is queued and waiting to start.                        |
| `"Processing"` | Report generation is in progress.                             |
| `"Completed"`  | Report is ready to download.                                  |
| `"Failed"`     | Report generation encountered an error. Retry the submission. |

### Downloaded report structure

When you call Download Report with `"file_type": "json"`, the response is a nested JSON object with section names as top-level keys. The primary sections are:

**Firmographics**

Path: `report["Firmographics"]["Firmographic"]["firmographics"]["firmographics"]`

An array of company objects, one per company in the same order as the submitted `websites` array. Each object contains:

* `organization` — `name`, `logo` (URL), `url` (Wokelo dashboard link)
* `details` — firmographic fields:
  * `industry` (string), `product_category` (string), `founded_year` (string), `hq` (string), `company_type` (`"public"`, `"private"`, `"startup"`)
  * `employees_in_crunchbase` (string range, e.g. `"1001-5000"`), `website` (string)
  * `total_funding` (integer USD, e.g. `2967000000` for \$2.97B) — may be `0` for unfunded/bootstrapped companies
  * `last_funding_round` — object with `currency`, optional `value` (integer USD), optional `date` (`YYYY-MM-DD`), and `round` (e.g. `"Series E"`, `"Public"`, `"Acquired"`)
  * `financials` — a dict of financial metric objects. Each metric (e.g. `"Total revenue"`, `"EBITDA"`, `"Market cap"`) contains `period` (`YYYY-MM-DD`), `v` (numeric value or `null`), and `currency`. For public companies, this also includes `"Ticker"` (string), `"EV/Sales"`, `"EV/EBITDA"`, `"P/E"`, `"EV/EBIT"`, margin fields, and EPS fields. Private companies typically have fewer or no `financials` fields.
* `description` — paragraph description of the company's product and market position
* `sources` array — data attribution (Crunchbase, S\&P CapIQ, Wokelo Synthesis)

**Product offerings**

Path: `report["Product offerings"]["Product offerings"]`

Contains two parallel views of the competitive product landscape:

`product_offerings` — array of area objects, one per product capability area (e.g. "Expense Management & Reporting", "Corporate Card & Payment Solutions"). Each area object has an `area` label and a `details` array with one entry per company, containing:

* `company_name`, `permalink`, `area_details` (prose description of that company's capability in this area), and `sources` array

`key_product_features` — a structured feature matrix with:

* `rows` — array of feature objects. Each object has `category_name` (the feature being compared), `commentary` (a sentence summarising the competitive picture), and `company_names` (array of `{result, company_name, company_permalink}` objects where `result` is `"yes"` or `"no"`)
* `company_result_mapping` — dict keyed by permalink with `"yes"` / `"no"` values — useful for programmatic table generation
* `columns` — ordered list of permalinks matching the companies in the comparison

**Business model**

Path: `report["Business model"]["Business model"]["business_model"]`

Array of area objects following the same `area` / `details` pattern as product offerings, covering: Revenue & Pricing, Operational Setup, Value Proposition, and Customer Segments.

<Info>
  The `financials` dict in Firmographics uses `"v"` as the value key (not `"value"`). It can be `null` for metrics that are unavailable or not applicable (e.g. P/E for a company with negative earnings). Always check for `null` before performing arithmetic on financial fields.
</Info>

### Financial field notes

Financial data in the `details.financials` dict uses a consistent structure:

```json theme={"system"}
"Total revenue": {
  "period": "2024-12-31",
  "v": 139236000.0,
  "currency": "USD"
}
```

Raw values are in the base unit (USD, not millions). To convert:

```python theme={"system"}
# Revenue in millions
revenue_m = financials["Total revenue"]["v"] / 1_000_000  # → 139.2

# EBITDA margin is stored as a fraction, not a percentage
ebitda_margin_pct = financials["EBITDA margin (%)"]["v"] * 100  # → 0.068%
```

Note that `EBITDA margin (%)` and `Net margin (%)` store fractional values (e.g. `0.0675` = 6.75%), not percentage values. Multiply by 100 before displaying.

### Download format options

| `file_type` | Description                                                                                                                                  |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `"json"`    | Fully structured JSON — all sections, fields, and source citations. Best for programmatic processing, CRM enrichment, or pipeline ingestion. |
| `"pdf"`     | Formatted PDF with comparison tables and charts. Best for sharing with stakeholders.                                                         |
| `"docx"`    | Editable Word document. Best for analysts who customise reports before distribution.                                                         |
| `"ppt"`     | PowerPoint presentation. Best for IC decks, LP updates, and deal memos.                                                                      |

***

## 6. Examples

### Basic peer comparison — three companies

<CodeGroup>
  ```bash cURL theme={"system"}
  # Step 1: Submit
  curl --location 'https://api.wokelo.ai/api/workflow_manager/start/' \
    --header 'Authorization: Bearer <YOUR_API_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data '{
      "workflow": "player_comparison",
      "websites": ["stripe.com", "paypal.com", "square.com"],
      "workbook_name": "Payments Competitive Landscape"
    }'

  # Step 2: Poll (replace 1002345 with your report_id)
  curl --location 'https://api.wokelo.ai/api/assets/get_notebook_status/?report_id=1002345' \
    --header 'Authorization: Bearer <YOUR_API_TOKEN>'

  # Step 3: Download
  curl --location 'https://api.wokelo.ai/api/assets/download_report/' \
    --header 'Authorization: Bearer <YOUR_API_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data '{"report_id": 1002345, "file_type": "json"}'
  ```

  ```python Python theme={"system"}
  import requests, time

  API_KEY = "<YOUR_API_TOKEN>"
  HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

  # Step 1: Submit
  submit = requests.post(
      "https://api.wokelo.ai/api/workflow_manager/start/",
      headers=HEADERS,
      json={
          "workflow": "player_comparison",
          "websites": ["stripe.com", "paypal.com", "square.com"],
          "workbook_name": "Payments Competitive Landscape"
      }
  )
  report_id = submit.json()["report_id"]
  print(f"Submitted. report_id: {report_id}")

  # Step 2: Poll
  while True:
      r = requests.get(
          "https://api.wokelo.ai/api/assets/get_notebook_status/",
          headers=HEADERS,
          params={"report_id": report_id}
      )
      status = r.json().get("status")
      print(f"Status: {status}")
      if status == "Completed":
          break
      if status == "Failed":
          raise Exception("Report failed")
      time.sleep(20)

  # Step 3: Download
  result = requests.post(
      "https://api.wokelo.ai/api/assets/download_report/",
      headers=HEADERS,
      json={"report_id": report_id, "file_type": "json"}
  )
  report = result.json()
  print(f"Sections: {list(report.keys())}")
  ```
</CodeGroup>

**Sample submission response:**

```json theme={"system"}
{
  "report_id": 1002345
}
```

### Extracting and displaying firmographic data

Parse the firmographics section to build a comparison table of key metrics across all companies.

```python theme={"system"}
# Assume report is already downloaded as a dict
report = result.json()

companies = (
    report["Firmographics"]
    ["Firmographic"]
    ["firmographics"]
    ["firmographics"]
)

print(f"{'Company':<20} {'Type':<12} {'HQ':<30} {'Employees':<20} {'Total Funding'}")
print("-" * 100)

for co in companies:
    name    = co["organization"]["name"]
    details = co["details"]
    funding = details.get("total_funding", 0)
    fund_str = f"${funding / 1e9:.1f}B" if funding >= 1e9 else (f"${funding / 1e6:.0f}M" if funding > 0 else "—")

    print(
        f"{name:<20} "
        f"{details.get('company_type', '—'):<12} "
        f"{details.get('hq', '—'):<30} "
        f"{details.get('employees_in_crunchbase', '—'):<20} "
        f"{fund_str}"
    )
```

### Comparing financial metrics across public and private companies

Financial data is only populated for public companies and well-covered private ones. Always guard against `null` values.

```python theme={"system"}
def safe_financial(financials, key, scale=1, as_percent=False):
    """Safely extract a financial metric, handling null and missing fields."""
    field = financials.get(key, {})
    v = field.get("v")
    if v is None:
        return "N/A"
    if as_percent:
        return f"{v * 100:.1f}%"
    if scale == 1e9:
        return f"${v / 1e9:.1f}B"
    if scale == 1e6:
        return f"${v / 1e6:.0f}M"
    return f"{v:.2f}"

companies = (
    report["Firmographics"]["Firmographic"]["firmographics"]["firmographics"]
)

print(f"{'Company':<20} {'Revenue':<15} {'EBITDA Margin':<18} {'Net Margin':<15} {'Market Cap'}")
print("-" * 90)

for co in companies:
    name = co["organization"]["name"]
    fin  = co["details"].get("financials", {})
    print(
        f"{name:<20} "
        f"{safe_financial(fin, 'Total revenue', scale=1e6):<15} "
        f"{safe_financial(fin, 'EBITDA margin (%)', as_percent=True):<18} "
        f"{safe_financial(fin, 'Net margin (%)', as_percent=True):<15} "
        f"{safe_financial(fin, 'Market cap', scale=1e9)}"
    )
```

### Reading the feature matrix

The `key_product_features` matrix is the most structured part of the report and is ideal for programmatic table generation.

```python theme={"system"}
product_data = (
    report["Product offerings"]
    ["Product offerings"]
    ["key_product_features"]
)

columns    = product_data["columns"]    # ordered list of permalinks
rows       = product_data["rows"]

# Build company-name lookup from firmographics
company_names = {
    co["details"].get("website", "").replace("https://", "").replace("http://", "").rstrip("/"): co["organization"]["name"]
    for co in report["Firmographics"]["Firmographic"]["firmographics"]["firmographics"]
}

# Print matrix
header = f"{'Feature':<40} " + "  ".join(f"{c[:12]:<12}" for c in columns)
print(header)
print("-" * (40 + 14 * len(columns)))

for row in rows:
    feature = row["category_name"]
    mapping = row["company_result_mapping"]
    cells   = "  ".join(f"{'✓' if mapping.get(c) == 'yes' else '✗':<12}" for c in columns)
    print(f"{feature:<40} {cells}")
    print(f"  {row['commentary'][:100]}...")
    print()
```

### Extracting business model comparison

Parse the Business model section to compare revenue models and customer segments across all peers.

```python theme={"system"}
business_model = (
    report["Business model"]
    ["Business model"]
    ["business_model"]
)

for area_obj in business_model:
    area = area_obj["area"]
    print(f"\n{'='*60}")
    print(f"  {area}")
    print('='*60)
    for detail in area_obj["details"]:
        name  = detail["company_name"]
        desc  = detail["area_details"]
        print(f"\n  {name}:")
        print(f"    {desc[:200]}...")
```

### Batch comparisons across multiple competitive sets

Run comparisons for several competitive landscapes and collect all reports.

```python theme={"system"}
import requests, time, concurrent.futures

API_KEY  = "<YOUR_API_TOKEN>"
HEADERS  = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

COMPARISONS = {
    "payments":    ["stripe.com", "paypal.com", "square.com", "adyen.com"],
    "crm":         ["salesforce.com", "hubspot.com", "pipedrive.com", "zoho.com"],
    "spend_mgmt":  ["ramp.com", "expensify.com", "brex.com", "airbase.com"],
}

def submit_comparison(name, websites):
    r = requests.post(
        "https://api.wokelo.ai/api/workflow_manager/start/",
        headers=HEADERS,
        json={
            "workflow":      "player_comparison",
            "websites":      websites,
            "workbook_name": f"{name} Peer Comparison"
        }
    )
    return name, r.json()["report_id"]

def poll_until_done(report_id, interval=20, timeout=900):
    elapsed = 0
    while elapsed < timeout:
        r = requests.get(
            "https://api.wokelo.ai/api/assets/get_notebook_status/",
            headers=HEADERS,
            params={"report_id": report_id}
        )
        status = r.json().get("status")
        if status == "Completed":
            return True
        if status == "Failed":
            return False
        time.sleep(interval)
        elapsed += interval
    return False

def fetch_report(report_id):
    r = requests.post(
        "https://api.wokelo.ai/api/assets/download_report/",
        headers=HEADERS,
        json={"report_id": report_id, "file_type": "json"}
    )
    return r.json()

# Submit all
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
    futures = {pool.submit(submit_comparison, name, urls): name for name, urls in COMPARISONS.items()}
    jobs = {}
    for f in concurrent.futures.as_completed(futures):
        name, report_id = f.result()
        jobs[name] = report_id
        print(f"Submitted: {name} → report_id {report_id}")

# Collect
reports = {}
for name, report_id in jobs.items():
    ok = poll_until_done(report_id)
    if ok:
        reports[name] = fetch_report(report_id)
        print(f"Collected: {name}")
    else:
        print(f"Failed: {name}")

print(f"\nCollected {len(reports)} / {len(COMPARISONS)} comparisons")
```

### Exporting a PPT for an IC deck

```python theme={"system"}
import requests

HEADERS = {"Authorization": "Bearer <YOUR_API_TOKEN>"}

ppt_r = requests.post(
    "https://api.wokelo.ai/api/assets/download_report/",
    headers=HEADERS,
    json={"report_id": 1002345, "file_type": "ppt"}
)

with open("payments_peer_comparison.pptx", "wb") as f:
    f.write(ppt_r.content)

print("Saved payments_peer_comparison.pptx")
```

***

## 7. Error Handling

The API uses standard HTTP status codes. The submission endpoint returns errors synchronously; processing errors appear as `"Failed"` status when polling.

| Status                      | Meaning             | Cause & Resolution                                                                                                                                          |
| --------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200 OK`                    | Request accepted    | `report_id` returned. Proceed to polling.                                                                                                                   |
| `400 Bad Request`           | Invalid parameters  | Missing `workflow`, missing or empty `websites`, fewer than 2 or more than 5 websites, wrong `workflow` value, or malformed URLs. Check the `detail` field. |
| `401 Unauthorized`          | Auth failed         | The `Authorization` header is missing or contains an invalid token. Verify your key in **Settings → API Keys**.                                             |
| `403 Forbidden`             | Insufficient access | Your plan does not include access to this endpoint. Contact [support@wokelo.ai](mailto:support@wokelo.ai).                                                  |
| `429 Too Many Requests`     | Rate limit exceeded | Implement exponential back-off on submission. The response includes a `Retry-After` header.                                                                 |
| `500 Internal Server Error` | Server error        | Retry the submission after a brief delay. If the issue persists, contact [support@wokelo.ai](mailto:support@wokelo.ai).                                     |

**Handling a `"Failed"` report status:**

```python theme={"system"}
status_r = requests.get(
    "https://api.wokelo.ai/api/assets/get_notebook_status/",
    headers=HEADERS,
    params={"report_id": report_id}
)
if status_r.json().get("status") == "Failed":
    print(f"Report {report_id} failed. Resubmitting...")
    resubmit = requests.post(
        "https://api.wokelo.ai/api/workflow_manager/start/",
        headers=HEADERS,
        json={
            "workflow":  "player_comparison",
            "websites":  ["stripe.com", "paypal.com", "square.com"],
            "workbook_name": "Payments Comparison (retry)"
        }
    )
    new_id = resubmit.json()["report_id"]
    print(f"New report_id: {new_id}")
```

**Retry with exponential back-off:**

```python theme={"system"}
import time, requests

def submit_with_retry(body, api_key, max_retries=3):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    for attempt in range(max_retries):
        try:
            r = requests.post(
                "https://api.wokelo.ai/api/workflow_manager/start/",
                headers=headers,
                json=body,
                timeout=30
            )
            if r.status_code == 429:
                time.sleep(2 ** attempt)
                continue
            r.raise_for_status()
            return r.json()["report_id"]
        except requests.exceptions.Timeout:
            if attempt == max_retries - 1:
                raise
            time.sleep(1)
    raise Exception(f"Submission failed after {max_retries} attempts")
```

***

## 8. Best Practices

**`workflow` must always be `"player_comparison"` — distinct from all other Workflow APIs**

The `/api/workflow_manager/start/` endpoint is shared across Company Research (`"company_primer"`), Industry Research (`"industry_primer"`), Peer Comparison (`"player_comparison"`), and Custom Workflow. For Peer Comparison, the value must be exactly `"player_comparison"`. Using any other value will produce the wrong report type or a `400` error.

**Use website URLs, not permalinks**

Peer Comparison identifies companies by website URL, not by Wokelo/Crunchbase permalink. Both bare domain (`"stripe.com"`) and full URL (`"https://www.stripe.com"`) formats are accepted. If Wokelo cannot resolve a URL to a known company, that company may be omitted or have incomplete data in the report. If resolution fails, try the company's canonical homepage URL:

```python theme={"system"}
# ❌ Not accepted by this API
"websites": ["salesforce", "hubspot"]         # Permalinks, not URLs

# ✅ Accepted — bare domain
"websites": ["salesforce.com", "hubspot.com"]

# ✅ Also accepted — full URL
"websites": ["https://www.salesforce.com", "https://www.hubspot.com"]
```

**Pass 2–5 companies — the API does not accept fewer or more**

The `websites` array must contain between 2 and 5 entries. Passing a single URL (not a comparison), an empty array, or more than 5 will return a `400 Bad Request`.

**Store `report_id` immediately**

The submission response contains only `report_id`. There is no list-reports endpoint to recover lost IDs. Store it to a database or log immediately:

```python theme={"system"}
report_id = response.json()["report_id"]
save_to_db({"comparison_name": workbook_name, "report_id": report_id, "submitted_at": datetime.now()})
```

**Use 20-second polling intervals — comparisons take longer than single-company reports**

A 5-company comparison runs more research jobs in parallel than Company Research and typically takes 3–7 minutes. Poll every 20 seconds rather than every 15 to avoid unnecessary rate limit consumption:

```python theme={"system"}
for wait in [20, 20, 30, 30, 60, 60, 120]:
    time.sleep(wait)
    status = get_status(report_id)
    if status in ("Completed", "Failed"):
        break
```

**Guard `financials["v"]` against `null` — private companies have sparse financial data**

The `financials` object is populated from S\&P CapIQ and is comprehensive for public companies. For private companies, most or all financial metrics will have `"v": null`. Always check before performing arithmetic:

```python theme={"system"}
# ❌ TypeError when v is null
revenue_m = company["details"]["financials"]["Total revenue"]["v"] / 1e6

# ✅ Safe
fin = company["details"].get("financials", {})
revenue_raw = fin.get("Total revenue", {}).get("v")
revenue_m = revenue_raw / 1e6 if revenue_raw is not None else None
```

**`EBITDA margin (%)` and `Net margin (%)` are fractions, not percentages**

These fields store values like `0.0675` (= 6.75%), not `6.75`. Multiply by 100 before displaying. This is easy to miss when company\_type is "public" and you're cross-checking against other financial sources.

**Use `company_result_mapping` for programmatic matrix generation**

The `key_product_features` section includes a `company_result_mapping` dict keyed by permalink with `"yes"` / `"no"` values. This is the most ergonomic field for building comparison tables programmatically, as you can look up any company's result by permalink without iterating the `company_names` array:

```python theme={"system"}
for row in key_product_features["rows"]:
    ramp_result = row["company_result_mapping"].get("ramp-financial", "unknown")
```

**Use `workbook_name` to differentiate runs — especially in batch workflows**

When running multiple concurrent comparisons, a descriptive `workbook_name` makes it easier to match `report_id` values to their intended use later. Include the date, the landscape name, and any versioning:

```python theme={"system"}
workbook_name = f"Payments Competitive Landscape — {datetime.today().strftime('%Y-%m-%d')}"
```

***

## 9. Related APIs

<CardGroup cols={3}>
  <Card title="Company Research" icon="building" href="/company-research-doc">
    Generate a single-company deep-dive report — same async workflow pattern, but one company covered in much greater depth.
  </Card>

  <Card title="Industry Research" icon="chart-line" href="/industry-research-doc">
    Generate a sector-level intelligence report — covers market sizing, trends, and transaction activity across an entire industry.
  </Card>

  <Card title="Company Instant Enrichment" icon="bolt" href="/company-instant-enrichment-doc">
    Synchronous firmographic enrichment for a single company — instant, no polling, useful for lightweight data needs.
  </Card>

  <Card title="Company News Monitoring" icon="newspaper" href="/company-news-monitoring-doc">
    Fetch real-time news for any company — use alongside a Peer Comparison report to layer in the latest competitive developments.
  </Card>

  <Card title="Target Screening" icon="crosshairs" href="/target-screening-doc">
    Identify and score acquisition targets across a market — use before Peer Comparison to narrow down which companies to benchmark.
  </Card>

  <Card title="Supporting APIs" icon="wrench" href="/supporting-apis-doc">
    Report Status, Download Report, and File Upload — all used alongside Peer Comparison in the async workflow.
  </Card>
</CardGroup>
