Public API Lead Endpoint
This endpoint lets an external system submit a lead to a specific brayv.ai account. A successful request creates or updates a contact immediately, records submission history, and queues background lead processing such as journey routing, opportunity creation, assignment, and notifications.
Endpoint
POST https://api.brayv.ai/v1/accounts/{account_id}/leads
Authorization: Bearer {api_key}
Content-Type: application/json
Replace {account_id} with the brayv.ai account ID receiving the lead.
Authentication
Send the API key in the Authorization header:
Authorization: Bearer brayv_live_...
Each API key is scoped to either one account or an organization that has access to the target account. The same key also authorizes the related endpoints below.
Related Endpoints
| Endpoint | Use it to |
|---|---|
PATCH /v1/accounts/{account_id}/contacts | Update a contact that already exists, identified by contact_id or phone, without creating a new lead. See Contact Update API. |
GET /v1/accounts/{account_id}/custom-fields | Look up the account's custom field IDs, types, and options to use in custom_fields. See Custom Fields API. |
Response
Successful submissions return immediately after the contact is created or updated and the background processing job is queued.
{
"status": "success",
"submission_id": "e7ac371f-478e-4290-b147-524a0f491ab2",
"contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
"idempotent_replay": false,
"ignored_fields": [],
"account_status": "active",
"notice": null
}
| Field | Meaning |
|---|---|
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. |
Account Paused for a Payment Issue
If the target account's payment method has failed or its wallet is empty, brayv.ai pauses outbound activity for that account until billing is fixed. Submissions still succeed: the contact is created and the lead is routed into its journey, but no texts, calls, or AI outreach go out until the account admin resolves the payment issue. Journey steps that come due while the account is paused are held, and are re-scheduled at their normal interval once billing recovers.
{
"status": "success",
"submission_id": "e7ac371f-478e-4290-b147-524a0f491ab2",
"contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
"idempotent_replay": false,
"ignored_fields": [],
"account_status": "frozen",
"notice": "Account is paused for a payment issue. The contact was created, but no outbound outreach will occur until the payment issue is resolved."
}
Account Inactive
If the target account's brayv.ai subscription has been cancelled, the request returns 403 Forbidden. The contact is still created so the data is there when the account reactivates, but nothing else happens: no opportunity, no journey, no outreach. Have an account admin reactivate the subscription, then resubmit. Do not treat this as a transient error; retrying without reactivating will keep returning 403.
{
"detail": {
"code": "account_inactive",
"message": "Account is inactive. Please have an account admin reactivate the Brayv subscription.",
"contact_id": "34726940-88ec-49e3-9529-6e0845703eda",
"submission_id": "e7ac371f-478e-4290-b147-524a0f491ab2"
}
}
Required Fields
| Field | Type | Required | Notes |
|---|---|---|---|
phone | string | Yes | Must match one of the accepted phone formats below. Saved as +1XXXXXXXXXX. |
source | string | Yes | Saved on the contact as api: {source}. Example: api: angi. |
text_opt_in | boolean | Yes | Whether the contact opted into SMS/text outreach. |
call_opt_in | boolean | Yes | Whether the contact opted into outbound calls. |
voice_ai_opt_in | boolean | Yes | Whether the contact opted into outbound AI voice calls. Controls has_voice_ai_outbound. |
full_name | string | Conditional | At least one of full_name, first_name, or last_name is required. |
first_name | string | Conditional | At least one of full_name, first_name, or last_name is required. |
last_name | string | Conditional | At least one of full_name, first_name, or last_name is required. |
Recommended Fields
These fields are not required, but they are strongly recommended because they are commonly used in scripted engagement messages and AI agent instructions.
| Field | Type | Why it is useful |
|---|---|---|
hook | string | A short description of what the lead is interested in. Commonly used in scripted messages, for example: Following up on your {hook} request, is now a better time to chat? |
friendly_source | string | A human-readable description of where the lead came from (e.g. 'our ad' or 'our website'). Commonly used in AI agent instructions, for example: The user you are chatting with reached out to us via {friendly_source}. |
Consent Behavior
| Submitted fields | Contact behavior |
|---|---|
text_opt_in: false | Sets dnd_sms = true and has_text_ai = false. |
text_opt_in: true | Allows SMS/text AI, unless the contact has a carrier-level STOP opt-out. |
| Existing contact has carrier-level STOP opt-out | API cannot clear SMS DND or enable text AI. The submitted opt-in is ignored and logged. |
call_opt_in: false | Sets dnd_calls = true and has_voice_ai_outbound = false. A submitted voice_ai_opt_in: true is ignored and logged, because nobody may call the contact at all. |
call_opt_in: true, voice_ai_opt_in: false | Sets dnd_calls = false and has_voice_ai_outbound = false. Your team may call; an AI agent may not dial. |
call_opt_in: true, voice_ai_opt_in: true | Sets dnd_calls = false and has_voice_ai_outbound = true. |
Opt-out fields do not block contact creation or opportunity creation. They control outreach eligibility.
The Two Voice AI Fields
A contact carries two separate voice AI settings, and they answer different questions:
| Field | Meaning when false | How to set it |
|---|---|---|
has_voice_ai | An AI voice agent will never answer a call from this contact and will never dial them. Voice AI is off for them entirely. | Send has_voice_ai directly. Optional; defaults to true. |
has_voice_ai_outbound | An AI voice agent will never dial this contact. They can still reach an AI agent when they call you. | Derived from the required voice_ai_opt_in field, per the table above. |
Both default to true, so voice AI is available unless you say otherwise. Send has_voice_ai: false for a contact who should only ever deal with a human, and use voice_ai_opt_in: false for contacts where you did not explictly gather consent to call them using AI.
has_voice_ai: false implies no AI dialing regardless of voice_ai_opt_in, since it turns voice AI off in both directions. The two fields are stored independently, so you can still send voice_ai_opt_in: true alongside it, and the outbound flag will read true while has_voice_ai keeps the AI away from the contact.
Both fields are enforced. An AI voice agent asked to dial a contact whose has_voice_ai or has_voice_ai_outbound is false does not place the call, and the reason is recorded on the contact's activity history. A contact whose has_voice_ai is false who calls one of the account's Voice AI numbers is forwarded to a person instead of reaching an agent.
dnd_calls remains the broadest setting and covers the whole team, not just the AI: it also blocks calls placed by hand from the in-app dialer. call_opt_in: false sets it, and it is what to rely on for a contact who asked not to be called by anyone.
Required Formats
Phone
Only these formats are accepted:
| Format | Example | Saved as |
|---|---|---|
+1XXXXXXXXXX | +15551234567 | +15551234567 |
1XXXXXXXXXX | 15551234567 | +15551234567 |
XXXXXXXXXX, where first digit is not 1 | 5551234567 | +15551234567 |
Formatted numbers such as (555) 123-4567, 555-123-4567, or numbers with spaces are rejected. Rejected submissions are still logged, but no contact is created.
email is optional. If provided, it must match a basic email pattern:
[email protected]
If the email does not match, the submission can still succeed, but the email is ignored and not saved to the contact.
Dates
birthday and date custom fields should be parseable date strings. ISO format is recommended:
YYYY-MM-DD
Invalid optional dates are ignored and logged.
Optional Standard Contact Fields
Unknown fields are ignored and logged. These standard contact fields are accepted:
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | No | Must match [email protected], otherwise ignored. |
has_voice_ai | boolean | No | Whether an AI voice agent may handle this contact's calls at all. Defaults to true. Non-boolean values are ignored. See The Two Voice AI Fields. |
business_name | string | No | Business or company name. |
website | string | No | Website URL or domain. |
birthday | string | No | Parseable date string. ISO YYYY-MM-DD recommended. |
timezone | string | No | Must be one of the accepted timezone values below. |
additional_phones | string[] | No | List of additional phone values. |
additional_emails | string[] | No | Invalid emails are ignored. |
street_address | string | No | Street address. |
city | string | No | City. |
state | string | No | State or province. |
zip_code | string | No | ZIP or postal code. |
country | string | No | Country. |
tags | string[] | No | Tags to apply to the contact. |
friendly_source | string | No | Human-readable source label for AI context. |
hook | string | No | Short description of what the contact is interested in. |
service_description | string | No | Longer description of requested service or issue. |
utm_source | string | No | UTM source. |
utm_medium | string | No | UTM medium. |
utm_campaign | string | No | UTM campaign. |
utm_content | string | No | UTM content. |
utm_keyword | string | No | UTM keyword. |
referrer | string | No | Referrer URL. |
landing_page_url | string | No | Landing page URL. |
facebook_click_id | string | No | Facebook fbclid. |
google_click_id | string | No | Google gclid. |
google_wbraid | string | No | Google wbraid. |
google_gbraid | string | No | Google gbraid. |
facebook_lead_id | string | No | Facebook instant-form lead ID (leadgen_id). Definitive Facebook ad attribution. See Forwarding Facebook Instant-Form Leads. |
campaign_id | string | No | Ad platform campaign ID (for Facebook leads, the Meta campaign ID - enables per-campaign ROAS reporting). |
facebook_ad_id | string | No | Meta ad ID (ad_id on a Facebook lead). Credits the lead to the specific ad, so it shows in the campaign's By set of ads results. |
brayv_campaign_id | string | No | The brayv_campaign_id URL parameter on a brayv.ai website ad's link. Send it (and the link's brayv_ad_id parameter as facebook_ad_id) if your own website form captures URL parameters on a page brayv.ai ads send people to. |
Forwarding Facebook Instant-Form Leads
If your team collects Facebook instant-form leads in its own system first (your CRM, Zapier, or an agency's tool) and forwards them here, include Meta's IDs with each lead. brayv.ai then treats the lead exactly like one it received from Facebook directly:
| Field | Needed | Facebook lead field | What it does in brayv.ai |
|---|---|---|---|
facebook_lead_id | Yes | id (also called leadgen_id) | Counts the lead as coming from a Facebook ad, and lets brayv.ai match the updates it sends Facebook (first reply, converted lead, job won) to that exact lead |
campaign_id | Yes | campaign_id | Credits the lead to its campaign in AI Ads reporting |
facebook_ad_id | Optional | ad_id | Credits the lead to the specific ad, so it appears in the campaign's By set of ads results |
Send both facebook_lead_id and campaign_id. Neither works on its own:
- With only
facebook_lead_id, the lead counts as a Facebook ad lead but isn't credited to any campaign, so it's missing from that campaign's results. - With only
campaign_id, the lead isn't recognized as coming from a Facebook ad at all.
Without facebook_ad_id, the lead still counts toward its campaign but shows as unmatched in By set of ads. Send Meta's numeric IDs exactly as Facebook provides them, not campaign or ad names.
Send these with the lead itself. Ad credit is decided when the lead is submitted, so adding them later through the Contact Update API doesn't change it.
Don't also connect the form in brayv.ai. brayv.ai only imports a Facebook form's leads directly once that form is mapped under Settings > Lead Sources; until then it skips them on purpose, so teams that forward leads themselves aren't double-counted. If you forward a form's leads through this API, leave that form unmapped in brayv.ai, or every lead will arrive twice. See Facebook Lead Forms.
The journey, assignment, and notifications come from the API lead source the request is sent to, like any other API lead.
Accepted timezone Values
US/Eastern
US/Central
US/Mountain
US/Pacific
US/Alaska
US/Hawaii
America/New_York
America/Detroit
America/Toronto
America/Montreal
America/Chicago
America/Winnipeg
America/Denver
America/Edmonton
America/Phoenix
America/Los_Angeles
America/Vancouver
America/Anchorage
Pacific/Honolulu
America/Halifax
America/St_Johns
If timezone is provided and is not one of these values, it is ignored and logged.
Custom Fields
Custom fields should be sent inside the custom_fields object.
{
"custom_fields": {
"service_type": "HVAC",
"lead_priority": "High"
}
}
The key must match the custom field ID configured in brayv.ai. Use the Custom Fields API to list an account's field IDs, types, and options.
Custom field validation:
| Custom field type | Expected value | Behavior |
|---|---|---|
text | string | Saved as submitted. |
number | number or numeric string | Saved if numeric, otherwise ignored. |
date | parseable date string | Saved as YYYY-MM-DD, otherwise ignored. |
select | string matching one configured option | Case-insensitive match. Non-matching values ignored. |
radio | string matching one configured option | Case-insensitive match. Non-matching values ignored. |
multiselect | string or string[] matching configured options | Matching options are saved. Non-matching options ignored. |
checkbox | boolean-like value | Accepts true, false, 1, 0, yes, no, y, n, on, off, t, f. |
Invalid optional custom field values do not fail the submission. They are ignored and logged in submission history.
Note
note is optional and attaches a single note to the contact. It is not a contact field; it creates a note on the contact's Notes panel in brayv.ai.
{
"note": {
"content": "Homeowner says the upstairs unit is the problem, downstairs is fine.",
"surface_to_agents": true
}
}
Tip: if surfacing to agents, and the note is derived from something like an open ended 'comments' section on a lead form, it's good practice to pass in that context, i.e.:
{
"note": {
"content": "Comment submitted on quote request form: the upstairs unit is the problem, downstairs is fine.",
"surface_to_agents": true
}
}
A plain string is also accepted as shorthand, and leaves surface_to_agents at its default of false:
{
"note": "Homeowner says the upstairs unit is the problem."
}
| Field | Type | Required | Notes |
|---|---|---|---|
content | string | Yes | The note text. Trimmed to 1000 characters if longer. |
surface_to_agents | boolean | No | Whether AI agents may read this note. Defaults to false. |
Note Visibility
surface_to_agents controls who can read the note:
| Value | Who sees the note |
|---|---|
false (default) | Your team only, in the contact's Notes panel. AI agents never see it. |
true | Your team, plus the AI text and voice agents, which receive the note as context about the contact. |
Send surface_to_agents: true only for context an AI agent should know, and keep it false for internal commentary you do not want repeated to the contact. An agent reads at most the 3 most recently edited surfaced notes on a contact, so a surfaced note can push an older one out of view.
Note Behavior
| Situation | Behavior |
|---|---|
| More than 1000 characters | Trimmed to 1000 characters. The submission still succeeds and the trim is reported in ignored_fields. |
Empty or whitespace-only content | No note is created. Ignored and logged. |
note is not a string or object, or content is not a string | No note is created. Ignored and logged. |
surface_to_agents is not a boolean | Note is created as internal-only (false) and the value is logged. |
| The contact already has a note with identical text | No duplicate note is created. Logged as a duplicate. |
| Submission is rejected for invalid required fields | No contact and no note are created. |
Notes created this way are labelled Integration in brayv.ai and show no author, since they did not come from a team member.
Idempotency
idempotency_key is optional.
When provided, repeated requests with the same API key, target account, and idempotency key within about 5 minutes return the same result instead of creating a second submission.
{
"idempotency_key": "external-lead-123"
}
Use a unique idempotency key per lead from your system.
Full Example Payload
{
"phone": "5551234567",
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"source": "angi",
"text_opt_in": true,
"call_opt_in": true,
"voice_ai_opt_in": false,
"has_voice_ai": true,
"idempotency_key": "angi-lead-12345",
"email": "[email protected]",
"business_name": "Doe Family Home",
"website": "https://example.com",
"timezone": "US/Eastern",
"additional_phones": ["+15557654321"],
"additional_emails": ["[email protected]"],
"street_address": "123 Main St",
"city": "Charlotte",
"state": "NC",
"zip_code": "28202",
"country": "US",
"tags": ["angi", "priority-lead"],
"friendly_source": "Angi",
"hook": "HVAC repair",
"service_description": "Customer says the AC is not cooling and wants service this week.",
"utm_source": "angi",
"utm_medium": "lead_partner",
"utm_campaign": "summer_hvac",
"utm_content": "ac_repair",
"utm_keyword": "emergency ac repair",
"referrer": "https://www.angi.com/",
"landing_page_url": "https://example.com/hvac",
"facebook_click_id": "fbclid-example",
"google_click_id": "gclid-example",
"google_wbraid": "wbraid-example",
"google_gbraid": "gbraid-example",
"facebook_lead_id": "1234567890123456",
"campaign_id": "120210000000000000",
"facebook_ad_id": "120210000000000123",
"custom_fields": {
"service_type": "HVAC",
"lead_priority": "High",
"preferred_service_date": "2026-06-15"
},
"note": {
"content": "Customer says the AC is not cooling upstairs only, and asked us to call before 5pm.",
"surface_to_agents": true
}
}
Example cURL
curl -X POST "https://api.brayv.ai/v1/accounts/ACCOUNT_ID/leads" \
-H "Authorization: Bearer brayv_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "5551234567",
"full_name": "Jane Doe",
"source": "angi",
"text_opt_in": true,
"call_opt_in": true,
"voice_ai_opt_in": false,
"idempotency_key": "angi-lead-12345",
"friendly_source": "Angi",
"hook": "HVAC repair",
"custom_fields": {
"service_type": "HVAC",
"lead_priority": "High"
}
}'
Error Example
Invalid required fields return a validation error. The rejected submission is still recorded in submission history.
{
"detail": {
"message": "Invalid lead submission",
"submission_id": "e7ac371f-478e-4290-b147-524a0f491ab2",
"validation_errors": [
{
"field": "phone",
"reason": "Phone must be one of: +1XXXXXXXXXX, 1XXXXXXXXXX, or XXXXXXXXXX where the leading digit is not 1"
}
]
}
}