Skip to main content

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.

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 endpoint and retrieve the finished report using the Download Report endpoint. The three-step workflow:
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
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 for a full explanation of the polling lifecycle.

2. Quick Start

Step 1 — Submit the report request
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"
  }'
Step 2 — Poll for completion
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
# 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.
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 if you do not yet have API access.
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.

4. Request Reference

Endpoint
POST https://api.wokelo.ai/api/workflow_manager/start/
All parameters are passed as JSON in the request body.
ParameterTypeRequiredDescription
workflowstringRequiredMust 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.
websitesstring[]RequiredArray 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_namestringOptionalLabel 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_filesobject[]OptionalArray 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.
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.
Full request example:
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"
  }'

5. Response

Submission response

The initial POST returns immediately with a single field:
{
  "report_id": 1002345
}
FieldTypeDescription
report_idintegerUnique 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 valueMeaning
"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:
  • organizationname, 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.
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.

Financial field notes

Financial data in the details.financials dict uses a consistent structure:
"Total revenue": {
  "period": "2024-12-31",
  "v": 139236000.0,
  "currency": "USD"
}
Raw values are in the base unit (USD, not millions). To convert:
# 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_typeDescription
"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

# 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"}'
Sample submission response:
{
  "report_id": 1002345
}

Extracting and displaying firmographic data

Parse the firmographics section to build a comparison table of key metrics across all companies.
# 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.
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.
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.
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.
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

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.
StatusMeaningCause & Resolution
200 OKRequest acceptedreport_id returned. Proceed to polling.
400 Bad RequestInvalid parametersMissing workflow, missing or empty websites, fewer than 2 or more than 5 websites, wrong workflow value, or malformed URLs. Check the detail field.
401 UnauthorizedAuth failedThe Authorization header is missing or contains an invalid token. Verify your key in Settings → API Keys.
403 ForbiddenInsufficient accessYour plan does not include access to this endpoint. Contact support@wokelo.ai.
429 Too Many RequestsRate limit exceededImplement exponential back-off on submission. The response includes a Retry-After header.
500 Internal Server ErrorServer errorRetry the submission after a brief delay. If the issue persists, contact support@wokelo.ai.
Handling a "Failed" report status:
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:
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:
# ❌ 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:
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:
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:
# ❌ 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:
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:
workbook_name = f"Payments Competitive Landscape — {datetime.today().strftime('%Y-%m-%d')}"

Company Research

Generate a single-company deep-dive report — same async workflow pattern, but one company covered in much greater depth.

Industry Research

Generate a sector-level intelligence report — covers market sizing, trends, and transaction activity across an entire industry.

Company Instant Enrichment

Synchronous firmographic enrichment for a single company — instant, no polling, useful for lightweight data needs.

Company News Monitoring

Fetch real-time news for any company — use alongside a Peer Comparison report to layer in the latest competitive developments.

Target Screening

Identify and score acquisition targets across a market — use before Peer Comparison to narrow down which companies to benchmark.

Supporting APIs

Report Status, Download Report, and File Upload — all used alongside Peer Comparison in the async workflow.