Discoveries

Start a discovery run

Required permissions: events:write.

ASYNCHRONOUS. Returns 202 immediately with a run whose status is running — nothing has been searched yet. Poll GET /v1/discoveries/{runId} until settledAt is non-null, then read the events that stand for its scope with GET /v1/events?discoveryRunId={runId}. settledAt is set when the run is final — searched AND every event written — and status reads completed or failed at the same instant. candidatesScreened, candidatesMatched and eventsNew are frozen execution counters; the scoped event page holds everything this run wrote — including events dated outside the lookback, as a job change dated by its start month can be — plus matching canonical events written by earlier runs.

Re-running an identical discovery is safe. Results deduplicate against everything already in the workspace, so the same accounts/signals/lookback typically returns eventsNew: 0 the second time and creates no duplicate events. That is success.

Idempotency: send Idempotency-Key on every call you might retry. Repeating the same key with the byte-identical body returns the ORIGINAL run with the same id, without starting another search, but with its CURRENT state, not a frozen copy: if the original has since completed, the replay shows status: completed and real counts. The same key with a DIFFERENT body is a 409. Keys are not scoped to a time window in this version.

A 202 does not guarantee events will follow. If none of the requested accounts are eligible — every account must be assigned to a user, which accounts created via POST /v1/accounts always are — the run still returns 202 and then immediately reads status: completed, eventsNew: 0, with error explaining why. This is the single most likely way a first-time caller sees "no events": check error before assuming discovery is still running or that your signals match nothing.

You can use new accounts and signal keys immediately. Poll the same run until settledAt is set.

Public REST and MCP usage is recorded without daily usage caps, workspace quota cutoffs or lookup quotas for now. Workspace request pacing still applies. People: by default (people: "none") a run adds no people to your workspace — its events come back with people: []. A job_change event still names the person who changed jobs; add them with POST /v1/people if you want them. Send people: "add" to have the people who matter to each event added to your workspace and listed on it.

Read blockedSignals before reporting an empty result. These outcomes apply to every signal type, including news. While a run is active, keep polling the same run. account_not_ready means Trayo could not search the account this time. After the run settles, retry the affected account IDs. company_not_found means Trayo could not identify the company from the supplied details. For company_not_found, check the named accounts before starting another discovery. If an identity is wrong, send the correct linkedinHandle with trayo_update_account over MCP, or with PATCH /v1/accounts/{id} and a workspace API key over REST. When no handle is known, send a corrected website url instead. Both need accounts:write. trayo_update_account also reports whether Trayo can now identify the company. Importing the same hostname again skips the existing account and does not update it. After the current run settles, start a new discovery for the corrected account IDs.

Each blocked entry NAMES the accounts in accountIds (the first 200 of them; accountsAffected is the true total), so a re-run can be scoped to exactly those rather than everything you sent.

post/v1/discoveries

Request

  • Base URL: https://api.trayo.ai
  • URL: https://api.trayo.ai/v1/discoveries
  • Auth: HTTP bearer

Headers

Idempotency-Keystring

Free-form string. Supported on THIS route only — no other /v1 route reads it.

Request body

accountIdsstring[] required

Accounts to scan, 1 to 500. Every id must exist in your workspace — an unknown one is a 400 (unknown_account) listing the offenders, not a silent skip.

lookbackDaysinteger required

How far back to look for events, in days. jobs signals read job postings from about the last 60 days only, whatever this value is: report their period as about 60 days, not the lookback.

maxResultsinteger

Requested source-result cap per account and signal, 1 to 1000, before filtering and deduplication; this is not a target number of matching events. Omit it to use each source's default — 25 for jobs, posts and people searches. A number raises or lowers those defaults. Google News account searches always use a fixed 10-result budget and ignore maxResults. Send a number or omit the field — null is rejected.

signalKeysstring[]

Omit to run every signal in the workspace. You can use a new signal key immediately. A key that does not exist is a different thing and is a 400 (unknown_signal). Send at least one key or omit the field — an EMPTY array is rejected, because it reads like "no signals" and used to mean "all of them". Each selected signal searches its enabled sources for every account. More accounts, signals and source results mean more work and a longer wait. Select only the signal keys relevant to the task; omitting this field can make a large run take much longer.

people'add' | 'none'

none (default): this run adds no people to your workspace; its events come back with people: []. A job_change event still names the person who changed jobs; add them with POST /v1/people if you want them. add: the people who matter to each event are found, added to your workspace and listed on the event's people as the run goes. Find and add people yourself any time with POST /v1/find and POST /v1/people/batch.

Response

