# Trayo API

> Track companies (accounts), define what counts as an event (signals), run discovery and read the events it finds; find companies and people you do not have yet; look up emails and phones; research a company or a person; ask a question about someone you hold; keep working sets in lists.

REST over HTTPS, JSON in and out. Every route below is under `https://api.trayo.ai`. Read the full reference before writing a client: every route, every field and every error code is in it, and several routes have an asynchronous or non-paginating shape that a guess gets wrong.

## Docs

- [Full reference, as Markdown](https://api.trayo.ai/llms-full.txt): everything below, in one file
- [OpenAPI document](https://api.trayo.ai/openapi.json): the same reference as OpenAPI 3.1 JSON (YAML at https://api.trayo.ai/v1/openapi.yaml)
- [Recipes](https://api.trayo.ai/v1/recipes): the call sequences that make up a task, as JSON
- [Rendered reference](https://api.trayo.ai/docs): the same document as a web page, for people
- [Claude Code / MCP plugin](https://github.com/trayoai/trayo-plugin): connect this API to Claude Code, Cowork, Codex and other MCP clients

## Connect Trayo to an agent

Asked to set up or connect Trayo in Claude Code, Codex, Cursor or another agent? Follow the steps in https://api.trayo.ai/agent-install.txt. It connects the Trayo MCP server with the sign-in the user completes in their browser, confirms the workspace, and sets up a new workspace. Everything below is for scripts and integrations that call the REST API directly.

## Authentication

`Authorization: Bearer <your-api-key>` on every route except this file, the documents above and `GET /`. The key identifies your workspace and the user it acts as; there is no workspace id to send. Start with `GET https://api.trayo.ai/v1/whoami`: it confirms the key and returns the plan, the lookup limits.

## Rate limits

Per workspace, in two rolling 60-second buckets: a workspace starts with 120 requests per minute for the API and 7 per minute for the routes that do the most work per call (`POST /v1/discoveries`, `POST /v1/find`, `POST /v1/accounts/lookalikes`, `POST /v1/companies/competitors`, `POST /v1/posts/search`, `POST /v1/posts/search/keywords`, `POST /v1/research/person/batch`, `POST /v1/stakeholders/search/batch`, `POST /v1/people/enrich/batch`). REST, MCP, all API keys and OAuth users share these buckets; adding credentials does not increase throughput. Support can adjust these defaults for a workspace. Every authenticated response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` for the bucket it was counted in, so read your own budget from the calls you are already making. Over the limit is `429 rate_limited` with `Retry-After` in seconds; wait, then resend the same request. Per-network transport protection also runs before the key is read, and it is the only limit on the unauthenticated `/v1` documents: its 429 carries `Retry-After` and no `RateLimit-*` headers. `GET https://api.trayo.ai/v1/whoami` is limited like every other route, so a poll on a timer competes with the work it is watching. Fanning out is what hits the limit: N concurrent calls are N requests in one window.

Workspace usage enforcement is configurable. With enforcement off, usage is metered without balance cutoffs, daily caps or lookup quotas. With enforcement on, configured usage limits apply. `GET /v1/whoami` (MCP: `trayo_whoami`) reports `null` lookup allowances when no cap applies. Workspace request pacing, authentication, permissions, lifecycle and validation still apply.

## Adding accounts

When calling `POST /v1/accounts` or its batch form, send `linkedinHandle` whenever you know the LinkedIn company page. Use the bare handle (for example `stripe`), not the full URL: it is the strongest way to identify the company and is tried before the website `url`, then the name. A correct handle at create time makes a `company_not_found` result less likely.

## Recipes

The order of calls for each task. Details, including what each step returns, are in the full reference.

- **First three calls with a new key** (`start-here`): You have a key and nothing else. Confirms the key works, learns which workspace and plan it acts in, and sets the two settings every stakeholder-finding route reads. Steps: GET /v1/whoami → GET /v1/workspace → PATCH /v1/workspace → GET /v1/recipes
- **Run discovery for companies and read their events** (`discover-account-events`): You have a list of companies and want events — news, hiring, job changes — matched against signals you define. This is the core loop of the API. Import accounts and create signals once, then save the account IDs and signal keys. To automate later checks, have a scheduler you control call `POST /v1/discoveries` with those saved values on each execution, wait for the run to settle, and read its events. Saving accounts or signals does not schedule a run in an API-only workspace. Steps: POST /v1/accounts/batch → POST /v1/signals → POST /v1/discoveries → GET /v1/discoveries/{runId} → GET /v1/events
- **Find companies like these** (`lookalike-companies`): You have example companies and want a ranked list of similar companies. Steps: POST /v1/accounts/lookalikes
- **Rank a company's competitors** (`competitor-roster`): You want a short, ranked list of who a company competes with, with the reason for each row. Steps: POST /v1/companies/competitors
- **Find companies or people you do not have, and keep the good ones** (`prospect-to-list`): You are starting from a description ("Series B fintechs in Europe", "heads of data at logistics companies") rather than from records you already hold. Steps: POST /v1/find → POST /v1/accounts/batch → POST /v1/people/batch → POST /v1/lists → POST /v1/lists/{id}/members
- **Find the right people at one company and get their contact details** (`stakeholders-at-a-company`): You know the company and need the people who own or buy what you sell, with an email or phone to reach them. Steps: POST /v1/research/company → POST /v1/stakeholders/search → POST /v1/people/batch → POST /v1/people/{id}/enrich → GET /v1/people/{id}/enrich
- **One person's email or phone, without adding them to the workspace** (`contact-without-importing`): You have a name and a LinkedIn profile or a company website, and want contact details for someone who does not need to become a record. Steps: POST /v1/contacts/lookup → GET /v1/contacts/lookups/{lookupId}
- **What to know about a person before writing to them** (`brief-before-outreach`): You are about to reach out and want the angle, the likely challenges, and your own history with them. Steps: POST /v1/research/person → POST /v1/people/{id}/ask
- **Companies and people by exact criteria** (`exact-prospecting`): Your target is a WHERE clause — an industry, a headcount band, a country, a founded year, a title — and you want all available matches, paginated. Same route as `prospect-to-list`. send `filters` instead of (or as well as) `query`. Recorded funding rounds stopped updating in November 2025 and are not a filter. For companies that raised money, use a discovery `news` signal, such as `"detects": "The company raised a funding round"`. Steps: GET /v1/find/industries → POST /v1/find → POST /v1/find → POST /v1/accounts/batch → POST /v1/people/batch
- **People who just changed company** (`recent-movers`): A champion left a customer, or a competitor's people are moving — you want who moved where in the last 90 days. Or you want everyone who recently started a role, such as a new CISO, at any company. Steps: POST /v1/job-changes/search → POST /v1/people/batch → POST /v1/people/{id}/enrich
- **Each run: new companies, their recent signals, a few people each, their emails** (`signal-triggered-prospecting`): Before you write any code, ask the user every question in `ask` and wait for the answers. If you cannot ask, start your reply with your guess for each one, then build. You are building an app or script that, when the user starts it or their own scheduler triggers it, takes a batch of companies the workspace does not hold yet, checks them for signals over a recent window, and returns a few people at each company that fired, with work emails. The chain is find, add companies, discover, read events, search stakeholders, add people, look up emails. Steps: GET /v1/whoami → GET /v1/signals → POST /v1/find → POST /v1/accounts/batch → POST /v1/discoveries → GET /v1/discoveries/{runId} → GET /v1/events → POST /v1/stakeholders/search → POST /v1/people/batch → POST /v1/people/enrich/batch → GET /v1/people/{id}/enrich
- **Map the buying committee at one company** (`buying-committee-for-a-company`): Before you write any code, ask the user every question in `ask` and wait for the answers. If you cannot ask, start your reply with your guess for each one, then build. You are building an app where someone types a company and gets the people who would buy, champion or evaluate what the workspace sells, each with a reason, and optionally their emails. Works for any company, whether the workspace holds it or not. Steps: GET /v1/whoami → GET /v1/workspace → POST /v1/research/company → POST /v1/stakeholders/search → POST /v1/accounts/batch → POST /v1/people/batch → POST /v1/people/enrich/batch → GET /v1/people/{id}/enrich

## Reading a search result

`POST /v1/find` is metered per search, and a search that RAN counts against your quota whether or not its results were useful. Three fields on the response say why a search came back thin. Read them before sending the same words again.

- `interpreted` — how Trayo interpreted your sentence. A criterion missing from `interpreted.filters` was NOT enforced; resend that object as `filters` to make the search exact and repeatable. Null on a filters-only search.
- `truncated` — more matched than this search can show. Narrow the request; do not page deeper.
- `degraded` — the search was incomplete, so a short page does not prove there are no other matches. Refine or wait rather than spend another search on the same words. Always false on a filters-only search.

Most thin results are an unqualified sentence rather than an empty data: "GPU cloud and AI compute providers" comes back dominated by companies over 27,000 people, because nothing in it says how big. Say "startup", or send a `filters.headcount` band, and the same search returns the companies you meant.

## Endpoints

### Accounts

- `POST /v1/accounts/lookalikes`: Find companies similar to a list of seed companies Permissions: `accounts:read`.
- `POST /v1/companies/competitors`: Rank the competitors of one company Permissions: `accounts:read`.
- `GET /v1/accounts`: List accounts Permissions: `accounts:read`.
- `POST /v1/accounts`: Create an account Permissions: `accounts:write`.
- `POST /v1/accounts/batch`: Batch create accounts Permissions: `accounts:write`.
- `GET /v1/accounts/{id}`: Get an account Permissions: `accounts:read`.
- `PATCH /v1/accounts/{id}`: Update an account Permissions: `accounts:write`.
- `DELETE /v1/accounts/{id}`: Delete an account Permissions: `accounts:delete`.

### Signals

- `GET /v1/signals`: List signals Permissions: `events:read`.
- `POST /v1/signals`: Create a signal Permissions: `settings:write`.
- `GET /v1/signals/{signalKey}`: Get a signal Permissions: `events:read`.
- `PATCH /v1/signals/{signalKey}`: Update a signal Permissions: `settings:write`.
- `DELETE /v1/signals/{signalKey}`: Delete a signal Permissions: `settings:write`.

### Discoveries

- `GET /v1/discoveries`: List discovery runs Permissions: `events:read`.
- `POST /v1/discoveries`: Start a discovery run Permissions: `events:write`.
- `GET /v1/discoveries/{runId}`: Get a discovery run Permissions: `events:read`.

### Events

- `GET /v1/events`: List events Permissions: `events:read`.
- `GET /v1/events/{id}`: Get an event Permissions: `events:read`.
- `DELETE /v1/events/{id}`: Delete (dismiss) an event Permissions: `events:write`.

### People

- `GET /v1/people`: List people Permissions: `people:read`.
- `POST /v1/people`: Create a person Permissions: `people:write`.
- `POST /v1/people/batch`: Batch create people Permissions: `people:write`.
- `GET /v1/people/{id}`: Get a person Permissions: `people:read`.
- `PATCH /v1/people/{id}`: Update a person Permissions: `people:write`.
- `DELETE /v1/people/{id}`: Delete a person Permissions: `people:delete`.
- `POST /v1/people/enrich/batch`: Look up emails and phones for many people Permissions: `body.fields` includes `email`: `people:enrich_email`; `body.fields` includes `phone`: `people:enrich_phone`. All scopes for the selected fields are required. Omitting `body.fields` uses `["email","phone"]` and requires `people:enrich_email` and `people:enrich_phone`. Malformed values: no additional scopes; unknown fields add no scopes. Request validation still applies.
- `GET /v1/people/{id}/enrich`: Read the contact lookup state Permissions: `people:read`.
- `POST /v1/people/{id}/enrich`: Look up an email and/or phone Permissions: `body.fields` includes `email`: `people:enrich_email`; `body.fields` includes `phone`: `people:enrich_phone`. All scopes for the selected fields are required. Omitting `body.fields` uses `["email","phone"]` and requires `people:enrich_email` and `people:enrich_phone`. Malformed values: no additional scopes; unknown fields add no scopes. Request validation still applies.
- `POST /v1/people/{id}/ask`: Ask a question about a person Permissions: `people:read`.
- `POST /v1/job-changes/search`: Find recent company moves Permissions: `people:read`.
- `POST /v1/posts/search`: Find LinkedIn posts by topic or author Permissions: `people:read`.
- `POST /v1/posts/engagers`: List the author and commenters of a LinkedIn post Permissions: `people:read`.
- `POST /v1/posts/search/keywords`: Search stored LinkedIn posts with explicit keyword groups Permissions: `people:read`.
- `POST /v1/stakeholders/search`: Find the stakeholders at a company that fit a definition Permissions: `research:trigger`.
- `POST /v1/stakeholders/search/batch`: Find the stakeholders at up to 50 companies in one call Permissions: `research:trigger`.
- `POST /v1/contacts/lookup`: Find an email or phone for a person you describe Permissions: `body.fields` includes `email`: `people:enrich_email`; `body.fields` includes `phone`: `people:enrich_phone`. All scopes for the selected fields are required. Omitting `body.fields` uses `["email"]` and requires `people:enrich_email`. Malformed values: no additional scopes; unknown fields add no scopes. Request validation still applies.
- `GET /v1/contacts/lookups/{lookupId}`: Collect a lookup that was still running Permissions: `people:enrich_phone`.

### Lists

- `GET /v1/lists`: List lists Permissions: `people:read`.
- `POST /v1/lists`: Create a list Permissions: `people:write`.
- `GET /v1/lists/{id}`: Get a list Permissions: `people:read`.
- `PATCH /v1/lists/{id}`: Rename a list Permissions: `people:write`.
- `DELETE /v1/lists/{id}`: Delete a list Permissions: `people:write`.
- `GET /v1/lists/{id}/members`: List members Permissions: `people:read`.
- `POST /v1/lists/{id}/members`: Add members Permissions: `people:write`.
- `DELETE /v1/lists/{id}/members/{memberId}`: Remove a member Permissions: `people:write`.

### Find

- `POST /v1/find`: Find companies or people not yet in your workspace Permissions: `body.target` equals `people`: `people:read`; `body.target` equals `companies`: `accounts:read`. Other or missing values: no additional scopes; request validation still applies.
- `GET /v1/find/industries`: The industry taxonomy `filters.industries` accepts Permissions: None beyond a valid API key.

### Workspace

- `GET /v1/workspace`: Read workspace onboarding assumptions and settings Permissions: `accounts:read`.
- `PATCH /v1/workspace`: Correct workspace onboarding assumptions and settings Permissions: `settings:write`.
- `GET /v1/whoami`: Who this key is: permissions, workspace and limits Permissions: None beyond a valid API key.

### Research

- `POST /v1/research/company`: A quick overview of a company: what it does, why now, and fit Permissions: `research:trigger`.
- `POST /v1/research/person`: A quick overview of a person: career, likely challenges, and angles Permissions: `research:trigger`.
- `POST /v1/research/person/batch`: A quick overview for up to 10 people in one call Permissions: `research:trigger`.

### Meta

- `GET /`: Where the documentation is and how to authenticate
- `GET /v1/recipes`: The call sequences that make up a task
- `GET /v1/openapi.yaml`: This document
- `GET /openapi.json`: This document, as JSON
- `GET /.well-known/openapi`: This document, at its well-known URI
