---
title: "Rank the competitors of one company"
method: POST
path: "/v1/companies/competitors"
tags: ["Accounts"]
---

# Rank the competitors of one company

`POST /v1/companies/competitors`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/companies/competitors`
- Auth: HTTP bearer

**Required permissions:** `accounts:read`.

Send one company ({ self: true } for your own workspace, or companyId / linkedinUrl / website / name) and get back a ranked roster, default 15 and at most 50. Rank 1 is your first pin when you send pins, else the category incumbent, else the closest match on product and buyer. Candidates are drawn from companies similar to the subject plus companies named while the roster is generated; each is judged direct, adjacent or not a competitor, with 0..100 product and ICP alignment.

The roster is stored per workspace and subject. A repeat call with the same pins and an equal or smaller cap returns the stored roster (reused: true) and does not generate a new one, so a changed positioning has no effect until you send refresh: true. Sending pins replaces the stored pins for that subject; a new roster is generated when the pin set changes. Sending pins stores them for the workspace under the same accounts:read scope. Synchronous. Generation typically takes 10 to 25 seconds. Allow more than 120 seconds for the client timeout.

## Request body

- CompetitorRosterBodyDto
  - `company` union, required: The subject whose competitors you want.
    - object
      - `self` true, required: The calling workspace's own company, resolved from its website.
    - object
      - `companyId` union: Exact company dataset ID, as a number or numeric string. Reuse companyId from a lookalike result or id from a known company find result. This is not a workspace account UUID. When supplied, it takes precedence; a missing ID is reported as not_found without trying other fields.
        - integer
        - string
      - `linkedinUrl` string: The company's LinkedIn page — the `linkedinUrl` a find result or a person row carries, copied through (`https://www.linkedin.com/company/zapier`); the bare handle (`zapier`) is accepted too. Tried first when present, and the most precise key there is. Showcase pages are not company pages: send the company's own. Copy the handle from a Trayo result or the company's own site rather than guessing it from the name — a slug that belongs to a different company resolves to that company.
      - `website` string: The company's own website, as a URL or a domain (`https://zapier.com`, `zapier.com`). The strongest key after the LinkedIn page: it is matched against our data, then, on a miss, followed through a cross-domain redirect, so a legacy or vanity domain that forwards to the current site still resolves. Sent on its own as the bare site (no path or subdomain), a data or social platform's domain names the platform itself (`zoominfo.com` is ZoomInfo); a page on such a site (`linkedin.com/company/acme`) is not a company website. When our data files the company under a different website, a website sent on its own is also tried as the LinkedIn page it spells (`gong.io` as `gong-io`), accepted only for an established company whose name matches the domain. An alias that does not forward to the company's current site is a miss: send the current website, the LinkedIn page or the name. Does not have to be in your workspace.
      - `name` string: The company name. Tried last and the weakest key, since a common name can match the wrong company — send the website when you have it. A name-only lookup finds only companies Trayo holds under that name; an employer that a Trayo person row names with `companyId: null` has no company record, and its name will usually miss.
  - `pins` object[]: Competitors you already know, most important first; the first is rank 1. Sending this REPLACES the stored pins: omit it to keep them, send [] to clear them, and to add a competitor send the current pins with it. Pins are also used as extra seeds, so changing them regenerates the roster.
    - `companyId` union: Exact company dataset ID, as a number or numeric string. Reuse companyId from a lookalike result or id from a known company find result. This is not a workspace account UUID. When supplied, it takes precedence; a missing ID is reported as not_found without trying other fields.
      - integer
      - string
    - `linkedinUrl` string: The company's LinkedIn page — the `linkedinUrl` a find result or a person row carries, copied through (`https://www.linkedin.com/company/zapier`); the bare handle (`zapier`) is accepted too. Tried first when present, and the most precise key there is. Showcase pages are not company pages: send the company's own. Copy the handle from a Trayo result or the company's own site rather than guessing it from the name — a slug that belongs to a different company resolves to that company.
    - `website` string: The company's own website, as a URL or a domain (`https://zapier.com`, `zapier.com`). The strongest key after the LinkedIn page: it is matched against our data, then, on a miss, followed through a cross-domain redirect, so a legacy or vanity domain that forwards to the current site still resolves. Sent on its own as the bare site (no path or subdomain), a data or social platform's domain names the platform itself (`zoominfo.com` is ZoomInfo); a page on such a site (`linkedin.com/company/acme`) is not a company website. When our data files the company under a different website, a website sent on its own is also tried as the LinkedIn page it spells (`gong.io` as `gong-io`), accepted only for an established company whose name matches the domain. An alias that does not forward to the company's current site is a miss: send the current website, the LinkedIn page or the name. Does not have to be in your workspace.
    - `name` string: The company name. Tried last and the weakest key, since a common name can match the wrong company — send the website when you have it. A name-only lookup finds only companies Trayo holds under that name; an employer that a Trayo person row names with `companyId: null` has no company record, and its name will usually miss.
  - `cap` integer: Maximum roster length. Default 15, maximum 50.
  - `positioning` string: What the subject sells and to whom. Defaults to the workspace's own positioning for { self: true }, else the company's public description. It is read only while a roster is being generated: a repeat call with the same pins and an equal or smaller cap returns the stored roster and ignores a changed positioning, so send refresh: true to apply one.
  - `refresh` boolean: Regenerate even when a stored roster with the same pins is available.

