People

Create a person

Required permissions: people:write.

A person is a name, a title, a note, an email, a LinkedIn username and a photo URL. That is the whole record you can WRITE. Phone numbers are only ever found: POST /v1/people/{id}/enrich looks up an email and/or a phone and the results appear on the person as email (with emailSource: "enrichment") and phone.

note is where sourcing and confidence go. It is free text you own — up to 1000 characters, no structure imposed, stored and returned verbatim and NEVER interpreted by Trayo: not parsed, not indexed, not searchable, not shown in the Trayo UI, and nothing in the product branches on it. "sourced from LinkedIn, unverified, 2026-09-02" is exactly the intended use. Set it here, change or clear it with PATCH /v1/people/{id}, read it back on every people route.

Do not put any of that in title. title is the job title, it is published, and humans and downstream systems read it as a job title — so a value like "VP Sales (UNVERIFIED, sourced from LinkedIn)" is a wrong job title for every reader of it, not an annotated one. That workaround is why note exists.

LinkedIn and email are upsert keys. A create that matches an existing person on either key updates that row with the fields you sent, makes it rep-visible, returns its existing id, and answers 200. A new person answers 201. WHERE Trayo looks depends on what you sent: with an accountId or a company, within THAT account only — the same human under two accounts (an advisor, a dual role) is two records and both are created. With neither, across your whole workspace, because you asked "do you have this person" and the answer must not depend on which account Trayo happens to attach them to. A person with neither key can never upsert. If LinkedIn and email point at two different people, the request is a 409 rather than merging those identities.

linkedinUrl is a source for linkedinUsername, not a decoration. Send either. With no linkedinUsername, the username is taken from the URL and stored, so a POST /v1/find contacts row keeps its LinkedIn identity when posted verbatim. A linkedinUsername you send always wins over the URL.

For a new person, a LinkedIn identity also FILLS the fields you left empty. A new row carrying one is looked up in Trayo's own profile mirror, and whatever it finds is used for title and photoUrl when you sent neither, and for the account link when you sent neither accountId nor company — so the response can carry a title, a profileImageUrl and an accountId you never supplied, and a company Trayo does not already have becomes a NEW account under the same name-only rule below. It never overwrites a value you DID send, and it is best-effort: a lookup that misses or takes too long creates the row exactly as you sent it rather than failing it. Supply the fields yourself to keep control of them; there is no other way to opt out. A strong-key upsert writes only fields you sent; it never copies these profile values onto the match.

Note isRepVisible on the response: it comes back true. Creating a person here counts as a deliberate human add, the same as adding one by hand in the app, so the record surfaces to reps. You cannot send the field and you cannot turn it off.

accountId and company are mutually exclusive ways to link the new person to an account — send at most one, or neither to create an unlinked person.

Company auto-link. company: { name } finds-or-creates the linked account by name, case-insensitive, within your workspace. A match reuses the existing account and company.domain is ignored. company.domain takes a bare host or a full website URL; the account is created at https://<host> either way, and a value that is not a usable hostname is refused — validation_failed, param: "company.domain".

A company that does NOT already exist is created anyway — with an empty url when you sent no company.domain, so a name-only row still gets an account rather than being refused. Supply company.domain when you have one so the new account gets a real url. Look up whether an account already exists, and get its id for the accountId route, with GET /v1/accounts?q=<company name>.

post/v1/people

Request

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

Request body

fullNamestring required
titlestring

The person's JOB TITLE — nothing else. It is a published field: it is shown to humans as a job title and read as one by everything downstream, so a value like "VP Sales (UNVERIFIED, sourced from LinkedIn)" is a wrong job title for every reader of it, not an annotated one.

Sourcing, confidence, dates and "unverified" go in note, which exists for exactly that, holds 1000 characters, and is never interpreted by Trayo.

Omit it and Trayo may fill it for you: a row carrying a LinkedIn identity and no title takes one from the profile Trayo has on file — the current role's title, or the profile headline when there is no current role. Send the field to keep that from happening.

notestring

Free text that is YOURS. Trayo stores it, returns it, and never reads it — it is not parsed, not indexed, not searchable, not shown in the Trayo UI, and nothing in the product branches on it. No structure is imposed: "sourced from LinkedIn, unverified, 2026-09-02" is exactly the intended use.

Yours, not PRIVATE: it is stored on the person record, so anyone with access to your workspace can read it back. Do not put secrets in it.

This is where provenance, sourcing, confidence and "unverified" go. Putting them in title instead corrupts a published field that humans and downstream systems read as a job title.

Up to 1000 characters; longer is validation_failed, not a truncation.

emailstring email
linkedinUsernamestring
accountIdstring uuid

Link to an existing account. MUTUALLY EXCLUSIVE with company — sending both is validation_failed (param: "company"). An id that does not resolve in your workspace is unknown_account.

Send NEITHER this nor company and the person is linked to the employer on their LinkedIn profile when the row carries a LinkedIn identity and Trayo has one on file — creating that account if it is new — and is left unlinked otherwise. Send one of the two to decide the account yourself.

photoUrlstring

The person's profile image URL. For a new person, omit it on a row that carries a LinkedIn identity and Trayo fills it from the profile it has on file, self-hosting the image; a value you send is kept as-is.

idstring

Accepted and IGNORED — declared only so a POST /v1/find contacts result can be posted here verbatim. Never persisted or echoed back.

linkedinUrlstring

