Back to Help Home
Last updated: 2026-09-17

Public API Contact Update Endpoint

This endpoint lets an external system update a contact that already exists in a brayv.ai account. It accepts the same contact fields as the Lead Submission API, but it never creates a contact and never runs lead intake: no journey routing, no opportunity, no notifications. Use it to keep a contact current after the lead has arrived.

Endpoint

PATCH https://api.brayv.ai/v1/accounts/{account_id}/contacts
Authorization: Bearer {api_key}
Content-Type: application/json

Replace {account_id} with the brayv.ai account ID that owns the contact.

Authentication

Send the API key in the Authorization header:

Authorization: Bearer brayv_live_...

The same keys used for lead submission work here. A key is scoped to either one account or an organization that has access to the target account. Rate limits are shared with the lead endpoint, per key and per account.

Identifying the Contact

Every request must identify one contact with contact_id, phone, or both. contact_id is the brayv.ai contact ID returned by the lead endpoint. phone must use one of the accepted phone formats below.

Fields sentBehavior
contact_id onlyThe contact with that ID is updated. 404 if it does not exist in this account.
phone onlyThe contact with that phone number is updated. 404 if none matches. 409 if more than one contact has that number; the response lists their contact_ids, so send contact_id instead.
contact_id and phonecontact_id is used. If the phone number does not belong to that contact, the request is refused with 409 and nothing is updated.
Neither422.

phone is used only to find the contact. This endpoint cannot change a contact's phone number. Use brayv.ai to edit a phone number.

Accepted Phone Formats

FormatExample
+1XXXXXXXXXX+15551234567
1XXXXXXXXXX15551234567
XXXXXXXXXX, where first digit is not 15551234567

Formatted numbers such as (555) 123-4567 are rejected with 422, even when contact_id is also present.

Response

{
  "status": "success",
  "contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
  "matched_by": "contact_id",
  "updated_fields": ["hook", "city", "custom_fields"],
  "ignored_fields": [],
  "note_created": true,
  "idempotent_replay": false,
  "account_status": "active",
  "notice": null
}
FieldMeaning
matched_bycontact_id or phone, whichever located the contact.
updated_fieldsContact fields whose stored value actually changed. A field that was accepted but already held the submitted value is not listed. tags and custom_fields appear as a whole.
ignored_fieldsFields that were not applied, each with a reason.
note_createdWhether a new note was added to the contact.
account_statusactive normally. frozen when the account is paused for a payment issue (see below).
noticenull normally. A human-readable explanation when account_status is not active.

Updatable Fields

All fields are optional. Send only what should change. These are the same fields the Lead Submission API accepts, with two exceptions: phone identifies the contact and is never written, and source cannot be changed.

FieldTypeNotes
full_namestringIf sent without first_name / last_name, it is split at the first space.
first_namestring
last_namestring
emailstringMust match [email protected], otherwise ignored. The contact's previous email is kept in additional_emails.
business_namestring
websitestring
birthdaystringParseable date string. ISO YYYY-MM-DD recommended.
timezonestringMust be one of the timezone values listed in the Lead Submission API.
additional_phonesstring[]Replaces the list.
additional_emailsstring[]Added to the existing list. Invalid emails are dropped.
street_addressstring
citystring
statestring
zip_codestring
countrystring
tagsstring[]Added to the contact's existing tags. Tags are never removed.
friendly_sourcestringHuman-readable source label for AI context.
hookstringShort description of what the contact is interested in.
service_descriptionstringLonger description of the requested service or issue.
has_voice_aibooleanWhether an AI voice agent may handle this contact's calls at all.
utm_source, utm_medium, utm_campaign, utm_content, utm_keywordstring
referrer, landing_page_urlstring
facebook_click_id, google_click_id, google_wbraid, google_gbraidstring
facebook_lead_id, campaign_idstring
text_opt_in, call_opt_in, voice_ai_opt_inbooleanOptional here. See Consent Behavior.
custom_fieldsobjectKeyed by custom field ID. See Custom Fields.
notestring or objectAdds a new note. See Note.
idempotency_keystringSee Idempotency.

Unknown fields are ignored and reported in ignored_fields.

Fields That Cannot Be Changed

FieldBehavior
phoneUsed to identify the contact only. Never written.
sourceIgnored and reported in ignored_fields. The source recorded when the lead arrived is kept, because an update is not a new lead.

How Updates Are Applied

RuleDetail
Empty values are ignorednull, "", and whitespace-only strings do not clear a field. This endpoint cannot blank out a value.
tags and additional_emails are additiveSubmitted values are added; existing ones stay.
custom_fields are mergedSubmitted keys overwrite their values; keys you do not send are kept.
A new email keeps the old oneThe previous address moves into additional_emails.
Address changes reset the map pinChanging street_address, city, state, zip_code, or country clears the contact's cached location so it is geocoded again.
Team member contacts keep their identityIf the contact is linked to one of your team members, first_name, last_name, full_name, and email are refused and reported in ignored_fields. Everything else applies. See Internal Staff Contacts.

Every update is recorded on the contact's activity history as Contact updated by Public contact API, listing the fields that changed and anything that was refused.

An Update That Changes Nothing

If a request carries no updatable field, no valid custom field, and no note, it is refused with 422 rather than answering 200 for a no-op. The ignored_fields list in the error says why each field was dropped.