Run accepted (or, for a replayed Idempotency-Key, the original run's current state).

idstring uuid required
status'running' | 'completed' | 'failed' required

running, completed or failed. This is the POLLING signal and nothing else: it tells you when to stop calling, not what the run achieved. Read error and partial even on a completed run.

completed means everything this run will ever write has been written. It covers both halves of the run: the search across your accounts, and the step that turns what the search found into events. It flips at the same instant settledAt is set, and from that instant the candidate and event counts are frozen and GET /v1/events?discoveryRunId= returns the events that stand for the run's scope: everything it wrote, at any event date, plus matches an earlier run wrote. There is nothing to wait for afterwards.

running covers the write step too, so a run whose search has finished still reads running while its events are being written. That is why completedAt can be set while status is running: the search is done, the run is not. Poll on settledAt (or on this field reaching completed, which is the same moment) and never on completedAt. A run that finds nothing reaches completed exactly as promptly as one that finds a hundred events.

lookbackDaysnumber required
maxResultsnumber nullable required

The requested source-result cap this run was started with, before filtering and deduplication. null means none was sent, so each source used its default. Google News account searches always use a fixed 10-result budget and ignore maxResults; this value is not an event count.

people'add' | 'none' required

The people option this run was started with: add or none. A run started before the option existed reads add, which is what it did.

accountCountnumber required

How many accounts the run was asked to scan.

candidatesScreenedinteger required

How many candidate items this run RETRIEVED and screened: articles, posts and profiles pulled from the sources for your accounts, before anything judged whether they match a signal. It is not a count of events, and these candidates cannot be read as events. candidatesMatched says how many of them matched a signal, eventsNew how many new events the run wrote, and the events themselves are read with GET /v1/events?discoveryRunId=. candidatesScreened: 12 with candidatesMatched: 0 means 12 items were screened and none matched any signal, so this execution wrote no events and there is nothing of its own to read. The same number as the deprecated eventsFound.

Final once settledAt is set, and still moving before that.

candidatesScreened: 0 on a RE-RUN does not mean nothing was searched. Items a signal has already seen are dropped before they are stored, so a second identical run reports 0 even though the same sources were queried and returned the same items: on any later run this number says how much was NEW to that signal, not how much was looked at. To tell "nothing was searched" from "nothing was new", use blockedSignals and error, which do not decay on re-runs.

candidatesMatchedinteger nullable required

How many of the screened candidates matched a signal and went into an event: a new event, or an event already in your workspace that absorbed them. Several matched candidates can make one event, so this is not an event count either; eventsNew is. candidatesMatched above zero with eventsNew: 0 means what matched was already known: the scoped events from GET /v1/events?discoveryRunId= hold it. The sum of bySignal[].candidatesMatched. Final once settledAt is set. Null only on a run settled before this field existed.

eventsFoundnumber required

Deprecated: read candidatesScreened, the same number under a name that says what it counts. Kept, with the same value, for callers that already read it. It counts candidate items retrieved and screened, NOT events: eventsFound: 12 with no events to read means 12 candidates were screened and none matched (candidatesMatched: 0), not 12 events that are missing.

eventsNewnumber required

How many new events THIS RUN WROTE to your workspace. It is the run's execution receipt, not the size of GET /v1/events?discoveryRunId=. That list answers the customer's question instead: every live event this run wrote, whatever its event date, plus every one standing for the run's accounts, signals and lookback window, including events written by earlier runs. It can therefore be non-empty, or longer than this count, when eventsNew is zero — and on a settled run, read to the end, it is never shorter than this count except where an event has since been withdrawn or deleted. One page is capped at 100, so compare against everything nextCursor yields, never against a single page.

The list can also be shorter. A safety net downstream of discovery can withdraw an event after the run ends, and a caller can delete one; the frozen receipt still says the run wrote it. Explicit state=dismissed reads Trayo's withdrawals in the same scope, while caller-deleted events remain absent.

Act on it once settledAt is set, and not before. Until then the run is still writing events and this number is still rising: a value read from an unsettled run is a partial count, not a small result. Once settledAt is set the number is frozen — it is the run's receipt, so it stays what it was even if you later delete some of the events the run created.

eventsNew: 0 on a completed run is a success, not a failure. Results are deduplicated against everything already in the workspace, so RE-RUNNING AN IDENTICAL DISCOVERY IS SAFE: the same accounts, signals and lookback typically return eventsNew: 0 on the second call and create no duplicate events. Do not treat a zero here as a reason to retry. Read the scoped events page, error, and blockedSignals; do not branch on status alone — a completed run always means the work finished, whatever it found.

partialboolean required

True when the results are INCOMPLETE: at least one account × signal check failed (failedChecks lists them), or the run was closed with work still outstanding (error says what). status stays completed on such a run — it says the run stopped, not that every search succeeded. Read failedChecks for which signals and accounts, and do not report eventsNew: 0 on a partial run as "nothing happened". Can turn true while the run is still going; final once settledAt is set.

retry'retry_later' nullable required

retry_later on a settled partial run whose failures were transient (a timeout or source_unavailable check, or work cut short): re-running the same accounts and signals unchanged, later, is the fix. Null otherwise — a clean run needs no retry, a run still going has not finished failing, and a run whose every failure is internal would fail the same way.

startedAtstring date-time required
completedAtstring date-time nullable required

When the SEARCH finished — every account in the run reported back. It is not the end of the run and it is not what to poll on: the events the search found are written afterwards, so this can be set while status is still running. Wait for settledAt. Null until the search finishes, then stable.

settledAtstring date-time nullable required

THE FINISH LINE — when the run became final: the search finished AND every event it produced has been written. Act when settledAt is set. Poll GET /v1/discoveries/{runId} until this field is non-null; at that moment status reads completed or failed, the candidate and event counts are frozen for good, and GET /v1/events?discoveryRunId= returns every live event this run wrote — at any event date, since a job change is dated by the month the job STARTED and can predate the lookback — plus every one standing for the run's account × signal × lookback scope, including events an earlier run wrote. The counts remain this run's frozen receipt. Do not poll on completedAt, and do not try to detect the end by watching eventsNew stop changing — a pause between two writes looks exactly like the end of the run and is not one. Null until the run settles, then stable.

errorstring nullable required

Read this even when status is completed. A completed run with a non-null error ended without doing everything it was asked to — most commonly because none of the requested accounts was eligible (every account must be assigned to a user), which is the single most likely reason a first-time caller sees no new events. The scoped events page can still contain matches written before this run, so this field remains the authority on whether this execution failed.

Changes

No recorded changes to this operation across all 1 revision of this API.