Related Articles
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 sent | Behavior |
|---|---|
contact_id only | The contact with that ID is updated. 404 if it does not exist in this account. |
phone only | The 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 phone | contact_id is used. If the phone number does not belong to that contact, the request is refused with 409 and nothing is updated. |
| Neither | 422. |
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
| Format | Example |
|---|---|
+1XXXXXXXXXX | +15551234567 |
1XXXXXXXXXX | 15551234567 |
XXXXXXXXXX, where first digit is not 1 | 5551234567 |
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
}
| Field | Meaning |
|---|---|
matched_by | contact_id or phone, whichever located the contact. |
updated_fields | Contact 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_fields | Fields that were not applied, each with a reason. |
note_created | Whether a new note was added to the contact. |
account_status | active normally. frozen when the account is paused for a payment issue (see below). |
notice | null 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.
| Field | Type | Notes |
|---|---|---|
full_name | string | If sent without first_name / last_name, it is split at the first space. |
first_name | string | |
last_name | string | |
email | string | Must match [email protected], otherwise ignored. The contact's previous email is kept in additional_emails. |
business_name | string | |
website | string | |
birthday | string | Parseable date string. ISO YYYY-MM-DD recommended. |
timezone | string | Must be one of the timezone values listed in the Lead Submission API. |
additional_phones | string[] | Replaces the list. |
additional_emails | string[] | Added to the existing list. Invalid emails are dropped. |
street_address | string | |
city | string | |
state | string | |
zip_code | string | |
country | string | |
tags | string[] | Added to the contact's existing tags. Tags are never removed. |
friendly_source | string | Human-readable source label for AI context. |
hook | string | Short description of what the contact is interested in. |
service_description | string | Longer description of the requested service or issue. |
has_voice_ai | boolean | Whether an AI voice agent may handle this contact's calls at all. |
utm_source, utm_medium, utm_campaign, utm_content, utm_keyword | string | |
referrer, landing_page_url | string | |
facebook_click_id, google_click_id, google_wbraid, google_gbraid | string | |
facebook_lead_id, campaign_id | string | |
text_opt_in, call_opt_in, voice_ai_opt_in | boolean | Optional here. See Consent Behavior. |
custom_fields | object | Keyed by custom field ID. See Custom Fields. |
note | string or object | Adds a new note. See Note. |
idempotency_key | string | See Idempotency. |
Unknown fields are ignored and reported in ignored_fields.
Fields That Cannot Be Changed
| Field | Behavior |
|---|---|
phone | Used to identify the contact only. Never written. |
source | Ignored 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
| Rule | Detail |
|---|---|
| Empty values are ignored | null, "", and whitespace-only strings do not clear a field. This endpoint cannot blank out a value. |
tags and additional_emails are additive | Submitted values are added; existing ones stay. |
custom_fields are merged | Submitted keys overwrite their values; keys you do not send are kept. |
A new email keeps the old one | The previous address moves into additional_emails. |
| Address changes reset the map pin | Changing street_address, city, state, zip_code, or country clears the contact's cached location so it is geocoded again. |
| Team member contacts keep their identity | If 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" }
]
}
}
Consent Behavior
The three consent flags are optional on this endpoint. Only the flags you send change anything; the others keep their current values.
| Submitted fields | Contact behavior |
|---|---|
text_opt_in: false | Sets dnd_sms = true and has_text_ai = false. |
text_opt_in: true | Sets 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: false | Sets 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 omitted | Sets 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 alone | Uses 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
| Status | code | When |
|---|---|---|
401 | Missing or invalid API key. | |
403 | Key is not authorized for this account. | |
403 | account_inactive | The account's subscription is cancelled. |
404 | contact_not_found | No contact matches contact_id, or no contact has the submitted phone. |
409 | identifier_mismatch | contact_id and phone were both sent and the phone number does not belong to that contact. |
409 | multiple_contacts | phone alone matched more than one contact. contact_ids lists them. |
422 | No identifier, a badly formatted phone, or a payload with nothing to update. | |
429 | Rate 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."
}'