A LinkedIn PROFILE URL (https://www.linkedin.com/in/ada-lovelace), accepted so a POST /v1/find contacts result can be posted here verbatim.

It is USED, not ignored: when linkedinUsername is absent, the username is taken from this URL and stored. linkedinUsername WINS when both are present and they disagree — this field never overwrites one you sent.

Country subdomains work (https://il.linkedin.com/in/ada-lovelace), as does a bare username. A LinkedIn URL that is not a personal profile — a /company/ page, the feed — yields no username and is IGNORED rather than rejected, so one such row cannot fail an otherwise good import; the person is created with linkedinUsername: null.

The URL itself is not stored and does not read back: linkedinUsername is the field this API keeps, and it is what GET /v1/people returns.

For a new person with no strong-key match, it also FILLS the fields you left empty. Sending a LinkedIn identity and no title, no photoUrl, or neither an accountId nor a company makes Trayo look the person up in its own profile mirror and use what it finds for exactly those fields — so the response can carry a title, a profileImageUrl and an accountId you never sent, and a company Trayo has never seen becomes a NEW account (with an empty url, like any name-only company). It never overwrites a value you did send, and it is best-effort: a miss, or a lookup that takes too long, creates the new row exactly as you sent it rather than failing it. There is no way to opt out other than by supplying the fields yourself.

statestring

Accepted and IGNORED — declared only so a POST /v1/find contacts result can be posted here verbatim. Never persisted or echoed back.

Response

Existing person updated.

idstring uuid required
accountIdstring uuid nullable required
fullNamestring required
titlestring nullable required

The person's JOB TITLE — nothing else. It is a published field: it is shown to humans as a job title and read as one by everything downstream, so a value like "VP Sales (UNVERIFIED, sourced from LinkedIn)" is a wrong job title for every reader of it, not an annotated one.

Sourcing, confidence, dates and "unverified" go in note, which exists for exactly that, holds 1000 characters, and is never interpreted by Trayo.

Omit it and Trayo may fill it for you: a row carrying a LinkedIn identity and no title takes one from the profile Trayo has on file — the current role's title, or the profile headline when there is no current role. Send the field to keep that from happening.

notestring nullable required

Free text that is YOURS. Trayo stores it, returns it, and never reads it — it is not parsed, not indexed, not searchable, not shown in the Trayo UI, and nothing in the product branches on it. No structure is imposed: "sourced from LinkedIn, unverified, 2026-09-02" is exactly the intended use.

Yours, not PRIVATE: it is stored on the person record, so anyone with access to your workspace can read it back. Do not put secrets in it.

This is where provenance, sourcing, confidence and "unverified" go. Putting them in title instead corrupts a published field that humans and downstream systems read as a job title.

Up to 1000 characters; longer is validation_failed, not a truncation.

null when you have never set one. It reads back exactly as you wrote it — Trayo does not normalize, trim or rewrite it, and nothing in the product will ever change it.

emailstring nullable required

The email to use. A value you wrote (here or in the app) always wins; otherwise the one POST /v1/people/{id}/enrich found; otherwise null. emailSource says which. A found email that later bounced is withheld — see GET /v1/people/{id}/enrich for the reason.

emailSource'manual' | 'enrichment' nullable required

manual = written by a caller or in the app; enrichment = found by a lookup.

phonestring nullable required

The phone number a lookup found, or null. Phones are only ever FOUND — there is no way to write one — so this is always a found value. Ask for one with POST /v1/people/{id}/enrich, or with trayo_enrich_phones on the MCP server.

linkedinUsernamestring nullable required
linkedinUrlstring nullable required

Their LinkedIn profile URL, when Trayo has it. Send it to POST /v1/contacts/lookup, POST /v1/research/person, or as linkedinUrl to POST /v1/people.

profileImageUrlstring nullable required

The stored photo value, as written. Render photoUrl instead: it is the same photo under the name every person-returning route uses, with placeholders removed.

photoUrlstring nullable required

The person's profile photo, as a directly loadable image URL. Null when Trayo holds no real photo for them — show initials then. Every route that returns a person carries this field under this name.

isRepVisibleboolean required

Whether Trayo shows this person to a sales rep. Derived by the system and not settable — you cannot send this field, and a value you do send is ignored.

A person is rep-visible if ANY of these holds: they are linked to a completed event; Trayo attached them to an account; or somebody deliberately added them, which is what POST /v1/people is. So a person you create here comes back true — creating a person through this API is treated as the same deliberate act as adding one by hand in the app, and it surfaces the same way.

What it is NOT: it is not "has an email", and it is not a quality or confidence score. Adding an email, a linkedinUsername or a title neither sets it nor changes it.

false is what you will see on people Trayo collected while searching but never surfaced — profiles no completed event and nothing else ever picked up. Filter them with GET /v1/people?isRepVisible=false; adding one with POST /v1/people turns it true on the same record.

lastEventAtstring date-time nullable required
createdAtstring date-time required
updatedAtstring date-time required
stakeholderReportstring nullable

WHY THIS PERSON MATTERS — the stakeholder report Trayo generates for them: who they are, what they own, and how they relate to what you sell. Markdown, the same report the Trayo app shows on the person, and the same text POST /v1/people/{id}/ask reasons over.

null until stakeholder research has run for this person — it is not produced on create, and a person you added through this API has none until it does. Never an empty string: a blank report reads as null, so a caller can test === null and never has to trim.

Present on GET /v1/people/{id}, and on GET /v1/people only when expand is sent. Without it the KEY is absent, which is not the same as null.

Changes

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