## Response `200`

- CompetitorRosterResponseDto
  - `subject` object, required
    - `companyId` integer, required
    - `name` string, required
    - `website` string, nullable, required
    - `linkedinUrl` string, nullable, required
    - `resolvedBy` string, required: Which key identified the subject: linkedin, website, redirect, name, or company_id for an exact lookup.
    - `indexed` boolean, required: false when similarity data is unavailable for the subject; the roster then comes from pins and nominations only.
  - `pins` object[], required: One entry per stored pin, in rank order.
    - `input` object, required
      - `companyId` union: Exact company dataset ID, as a number or numeric string. Reuse companyId from a lookalike result or id from a known company find result. This is not a workspace account UUID. When supplied, it takes precedence; a missing ID is reported as not_found without trying other fields.
        - integer
        - string
      - `linkedinUrl` string: The company's LinkedIn page — the `linkedinUrl` a find result or a person row carries, copied through (`https://www.linkedin.com/company/zapier`); the bare handle (`zapier`) is accepted too. Tried first when present, and the most precise key there is. Showcase pages are not company pages: send the company's own. Copy the handle from a Trayo result or the company's own site rather than guessing it from the name — a slug that belongs to a different company resolves to that company.
      - `website` string: The company's own website, as a URL or a domain (`https://zapier.com`, `zapier.com`). The strongest key after the LinkedIn page: it is matched against our data, then, on a miss, followed through a cross-domain redirect, so a legacy or vanity domain that forwards to the current site still resolves. Sent on its own as the bare site (no path or subdomain), a data or social platform's domain names the platform itself (`zoominfo.com` is ZoomInfo); a page on such a site (`linkedin.com/company/acme`) is not a company website. When our data files the company under a different website, a website sent on its own is also tried as the LinkedIn page it spells (`gong.io` as `gong-io`), accepted only for an established company whose name matches the domain. An alias that does not forward to the company's current site is a miss: send the current website, the LinkedIn page or the name. Does not have to be in your workspace.
      - `name` string: The company name. Tried last and the weakest key, since a common name can match the wrong company — send the website when you have it. A name-only lookup finds only companies Trayo holds under that name; an employer that a Trayo person row names with `companyId: null` has no company record, and its name will usually miss.
    - `company` object, nullable, required
      - `companyId` integer, required
      - `name` string, required
      - `website` string, nullable, required
    - `status` 'resolved' | 'not_found', required
    - `seeded` boolean, required: true when this pin seeded the similarity search in the run that produced this response; false on a reused roster.
  - `entries` object[], required: The roster, rank 1 first.
    - `rank` integer, required
    - `companyId` integer, required
    - `name` string, nullable, required
    - `website` string, nullable, required
    - `linkedinUrl` string, nullable, required
    - `industry` string, nullable, required
    - `sizeRange` string, nullable, required
    - `verdict` 'direct' | 'adjacent' | 'not_competitor', required
    - `productAlignment` integer, required
    - `icpAlignment` integer, required
    - `reason` string, required
    - `source` 'pool' | 'pin' | 'nomination', required: pool: retrieved from similarity data. pin: you named it. nomination: named while the roster was generated, then matched to a company.
    - `rankReason` 'pinned' | 'incumbent' | 'alignment', required
    - `similarity` number, nullable, required
    - `seedsMatched` integer, nullable, required
    - `customers` object, required: The customers this competitor shows on its own website.
      - `status` 'not_scanned' | 'pending' | 'running' | 'completed' | 'failed', required: Each roster call starts a background scan when the scan is missing or more than 30 days old. It takes a few minutes; call again to see it fill in. An entry with no website stays not_scanned.
      - `scrapedAt` string, nullable, required: ISO 8601; when the competitor's site was last read.
      - `pagesScraped` integer, required
      - `customersFound` integer, required
      - `customersResolved` integer, required
      - `error` 'no_pages_fetched' | 'queue_unavailable' | 'scan_failed', nullable, required: Why the last scan failed, or null. no_pages_fetched: none of the competitor's pages could be read. queue_unavailable: the scan could not be started. scan_failed: any other failure. A failed scan is tried again on a roster request made at least 24 hours later.
      - `customers` object[], required
        - `name` string, required
        - `domain` string, nullable, required
        - `companyId` integer, nullable, required: Set when the name or domain matched a company in our data.
        - `evidenceKind` 'logo' | 'case_study' | 'testimonial' | 'review' | 'customer_list', required: Where on the competitor's site the customer appeared.
        - `sourceUrl` string, required
        - `quote` string, nullable, required: The sentence or heading that named the customer, at most 240 characters.
        - `confidence` 'high' | 'medium' | 'low', required
        - `flag` 'self' | 'tenant', nullable, required: self: the competitor named itself. tenant: it named the company this roster is for.
  - `unresolvedNominations` object[], required: Companies named while the roster was generated that matched nothing in our data. Not in the roster.
    - `name` string, required
    - `website` string, nullable, required
  - `cap` integer, required
  - `generatedAt` string, required: ISO 8601.
  - `reused` boolean, required: true when the stored roster was returned without generating a new one.
  - `stats` object, required

## Other responses

- `400`: `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `404`: `company_not_found` (`retry: do_not_retry`) — We could not identify this company — not from the account's own record, nor from a LinkedIn page, website (including its redirect), or name — so there are no people to search. Retrying will not help. Send the company's own website if you did not; for any company, `POST /v1/find` on the contacts lane searches the web instead. From `POST /v1/find` with `filters.companyWebsites`, this means NO website resolved and nothing was searched; the message names every website. For one that is ambiguous (several possible companies on the domain) it also names the candidates and their ids: send the right id as `filters.companyIds`. When only some websites fail, the search runs over the rest and lists the others in `unresolvedWebsites`.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.
- `503`: `competitor_roster_not_configured` (`retry: do_not_retry`) — Competitor rosters are unavailable in this environment. Retrying will not help. Contact support. `competitor_roster_failed` (`retry: retry_later`) — Competitor roster generation failed or exceeded its 45 second budget. Nothing was saved; the previous roster, if any, is unchanged. Retry the same request shortly. `company_resolution_timed_out` (`retry: retry_later`) — Resolving the company by name timed out under load before we could answer. This is NOT a "we do not have it" result: nothing was recorded, and the same request sent again shortly will usually resolve. Companies sent by website or LinkedIn page do not take this path.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
