Update a contact

Move someone through your funnel, merge attributes, set your own id, or override a trust verdict.

Updated ·4 min read
PATCH/v1/contacts/{contact}

Requires the contacts:write scope.

Body parameters#

Parameter Type Description
stage string Must be one of this project's configured pipeline stages
attributes object Merged by key, up to 50 keys. A null value deletes that key
email string The address the contact is mailed at, for a contact identified by something else. null clears it
external_id string Your own id, unique per project. null clears it
trust_override enum verified, valid, unknown, risky or fake. null hands the verdict back to the engine

Every parameter is optional; send only what you are changing. Identity is not among them, for the reason below.

An external_id another contact in this project already holds returns 422 external_id_taken.

Attributes merge, they do not replace#

Sending attributes updates only the keys you send. Everything else stays.

{ "attributes": { "cohort": "beta-2", "source": null } }

That sets cohort and deletes source, leaving every other attribute untouched. Replacing the whole object would mean every caller has to read-modify-write, which is a race waiting to happen.

Contacts past your plan's size#

A contact hidden past your plan's contact size cannot be edited. The request answers 403 with code: contact_hidden and param: contact, and nothing is written: stage, attributes, trust_override and external_id stay as they were. The contact is saved, and editing works again once the workspace moves up a size. Deleting a hidden contact still works.

Changing the email address#

email is where a contact is mailed, which is separate from who they are. On a contact identified by a phone number, a username or a wallet you can set it, change it, or clear it with null. It is canonicalized before it is stored.

Changing or clearing an address the contact already has resets what was known about the old one: verified_at becomes null and deliverability returns to unknown, because both were facts about a mailbox the contact no longer uses. status stays as it is. Setting an address on a contact that had none resets nothing.

On a contact identified by an email, email is ignored. There the address is the identifier.

Identity is not editable#

There is no way to change identifier. Identity is resolved, not edited. A different identifier is a different person, and quietly rewriting one would corrupt deduplication and the referral graph.

If you genuinely have two records for one human, that is a merge, and merging is not something this endpoint should do silently.

Telling us somebody converted#

converted is the one stage nothing in LaunchList ever sets for you, and that is deliberate: whether somebody paid happens inside your product, on your servers, and we would only be guessing. This endpoint is how you close that loop.

{ "stage": "converted" }

Call it when the thing you actually care about happens: subscription created, plan upgraded, first order placed. The same applies to any stage you added yourself: stage accepts any key from your funnel, so a project with an activated stage can report activation the same way.

Next#

Was this page helpful?
curl -X PATCH https://api.getlaunchlist.com/v1/contacts/con_4mKp9Wq2Rx \\
  -H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk" \\
  -H "Content-Type: application/json" \\
  -d '{
  "stage": "invited",
  "attributes": {
    "cohort": "beta-2"
  }
}'
{
  "object": "contact",
  "id": "con_4mKp9Wq2Rx",
  "livemode": true,
  "project": "prj_7Qk2mN4pR8",
  "identifier": "ada@lovelace.dev",
  "identifier_type": "email",
  "email": "ada@lovelace.dev",
  "external_id": "user_9182",
  "attributes": { "role": "Founder", "cohort": "beta-2" },
  "status": "subscribed",
  "stage": "invited",
  "deliverability": null,
  "trust_tier": "verified",
  "position": 1284,
  "referral": {
    "code": "ADA7X2",
    "url": "https://acme.launchlist.page?ref=ADA7X2",
    "count": 3,
    "referred_by": "con_2Bn8Vd5Lk",
    "state": "credited"
  },
  "unsubscribed": false,
  "locked": false,
  "verified_at": "2026-07-28T14:02:55+00:00",
  "opt_in_confirmed_at": "2026-07-28T14:02:55+00:00",
  "consent": "signup",
  "consented_at": "2026-07-28T13:59:10+00:00",
  "unsubscribed_by": null,
  "unsubscribed_at": null,
  "created_at": "2026-07-28T13:59:10+00:00",
  "url": "/v1/contacts/con_4mKp9Wq2Rx"
}