---
title: "Correct workspace onboarding assumptions and settings"
method: PATCH
path: "/v1/workspace"
tags: ["Workspace"]
---

# Correct workspace onboarding assumptions and settings

`PATCH /v1/workspace`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/workspace`
- Auth: HTTP bearer

**Required permissions:** `settings:write`.

Partial update: send only the fields you are changing. `solutionItems` and `stakeholderPersonas` replace complete structured lists and regenerate their prose projections atomically. Pass the latest `revision` as `expectedRevision` for list edits, and re-read and merge on a 409 conflict. Changing solutions clears an omitted `solutionsBrief` rather than leaving an outdated summary. `buyerProfile` patches only supplied ICP assumptions, including nested geography and sizeBand. Legacy prose fields accept `null` to clear, but a direct prose edit discards the matching structured cards. Do not send both forms of the same setting. Discovery uses the saved values on future work.

**Write the definition as prose, the way you would brief a new researcher**: the roles that own or buy what you sell, the roles that influence the decision, and the roles to skip. Example: "Heads of platform, infrastructure, DevOps or SRE, and the VP or Director of Engineering they report to. Not recruiters, not IT, not security, not individual engineers."

**The stakeholder definition is what makes stakeholder search yours.** Two stages read it. The account-level search that runs when a company is added to the workspace turns it into search keywords and selects people at the company against it; without a definition that search seeds on seniority alone (chiefs, VPs, directors of any function). The per-event stage that a discovery run started with `people: "add"` runs attaches people, with a per-person `reasoning`, to each event; without a definition that stage does not run at all and `expand=people` is `[]` on every event.

**Set it FIRST — before you add accounts, and before you run discovery.** It steers work that starts after it is set and is never applied retroactively, and the account-level search for a company starts the moment that company is added. An account added before the definition existed keeps the people that search found for it then; the way to search it against the new definition is `POST /v1/stakeholders/search`.

## Request body

- UpdateWorkspaceBodyDto
  - `expectedRevision` string: The revision returned by trayo_get_workspace or GET /v1/workspace. Required for full-list replacements. A stale revision returns 409; re-read and merge instead of retrying the old list.
  - `stakeholderCriteria` string, nullable: Plain prose describing the roles to find and the roles to skip, e.g. "Heads of platform, infrastructure, DevOps or SRE, and the VP or Director of Engineering they report to. Not recruiters, not IT, not individual engineers." Send null to clear it.
  - `solutions` string, nullable: What you sell, in prose. Send null to clear it.
  - `solutionsBrief` string, nullable: A one- or two-sentence version of `solutions`. Send null to clear it. If solutions or solutionItems change and this is omitted, the old brief is cleared. Do not re-send an old brief that may be inaccurate after the change.
  - `solutionItems` object[]: Replace the complete solution-card list. Requires expectedRevision. The prose `solutions` projection is regenerated atomically, and the old `solutionsBrief` is cleared unless a new one is sent. Send [] to clear the cards; icons are server-owned.
    - `name` string, required
    - `description` string, required
    - `url` string, nullable
  - `stakeholderPersonas` object[]: Replace the complete buyer-persona list. Requires expectedRevision. `stakeholderCriteria` is regenerated atomically. Send [] to clear the personas; icons are server-owned.
    - `title` string, required
    - `seniority` 'C-level' | 'VP' | 'Director' | 'Manager', required
    - `buyingRole` 'economic_buyer' | 'decision_maker' | 'champion' | 'technical_evaluator', nullable
    - `rationale` string, required
  - `buyerProfile` object: Correct only the supplied ICP assumptions; nested geography and sizeBand are partial too. Requires an existing saved profile. Tenant-domain identity is never writable.
    - `isB2b` boolean
    - `notB2bReason` string
    - `sellerMode` 'horizontal' | 'vertical' | 'local_smb' | 'smb_horizontal' | 'enterprise'
    - `modeReason` string
    - `geography` object
      - `scope` 'local' | 'regional' | 'national' | 'global'
      - `countries` string[]
      - `regionLabel` string
      - `states` string[]
      - `cities` string[]
      - `radiusNote` string
    - `sizeBand` object
      - `min` integer
      - `max` integer
    - `sameIndustryBuyer` boolean
    - `rivalDefinition` string
    - `buyerThesis` string
    - `positiveSignals` string[]
    - `industries` string[]
    - `technologies` string[]
    - `competitorTerms` string[]
    - `categoryKeywords` string[]
    - `existingCustomers` string[]
    - `generationAngles` string[]

## Response `200`

The settings as they now stand.

- WorkspaceDto
  - `id` string, uuid, required: The workspace your key acts in.
  - `name` string, required: The workspace name, as the Trayo app shows it. Read-only here.
  - `revision` string, required: Opaque version of the saved onboarding settings. Pass it as expectedRevision when replacing solution or persona lists; re-read on conflict.
  - `stakeholderCriteria` string, nullable, required: Your stakeholder definition, in plain prose: who buys, owns, or influences what you sell, and who does not. It steers BOTH stakeholder stages — the account-level search that runs when a company is added to the workspace, and the per-event people (with `reasoning`) a discovery run started with `people: "add"` attaches. Null until set: the account-level search then seeds on seniority alone and no people are attached to events.
  - `solutions` string, nullable, required: What you sell, in prose. Read by the reasoning that explains why a person or event matters, and by the app when it drafts a stakeholder definition. Null until set.
  - `solutionsBrief` string, nullable, required: A one- or two-sentence version of `solutions`. Null until set.
  - `solutionItems` object[], nullable, required: Editable solution cards saved during onboarding. Null means this workspace only has prose; an empty array means no cards.
    - `name` string, required
    - `description` string, required
    - `url` string, nullable, required
    - `icon` string, nullable, required
  - `stakeholderPersonas` object[], nullable, required: Editable buyer and stakeholder personas saved during onboarding. Null means only prose is available.
    - `title` string, required
    - `seniority` 'C-level' | 'VP' | 'Director' | 'Manager', required
    - `buyingRole` 'economic_buyer' | 'decision_maker' | 'champion' | 'technical_evaluator', nullable, required
    - `rationale` string, required
    - `icon` string, nullable, required
  - `buyerProfile` object, required: The ICP assumptions: buyer thesis, industries, size and geography, exclusions and search angles. Always present. This field was previously null when no profile was saved; it no longer is, so read `buyerProfileSource` instead of checking for null, and check it before treating this as fact.
    - `isB2b` boolean, required
    - `notB2bReason` string, required
    - `sellerMode` 'horizontal' | 'vertical' | 'local_smb' | 'smb_horizontal' | 'enterprise', required
    - `modeReason` string, required
    - `geography` object, required
      - `scope` 'local' | 'regional' | 'national' | 'global', required
      - `countries` string[], required
      - `regionLabel` string, required
      - `states` string[], required
      - `cities` string[], required
      - `radiusNote` string, required
    - `sizeBand` object, required
      - `min` integer, required
      - `max` integer, required
    - `sameIndustryBuyer` boolean, required
    - `rivalDefinition` string, required
    - `buyerThesis` string, required
    - `positiveSignals` string[], required
    - `industries` string[], required
    - `technologies` string[], required
    - `competitorTerms` string[], required
    - `categoryKeywords` string[], required
    - `existingCustomers` string[], required
    - `generationAngles` string[], required
  - `buyerProfileSource` 'saved' | 'partial' | 'inferred' | 'unreadable', required: `saved`: read from onboarding's stored ICP. `partial`: the fields sent to this API are the caller's own; every field never sent is still the placeholder below. `inferred`: nothing was saved, so you got a fixed placeholder — the same wide defaults for every workspace, not read from your site or your name. `unreadable`: an ICP is saved but not in a shape this API can read, so you got that same placeholder and patching is refused; edit it in the Trayo app. Only `saved` fields and fields you supplied yourself are facts about this workspace; confirm the rest before relying on them.
  - `brand` object, nullable, required: Your company brand, from your workspace website: name, description, colors, social profiles, and logo images you can embed. Null when no brand is known for the website.
    - `domain` string, required: The website domain the brand belongs to, e.g. "stripe.com".
    - `name` string, nullable, required: The brand name.
    - `description` string, nullable, required: A one- or two-sentence description of the company.
    - `colors` string[], required: Brand colors as lowercase `#rrggbb`, most prominent first.
    - `socials` object[], required: Social profiles found for the brand.
      - `network` string, required: Network key, e.g. `linkedin`, `twitter`, `github`.
      - `url` string, required: Profile URL.
    - `logoUrl` string, required: Square logo image URL. Public, no key needed.
    - `brandmarkUrl` string, nullable, required: Wide logo lockup image URL (800px wide PNG). Public, no key needed. Null when none.
    - `bannerUrl` string, nullable, required: Social banner image URL (800px wide). Public, no key needed. Null when none.

## Other responses

- `400`: An empty body, an unknown field, a blank or oversized value, conflicting structured/prose forms, a list edit without expectedRevision, or a buyer-profile edit without a saved profile. `param` names the field. `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `409`: The workspace changed since expectedRevision. Re-read and merge the correction. `conflict` (`retry: fix_input`) — The generic 409: the request collides with existing state, and none of the specific codes covers it. `message` says what.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
