Batch create accounts
Required permissions: accounts:write.
Create up to 200 accounts in one call — the import path for bulk results (e.g. from POST /v1/find), where creating one account per HTTP call is the wrong shape. Every created account is assigned to the user your API key acts as, and starts becoming searchable, exactly like POST /v1/accounts. Read that route's description for what is and is not true of a new account; it applies to every row created here. Like that route, this call adds accounts only, never people.
This call does not wait for the background work each new account needs. That work is started without waiting for it, so 200 rows cost 200 inserts plus one website check that is capped at a few seconds; the accounts become searchable by jobs and job_change over the following minutes, and a discovery run tells you which are not ready yet rather than silently returning nothing for them.
Each created row carries a prediction of which company it is. Before answering, the websites of the created rows are checked together, and each row in created carries identity: resolved (with the company in matchedCompany), uncertain (several possible companies on that website, listed in alternatives), not_found (no company for that website: discovery will fail with company_not_found), or pending (no url, a linkedinHandle the website did not confirm, or the check did not answer in time; these are identified in the background, and a discovery run reports any that fail). Fix an uncertain or not_found account with PATCH /v1/accounts/{id} (its own website or its linkedinHandle), or delete it, before running discovery. identity is a prediction from the website: discovery runs its own match for each new account afterwards, and that match can land on a different company or on none. A discovery run lists any account it could not identify in blockedSignals; over MCP, trayo_get_account_identity reads the final result. Skipped rows carry none of this. These fields are on this response only, not on the account itself.
Project a find result before posting it — it is not accepted verbatim. A find company row carries id, website, linkedinUrl, industry, headcount, hq, fundingStage, foundedYear, companyType, description, why, known, state alongside name and url, this route accepts none of them, and an unprojected batch therefore lands EVERY row in skipped (the reason names the keys). The import that works:
curl -s .../v1/find -d '{"target":"companies","query":"..."}' | jq '{accounts: [.data[] | select(.state == "selectable" and .url) | {name, url}]}' \
| curl -s .../v1/accounts/batch -H 'content-type: application/json' -d @-
The select(.state == "selectable") half matters as much as the projection: an alreadyAdded row would be skipped as a duplicate anyway.
Partial success is the contract. A malformed row, or one that collides with another row in the SAME batch or with an account you already have, is reported in skipped by its 0-based index and a reason — never as a request error. This response is always 200, even if every row was skipped. Only a problem with the REQUEST itself fails the call, and an array over the 200-item cap is one of those: it ERRORS rather than clamping, unlike POST /v1/find's limit.
Public REST and MCP usage is recorded without daily usage caps, workspace quota cutoffs or lookup quotas for now. Workspace request pacing still applies. Dedup is best-effort, not a guarantee. A row is a duplicate when its url's hostname matches, lowercased and with a leading www. stripped (https://acme.com and https://www.acme.com/careers collide, and so does a stored value kept without a scheme, acme.com). Failing that, a row is a duplicate when its linkedinHandle matches — so a company you already have under a different domain (a rebrand, a regional TLD, an account whose url is empty) is still recognised. That comparison folds together every shape the handle is stored in: case, percent-encoding (airbus%2Dhelicopters), and a full company URL rather than the bare slug. Last, a row is a duplicate by name, case-insensitively, and how far that key reaches depends on what else the row carries. A row with NEITHER a url NOR a linkedinHandle has only its name, so it is matched against every account in your workspace, those with a website as well as those without — a name-only import is not created twice, and it attaches to the company you already have. A row that DOES carry a url or a handle and missed on it is matched by name only against an account that has NEITHER a usable website NOR a linkedinHandle — the only case where a name is the sole thing the two records share. Two accounts that each have a real website are never matched on name, so two different companies with the same name both land. The reason on the skip names WHICH of the three keys collided.
A duplicate skip still attaches you to the account. The account named by existingId is assigned to the user your key acts as, exactly as a created one is, so importing a company a teammate added already makes it eligible for your discovery runs. Importing a duplicate does not otherwise update the existing account; use PATCH /v1/accounts/{id} to correct its name, url or linkedinHandle.
This check happens in APPLICATION CODE — Account.domain has no unique database index, deliberately, because that migration needs a data backfill of its own. The consequence: two batches racing each other can both pass the duplicate check for the same key before either has committed, and both create an account. This endpoint cannot detect or prevent that race, there is no Idempotency-Key support here (unlike POST /v1/discoveries), and replaying the exact same request is not guaranteed to be a no-op. If your workflow cannot tolerate an occasional duplicate under concurrent imports, serialize your batch calls.
Request
- Base URL: https://api.trayo.ai
- URL: https://api.trayo.ai/v1/accounts/batch
- Auth: HTTP bearer
Request body
Response
The request was processed. Check skipped — a 200 does not mean every row was created; it means the call itself succeeded.
Changes
No recorded changes to this operation across all 1 revision of this API.