{
  "detail": {
    "message": "Invalid contact update",
    "validation_errors": [
      { "field": "payload", "reason": "No updatable fields were provided" }
    ],
    "ignored_fields": [
      { "field": "Hook", "reason": "Unknown field" }
    ]
  }
}

The three consent flags are optional on this endpoint. Only the flags you send change anything; the others keep their current values.

Submitted fieldsContact behavior
text_opt_in: falseSets dnd_sms = true and has_text_ai = false.
text_opt_in: trueSets dnd_sms = false and has_text_ai = true, unless the contact has a carrier-level STOP opt-out, in which case the opt-in is ignored and logged.
call_opt_in: falseSets dnd_calls = true and has_voice_ai_outbound = false. A voice_ai_opt_in: true in the same request is ignored and logged.
call_opt_in: true, voice_ai_opt_in omittedSets dnd_calls = false. AI dialing consent is left as it was.
call_opt_in: true, voice_ai_opt_in: <bool>Sets dnd_calls = false and has_voice_ai_outbound to the submitted value.
voice_ai_opt_in aloneUses the contact's current call consent. If the contact may be called, has_voice_ai_outbound is set to the submitted value. If the contact has dnd_calls set, a true is ignored and logged; AI dialing cannot be switched on for someone who asked not to be called.

Non-boolean values for any consent flag are ignored and logged.

Custom Fields

Send custom field values inside the custom_fields object, keyed by the custom field ID. Use the Custom Fields API to look up the IDs, types, and options for an account.

{
  "contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
  "custom_fields": {
    "service_type": "Plumbing",
    "preferred_service_date": "2026-10-02"
  }
}

Validation is the same as the lead endpoint: values that do not match the field's type or options are ignored and logged, and the request still succeeds. Keys you do not send are left as they were.

Note

note adds a new note to the contact. It never edits an existing note.

{
  "contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
  "note": {
    "content": "Customer rescheduled to Thursday afternoon.",
    "surface_to_agents": true
  }
}

A plain string is also accepted and leaves surface_to_agents at false. Content is trimmed to 1000 characters. If the contact already has a note with identical text, no duplicate is created and the note is reported in ignored_fields. Notes created this way are labelled Integration in brayv.ai. The visibility rules for surface_to_agents are the same as the Lead Submission API.

A request may consist of a note alone.

Idempotency

idempotency_key is optional. Repeated requests with the same API key, target account, and idempotency key within about 5 minutes return the original result with idempotent_replay: true instead of applying the update again.

Account Paused for a Payment Issue

If the target account is paused for a payment issue, the update still applies. The response reports account_status: "frozen" and a notice explaining that no outbound outreach will occur until billing is fixed.

Account Inactive

If the account's brayv.ai subscription has been cancelled, the request returns 403 Forbidden and nothing is changed. Have an account admin reactivate the subscription, then retry.

{
  "detail": {
    "code": "account_inactive",
    "message": "Account is inactive. Please have an account admin reactivate the Brayv subscription."
  }
}

Errors

StatuscodeWhen
401Missing or invalid API key.
403Key is not authorized for this account.
403account_inactiveThe account's subscription is cancelled.
404contact_not_foundNo contact matches contact_id, or no contact has the submitted phone.
409identifier_mismatchcontact_id and phone were both sent and the phone number does not belong to that contact.
409multiple_contactsphone alone matched more than one contact. contact_ids lists them.
422No identifier, a badly formatted phone, or a payload with nothing to update.
429Rate limit exceeded for this key and account.

Identifier mismatch example:

{
  "detail": {
    "code": "identifier_mismatch",
    "message": "The phone number does not belong to the contact identified by contact_id",
    "contact_id": "34726940-88ec-49e3-9529-6e0845703eda"
  }
}

Multiple matches example:

{
  "detail": {
    "code": "multiple_contacts",
    "message": "More than one contact has this phone number; send contact_id instead",
    "contact_ids": [
      "34726940-88ec-49e3-9529-6e0845703eda",
      "a1e2f0d5-6c8b-4d2a-9f3e-0b7c1d2e3f40"
    ]
  }
}

Full Example Payload

{
  "contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
  "phone": "5551234567",
  "first_name": "Jane",
  "last_name": "Doe",
  "email": "[email protected]",
  "street_address": "456 Oak Ave",
  "city": "Charlotte",
  "state": "NC",
  "zip_code": "28203",
  "tags": ["repeat-customer"],
  "hook": "Water heater replacement",
  "friendly_source": "Angi",
  "text_opt_in": true,
  "call_opt_in": true,
  "voice_ai_opt_in": true,
  "custom_fields": {
    "service_type": "Plumbing",
    "lead_priority": "High"
  },
  "note": {
    "content": "Customer confirmed the water heater is a 50-gallon gas unit.",
    "surface_to_agents": true
  },
  "idempotency_key": "angi-update-98765"
}

Example cURL

curl -X PATCH "https://api.brayv.ai/v1/accounts/ACCOUNT_ID/contacts" \
  -H "Authorization: Bearer brayv_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "5551234567",
    "hook": "Water heater replacement",
    "tags": ["repeat-customer"],
    "custom_fields": {
      "service_type": "Plumbing"
    },
    "note": "Customer confirmed the water heater is a 50-gallon gas unit."
  }'