Customers
The people being called. Campaigns select them by tag; inbound calls match them by number so a returning caller is recognised.
List customers
GET /api/voice/customers?search=priya&tags=winback,north&limit=50&offset=0
| Param | Notes |
|---|---|
search | Matches name, phone, or email |
tags | Comma-separated or repeated. Matches any of them |
limit / offset | Default 50 / 0 |
{
"customers": [
{
"uuid": "c1d2e3f4-a5b6-7890-abcd-ef1234567890",
"name": "Priya Kumar",
"phone": "+918888888888",
"email": "[email protected]",
"tags": ["winback", "north"],
"preferred_language": "hi-IN",
"metadata": { "cart_value": 850, "product_interest": "Standing Desk Pro" },
"total_calls": 3,
"last_contact": "2026-09-20T09:15:44Z",
"created_at": "2026-08-02T10:00:00Z",
"updated_at": "2026-09-20T09:15:44Z"
}
],
"total": 240
}
Ordered by last_contact descending, so whoever was spoken to most recently is
first.
Get a customer
GET /api/voice/customers/{uuid}
Create a customer
POST /api/voice/customers
{
"name": "Priya Kumar",
"phone": "+918888888888",
"email": "[email protected]",
"tags": ["winback", "north"],
"preferred_language": "hi-IN",
"metadata": { "cart_value": 850 }
}
| Field | Required | Notes |
|---|---|---|
name | ✓ | |
phone | ✓* | E.164. *Either phone or email is required |
email | ✓* | |
tags | — | Drive campaign targeting and visibility scoping |
preferred_language | — | Overrides the agent's language for this person |
metadata | — | Arbitrary JSON, substitutable into scripts |
| Status | Cause |
|---|---|
400 | {"detail": "name is required"} |
400 | {"detail": "provide a phone number or an email"} |
Uniqueness is on the exact phone string, while call matching uses the last ten
digits. +918888888888 and 8888888888 are stored as two customers, and an
inbound call attaches to whichever the query returns first. Normalise to E.164
before importing.
Re-adding an exact duplicate returns 500 {"detail": "internal error"} rather
than a 409.
Update a customer
PATCH /api/voice/customers/{uuid}
Same fields; send only what changes.
Fields are applied with COALESCE, so email and preferred_language cannot
be cleared once set — only replaced. Sending "tags": [] does clear tags.
Delete a customer
DELETE /api/voice/customers/{uuid} → 204
Bulk import
POST /api/voice/customers/bulk
{
"customers": [
{ "name": "Priya Kumar", "phone": "+918888888888", "tags": ["winback"] },
{ "name": "Arjun Rao", "phone": "+919999999999", "tags": ["winback"] }
]
}
At most 1000 per request. Upserts on (organization_id, phone), so
re-importing a list updates rather than duplicates.
{
"imported": 898,
"failed": [
{ "index": 41, "detail": "name and phone are required" },
{ "index": 502, "detail": "could not be saved" }
]
}
Partial success is reported rather than rolled back: a 900-row import with two
bad rows lands the 898, and the response names which failed so they can be
corrected and resent. index is the position in the array you sent.
Both name and phone are required for bulk rows — unlike single creation,
email-only contacts are rejected.
Tags
GET /api/voice/customers/meta/tags
{ "tags": ["north", "south", "winback"] }
Every distinct tag in use, for populating a filter.
Tags do two jobs:
- Campaign targeting — an outbound flow's
branchesselects by tag - Visibility scoping — a restricted user sees only customers carrying their assigned tags, and only the calls belonging to those customers
Customer memory
GET /api/voice/customers/{uuid}/memory
Returns the whole per-scope memory map as stored — what the agent has retained about this person across calls. Unfiltered by design: this is the operator's complete view, which is what a privacy request needs.
{
"sales": { "prefers_evening_calls": true, "last_objection": "price" }
}
Click-to-call
POST /api/voice/customers/{uuid}/calls
{
"call_agent_uuid": "us1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"datasource_ids": ["kb1a2b3c-…"]
}
| Field | Notes |
|---|---|
call_agent_uuid | The user UUID of the teammate who should take the call |
datasource_ids | Knowledge base documents available for live assistance |
Dials this customer from a human teammate's phone rather than the AI agent — that teammate's own number rings first, then the customer. They must have a phone number mapped; see Telephony.
Access scoping
| Caller | Sees |
|---|---|
| Unrestricted | Every customer in the organization |
| Tag-restricted | Only customers carrying at least one assigned tag |
| No tags assigned and restricted | Nothing |
The scope is applied in SQL, so a restricted user's pagination and totals are correct rather than short.