Leads and Contacts
Manage the tenant's unified contact list: add a single contact with consent, search across leads plus their recent call and SMS history, bulk-import into a campaign list, and run per-lead CRUD. Phone numbers are normalized to E.164 on every write.
Add a single contact
POST /api/v1/contacts adds a single contact (lead) with tenant-wide phone dedup. Scope: write_crm.
{
"first_name": "Jane",
"last_name": "Doe",
"phone": "+15551234567",
"email": "jane@example.com",
"company_name": "Acme LLC",
"consent_acknowledged": true,
"source": "website-form",
"tags": ["vip", "warm-lead"]
}
first_nameandphoneare required (E.164 preferred; 10-digit US numbers are auto-normalized).consent_acknowledged: trueis REQUIRED. Omitting it or setting it tofalsereturns422. The audit trail is written tocustom_fields.consentfor TCPA traceability.sourcedefaults toapi-manual-add;tagsare appended to defaults.
200 for both new and duplicate: branch on already_existed
Both outcomes return 200 OK. Branch on already_existed, not the status code. If the phone already exists anywhere in the tenant's leads, the existing lead_id is returned instead of creating a duplicate.
{
"lead_id": "uuid",
"phone": "+15551234567",
"already_existed": false,
"first_name": "Jane",
"last_name": "Doe",
"consent_captured_at": "2026-04-19T15:00:00Z"
}
{
"lead_id": "uuid",
"phone": "+15551234567",
"already_existed": true,
"first_name": "Jane",
"last_name": "Doe",
"consent_captured_at": null
}
Setting consent_acknowledged: true without actual prior consent from the contact is a compliance violation. The API trusts the caller but logs captured_at and captured_by for audit.
Search leads (with history)
GET /api/v1/leads/search searches leads by name, company, or phone and returns recent call and SMS history per match. Scope: read.
| Parameter | Description |
|---|---|
query | Free-text match against first_name, last_name, company_name, and (if 4+ digits) phone suffix |
phone | Exact phone match (normalized to E.164 before lookup); use for precise caller lookups |
limit | Max leads returned (1-50, default 10) |
history_limit | Max call plus SMS entries per lead (1-20, default 5) |
At least one of query or phone must be provided. Results are sorted by last_contacted_at (descending, nulls last). Customer-submitted strings (customer_name, SMS body) are wrapped in <tool_data>...</tool_data> tags so downstream AI consumers treat them as data, not instructions.
curl "https://app.autorev.ai/api/v1/leads/search?phone=%2B15551234567&history_limit=3" \
-H "Authorization: Bearer ar_live_..."
Each matched lead includes recent_calls (with outcome, duration_sec, timestamps) and recent_sms (with direction, body, status), plus lead fields like status, source, lead_score, first_seen_at, and last_contact_at. The response echoes query_used.
Bulk import (list-scoped)
POST /api/v1/leads/bulk upserts up to 1000 leads in a single call. Scope: write_crm. It auto-creates a lead list, so the returned list_id can target a subsequent POST /api/v1/campaigns.
Duplicates within a list (same tenant + list + phone) are silently skipped and reported under skipped. The same phone CAN appear in multiple lists. If you want one row per phone across the whole tenant, use POST /api/v1/contacts instead.
{
"leads": [
{
"phone": "+15551234567",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"address": "123 Main St",
"city": "Orlando",
"state": "FL",
"zip": "32801",
"source": "spring-promo-2026",
"tags": ["vip", "warranty"],
"notes": "Prefers morning appointments",
"chatbot_enabled": false,
"company_name": "Acme LLC",
"external_id": "crm_7890"
}
],
"list_name": "Spring Promo",
"list_description": "From our CRM",
"tag": "spring-promo-2026",
"source": "api-bulk-import"
}
leads is required (1-1000 items); within each lead only phone is required (any format, normalized). list_name defaults to API Import YYYY-MM-DD, source defaults to api-bulk-import, tag is applied to every lead, and chatbot_enabled adds a chatbot-enabled tag.
{
"list_id": "a1b2c3d4-...",
"list_name": "Spring Promo",
"inserted": 485,
"skipped": [
{ "phone": "+15551234567", "reason": "duplicate_within_batch" },
{ "phone": "abc", "reason": "invalid_phone_format" }
],
"lead_ids": ["uuid-1", "uuid-2", "..."],
"total_submitted": 500
}
Skip reasons: invalid_phone_format, duplicate_within_batch, duplicate_in_list, insert_error.
Leads CRUD and tags
List leads
GET /api/v1/leads returns a paginated lead list with simple filters: phone, email, tag, list_id, status.
curl -H "Authorization: Bearer ${AUTOREV_API_KEY}" \
"https://app.autorev.ai/api/v1/leads?tag=spring-promo&limit=25"
{
"data": [
{
"id": "4b3e...",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+15551234567",
"email": "jane@example.com",
"status": "ready",
"tags": ["spring-promo", "chatbot-enabled"],
"created_at": "2026-04-18T14:02:11Z"
}
],
"has_more": true,
"cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNC0xOFQxMzoyMDowMFoiLCJpZCI6IjRiM2UuLi4ifQ=="
}
Get a lead
GET /api/v1/leads/:id returns lead detail with the last 10 SMS messages inline:
{
"lead": { "id": "4b3e...", "phone": "+15551234567", "tags": ["spring-promo"] },
"recent_messages": [
{ "id": "...", "direction": "inbound", "body": "yes interested", "created_at": "2026-04-19T10:12:00Z" }
]
}
Update a lead
PATCH /api/v1/leads/:id performs a partial update. custom_fields is merged: send null for a key to delete it.
curl -X PATCH -H "Authorization: Bearer ${AUTOREV_API_KEY}" \
-H "Content-Type: application/json" \
https://app.autorev.ai/api/v1/leads/4b3e... \
-d '{
"tags": ["spring-promo", "replied"],
"custom_fields": { "last_contacted_via": "sms", "notes": "Called back, wants quote" },
"chatbot_enabled": true
}'
Tag operations
POST /api/v1/leads/:id/tags atomically adds and/or removes tags. At least one of add/remove must be non-empty.
curl -X POST -H "Authorization: Bearer ${AUTOREV_API_KEY}" \
-H "Content-Type: application/json" \
https://app.autorev.ai/api/v1/leads/4b3e.../tags \
-d '{ "add": ["vip"], "remove": ["cold"] }'
Delete a lead (soft delete)
DELETE /api/v1/leads/:id soft-deletes: it sets status=opted_out plus opted_out_at. The row is preserved so TCPA records persist.
{ "id": "4b3e...", "deleted": true, "status": "opted_out" }
Lead Lists
Lead lists group leads for campaign targeting. A lead can appear in multiple lists: "attaching" a lead to a new list clones the row with a new list_id so the source row stays intact.
Create a list
POST /api/v1/lead-lists:
curl -X POST -H "Authorization: Bearer ${AUTOREV_API_KEY}" \
-H "Content-Type: application/json" \
https://app.autorev.ai/api/v1/lead-lists \
-d '{ "name": "Spring Promo 2026", "description": "Existing customers, wireless phones only" }'
{
"list": {
"id": "d2c1...",
"name": "Spring Promo 2026",
"total_leads": 0,
"usable_leads": 0,
"created_at": "2026-04-22T13:00:00Z"
}
}
List and get
GET /api/v1/lead-listsreturns a paginated list of all lead lists in the tenant.GET /api/v1/lead-lists/:idreturns list detail with a live lead count (not the denormalized counter):
{
"list": { "id": "d2c1...", "name": "Spring Promo 2026", "total_leads": 412 },
"lead_count": 412
}
Attach leads to a list
POST /api/v1/lead-lists/:id/leads attaches existing leads (by id) to the list. In-list duplicates are skipped silently.
curl -X POST -H "Authorization: Bearer ${AUTOREV_API_KEY}" \
-H "Content-Type: application/json" \
https://app.autorev.ai/api/v1/lead-lists/d2c1.../leads \
-d '{ "lead_ids": ["4b3e...", "7fa2..."] }'
{
"list_id": "d2c1...",
"attached": 2,
"new_lead_ids": ["9aa1...", "a4d2..."],
"skipped": [],
"total_submitted": 2
}
Remove and delete
DELETE /api/v1/lead-lists/:id/leads/:leadIdremoves one lead row from a list. Surgical: it does not touch copies of the same phone in other lists.DELETE /api/v1/lead-lists/:iddeletes a lead list and cascades to all attached lead rows.