People

Batch create people

Required permissions: people:write.

Create up to 200 people in one call — the import path for bulk results (e.g. from POST /v1/find). A row is the same body as POST /v1/people, note included, so per-row sourcing ("found by search X, unverified") travels with the import instead of being flattened into title. Each row supports the same accountId/company auto-link as POST /v1/people, INCLUDING minting an account from a company name with no domain. That is the common case when piping the find contacts lane straight in here: those rows carry a company name and no domain, and each one now lands in an account rather than in skipped.

Every new row is enriched from its LinkedIn identity, exactly as on POST /v1/people: a new row that carries a linkedinUrl or linkedinUsername and leaves title, photoUrl or the account link empty has them filled from the profile Trayo has on file, and can land in an account it never named. Values you send are never overwritten, and a lookup that misses or runs long costs the row nothing. Strong-key upserts write only fields the row sent; they never copy these profile values onto the match.

Partial success is the contract. A malformed row, a row whose accountId/company cannot be resolved, or a name-only row that collides with another row in the SAME batch or with a person 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. An array over the 200-item cap ERRORS rather than clamping.

Sending the same batch twice creates nothing the second time. The same linkedinUsername or email (case-insensitive) updates the existing row with the fields this row sent. Those rows are returned in updated, with their submitted index; created contains new records only. Inside an account, a match on fullName alone is not safe enough to merge, so it stays in skipped as a duplicate and nothing is changed. The scope is the same rule as POST /v1/people: within the account the row NAMED (accountId or company), or across the whole workspace for a row that named neither. Name-only duplicates carry reasonCode: "duplicate" and existingId. Strong-key updates work for visible and hidden profiles alike and make the record rep-visible. If LinkedIn and email point at different existing people, the row is skipped with reasonCode: "identity_conflict" instead of fusing those identities. Send one identity, or PATCH the intended record explicitly. The same person under two DIFFERENT accounts you named is two records and both are created; a row with no account and neither a LinkedIn nor an email has nothing to match on and is always created.

fullName is the third key, and it is BATCH-ONLY. It applies only to a row that resolves to an account — an accountId, or a company that matched one you already have. Two people at ONE company sharing a full name is rare enough to be worth catching, whereas a bare name across a whole workspace collides too often to dedup on, so a row with no account is still matched on linkedinUsername/email alone and is created when it has neither. fullName is required, so a row that DOES name an account always carries this key: such a row can come back as a duplicate even when its email or linkedinUsername is one you have never seen. Read existingId before assuming the two rows are the same person. A single POST /v1/people never applies this key and still 201s on a name that already exists.

linkedinUrl carries identity here too. A row with no linkedinUsername takes one from linkedinUrl, and that derived username is both what is stored and what the row is deduplicated on — so a POST /v1/find contacts import is idempotent on replay rather than landing a fresh copy of every contact.

It is still best-effort, and here is the edge. The check happens in APPLICATION CODE, the same posture as POST /v1/accounts/batch and for the same reason: no unique database index backs it. Two batches running CONCURRENTLY can both pass the check for the same person before either has committed, and both create one. Serialize your batch calls if you cannot tolerate that.

post/v1/people/batch

Request

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

Request body

Response

The request was processed. Check skipped — a 200 does not mean every row was created or updated; it means the call itself succeeded. Read created for new records and updated for strong-key upserts. Then check degraded: those rows WERE created, but without an account (no_account) and/or without a LinkedIn/email identity (no_identity), and a row with no identity is one a replay of this batch will create again.

Changes

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