Topolo Docs Topolo Platform
Open workspace

Update prospect contact

Edit a retained active contact profile and merge selected custom fields.

Outcome

Edit a retained active contact profile and merge selected custom fields.

Before you start

  • Verify the environment, credential, authorized organization and the published action permission workspace:write.
  • Discover an accessible Engage workspace and bind resourceType=workspace and its resourceId separately from action input. Fixture identifiers grant no access.
  • Approve the exact mutation and its effects before CLI --confirm or MCP confirm:true. This documentation does not authorize outreach.

Discover the target

  • Use contacts.list or contacts.resolve to discover the contact in the selected workspace. Read contacts.get and retain the current profile, custom fields and archivedAt before planning.
topolo whoami --json
topolo actions get app_topolo_engage.contacts.update --json
topolo workspaces list topolo-engage --json

Input fields

FieldRequiredMeaning and constraints
idYesRequired identifier discovered or retained in the same authorized workspace, trimmed non-empty string up to 128 characters.
firstNameNoOptional trimmed profile text up to 120 characters; null or blank clears the field.
lastNameNoOptional trimmed profile text up to 120 characters; null or blank clears the field.
companyNoOptional trimmed text up to 200 characters; null or blank clears.
titleNoOptional trimmed text up to 200 characters; null or blank clears.
websiteNoOptional trimmed text up to 500 characters; null or blank clears.
countryNoOptional trimmed text up to 80 characters; null or blank clears.
timezoneNoOptional trimmed text up to 80 characters; null or blank clears.
customNoOptional object with trimmed keys of 1–64 characters and string values up to 1,000 characters. On update, supplied keys merge into retained custom fields; this action has no delete-key input.

Validate, plan and execute

Set ENGAGE_WORKSPACE_ID to the identifier of your selected workspace. Prepare payload.json from the current input schema and discovered identifiers. Use the same payload for validation, planning and execution.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions validate app_topolo_engage.contacts.update --data @payload.json --json
topolo actions plan app_topolo_engage.contacts.update --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --confirm --json

Review the plan and obtain approval for the exact effects before executing the unchanged request.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions call app_topolo_engage.contacts.update --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --confirm --json

For MCP, use topolo_get_action, topolo_validate_action, topolo_plan_action and topolo_call_action. Pass the selected resource context separately; confirm:true is required for mutations.

Worked requests and responses

Captured contacts.update · HTTP 200

Action input

{
  "id": "contact_1",
  "company": null,
  "title": "Countess",
  "custom": {
    "region": "uk"
  }
}

Actual local response

{
  "contact": {
    "id": "contact_1",
    "email": "ada@acme.io",
    "domain": "acme.io",
    "addressType": "person",
    "verificationStatus": "unknown",
    "firstName": "Ada",
    "lastName": null,
    "company": null,
    "title": "Countess",
    "website": null,
    "country": null,
    "timezone": null,
    "source": "manual",
    "custom": {
      "tier": "a",
      "region": "uk"
    },
    "createdAt": "2026-10-01T00:00:00.000Z",
    "updatedAt": "2026-10-01T00:00:00.000Z",
    "archivedAt": null
  }
}

Source test: audience routes > contacts > creates, reads, searches, edits and archives a manual contact. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured contacts.update · HTTP 200

Action input

{
  "id": "contact_2",
  "timezone": "Asia/Tokyo"
}

Actual local response

{
  "contact": {
    "id": "contact_2",
    "email": "grace@navy.mil",
    "domain": "navy.mil",
    "addressType": "person",
    "verificationStatus": "unknown",
    "firstName": null,
    "lastName": null,
    "company": null,
    "title": null,
    "website": null,
    "country": null,
    "timezone": "Asia/Tokyo",
    "source": "manual",
    "custom": {},
    "createdAt": "2026-10-01T12:00:00.000Z",
    "updatedAt": "2026-10-01T12:00:00.000Z",
    "archivedAt": null
  }
}

Source test: contactability routes > eligibility > applies quiet hours in the recipient timezone, falling back to Europe/London, across DST. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Effects and response meaning

  • Supplied profile fields replace retained values; null or blank clears them. Omitted fields stay unchanged. Supplied custom keys merge; omitted keys stay and no delete-key input exists. The email cannot be edited.
  • Returns the updated contact. Archived contacts return state_conflict; an id-only request without profile/custom returns invalid_input. This is not a compare-and-swap edit and cannot detect concurrent changes through an expected version input. Nothing is sent.

Verify the result

  • Read contacts.get and compare changed fields plus untouched fields to the reviewed prior state. Recheck archivedAt before a separately approved next operation.

Recover

After an uncertain edit, read contacts.get and reconcile the supplied fields before retrying. To restore profile values, plan a separate update with reviewed prior values; custom keys introduced by this update cannot be deleted through this action. Do not overwrite concurrent edits blindly.

Refused responses

Captured contacts.update · HTTP 422

Action input

{
  "id": "contact_1"
}

Actual local response

{
  "error": {
    "code": "invalid_input",
    "message": "Provide at least one field to change."
  }
}

Source test: audience routes > contacts > creates, reads, searches, edits and archives a manual contact. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured contacts.update · HTTP 409

Action input

{
  "id": "contact_1",
  "title": "x"
}

Actual local response

{
  "error": {
    "code": "state_conflict",
    "message": "Archived contacts cannot be edited."
  }
}

Source test: audience routes > contacts > creates, reads, searches, edits and archives a manual contact. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured contacts.update · HTTP 404

Action input

{
  "id": "contact_1",
  "title": "x"
}

Actual local response

{
  "error": {
    "code": "not_found",
    "message": "Contact was not found in this workspace."
  }
}

Source test: audience routes > contacts > returns 404 for a contact in another workspace or organization. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Error recovery

  • HTTP 403 permission_denied: Stop and inspect the permission required by this action in the current credential. Do not broaden permissions or switch organization automatically.
  • HTTP 422 invalid_action_input: Correct the request against the current input schema and validate it again before planning.
  • HTTP 422 invalid_input: Supply at least one actual editable field: contact profile/custom or list name/description. An id-only edit is refused; validate the intended payload again.
  • HTTP 409 state_conflict: Read the exact retained contact or list and inspect archivedAt. Archived records cannot be edited or have list memberships changed; repeated archive is refused. Do not invent an unarchive operation.
  • HTTP 404 not_found: Rediscover the identifier in the same authorized workspace. An identifier from another workspace is not a valid substitute; retain archived identifiers for direct reads.

Evidence and limits

  • contracts/audience.mjs

  • contracts/audience-filter.mjs

  • apps/api/src/audience/audience-routes.ts

  • apps/api/src/audience/contacts-handlers.ts

  • apps/api/src/audience/contact-search.ts

  • apps/api/src/audience/contacts-repository.ts

  • apps/api/src/audience/lists-handlers.ts

  • apps/api/src/audience/lists-repository.ts

  • apps/api/src/audience/filter-source.ts

  • apps/api/test/audience-routes.test.ts

  • apps/api/test/segments-routes.test.ts

  • Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

  • Widget/bootstrap use actual app handler but mocked worker runtime and app identity.

  • No gateway execution, live permissions, external delivery or deployed proof.

The examples are local route captures with synthetic context and simulated providers. They do not prove live authorization, CLI/MCP gateway execution, production state or delivery.