Audiencias
Segmentos de contactos definidos por condiciones, con sus conteos y su superposición.
Descarga la proyección completa de la API pública: openapi.json.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /audiences | List saved audiences |
POST | /audiences | Save an audience |
DELETE | /audiences/{id} | Delete an audience |
GET | /audiences/{id} | Fetch one audience |
PATCH | /audiences/{id} | Update an audience |
GET | /audiences/{id}/counts | Size and reach per channel |
POST | /audiences/{id}/duplicate | Duplicate an audience |
POST | /audiences/{id}/overrides | Add or remove people by hand |
GET | /audiences/condition-data | Which data-dependent conditions can match anyone |
GET | /audiences/overlap | Overlap of two audiences |
GET | /audiences/stats | Sizes and reach for several audiences |
GET /audiences
List saved audiences
Every audience as a summary, newest first, with meta.total: the filter, its derived kind (dynamic rule · static hand-picked list · mixed · all = the whole directory), the override list SIZES (not the lists), labels resolving every tag / pipeline / stage / custom field the filter names (labels.missing lists references that no longer exist — they match nobody), and campaign usage. No membership is computed here; ask GET /audiences/stats?ids= for sizes. q searches the name; kind filters.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
q | query | string | no | máx. 200 |
kind | query | dynamic \ | static \ | mixed \ |
curl https://api.vitrinadev.com/api/v1/audiences \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"name": "Pacientes con control anual",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
]
},
"kind": "dynamic",
"include_count": 0,
"exclude_count": 0,
"labels": {
"tags": {
"7a7a7a7a-0000-4000-8000-0000000000e1": "Control anual"
},
"pipelines": {},
"stages": {},
"custom_attributes": {},
"interest_topics": {},
"missing": {
"tags": [],
"pipeline_id": null,
"stage_id": null,
"custom_attributes": [],
"interest_topics": []
}
},
"usage": {
"campaigns_count": 2,
"last_used_at": "2026-09-20T12:00:00.000Z",
"active_campaigns_count": 1
},
"created_by": "7a7a7a7a-0000-4000-8000-000000000011",
"created_at": "2026-09-01T10:00:00.000Z",
"updated_at": "2026-09-15T10:00:00.000Z"
}
],
"meta": {
"total": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /audiences
Save an audience
Only name is required. The filter is stored exactly as sent — no clause is added. An audience with no filter and no includes is valid and means every contact in the workspace — deliberate, but worth a confirmation step in a UI before it is pointed at a campaign.
Filter clauses: lifecycle_stages, tag_ids_any / tag_ids_all / tag_ids_none, countries, languages, has_channel, pipeline_id (+ optional stage_id, matching contacts with an OPEN lead there), created_after / created_before, birthday_months / birthday_within_days (the workspace’s local today), vehicle_brands / vehicle_models / vehicle_years (the customer’s OWN current car, contact.vehicle_* — what they asked for is interests), and custom_attributes ([\{ key, values?: string[] \}] — any-of, trimmed, case- and accent-insensitive; value is accepted as values: [value]; neither means "has any value"), interests (\{ topic_ids, within_days?, min_strength?, include_past? \} — holds a projected interest in any of the topics from GET /interest-topics; a brand matches its models; min_strength 2 asked · 3 booked/quoted · 4 bought; by default only current, not superseded, interests) and interests_none (\{ topic_ids, min_strength?, include_past? \} — holds none of them; include_past defaults to true here, so «ya compró» survives supersession). A merged topic id is followed to its survivor. Clauses AND together; values inside one clause are any-of. The object is strict: an unrecognised key is a 400 rather than being ignored, so a typo cannot quietly widen an audience to everyone.
The two override lists are capped at 2.000 ids each and every id must be a contact of this workspace (422 unknown_contact_ids otherwise). Answers 201 with the audience detail.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
name | string | sí | mín. 1, máx. 200 |
description | string | null | — | máx. 2000 |
filter | object | — | |
include_contact_ids | uuid[] | — | |
exclude_contact_ids | uuid[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/audiences \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pacientes con control anual",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
]
},
"exclude_contact_ids": [
"7a7a7a7a-0000-4000-8000-0000000000f2"
]
}'Ejemplo de respuesta (201)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"name": "Pacientes con control anual",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
]
},
"kind": "dynamic",
"include_count": 0,
"exclude_count": 1,
"labels": {
"tags": {
"7a7a7a7a-0000-4000-8000-0000000000e1": "Control anual"
},
"pipelines": {},
"stages": {},
"custom_attributes": {},
"interest_topics": {},
"missing": {
"tags": [],
"pipeline_id": null,
"stage_id": null,
"custom_attributes": [],
"interest_topics": []
}
},
"usage": {
"campaigns_count": 0,
"last_used_at": null,
"active_campaigns_count": 0,
"active_campaigns": []
},
"created_by": "7a7a7a7a-0000-4000-8000-000000000011",
"created_at": "2026-09-01T10:00:00.000Z",
"updated_at": "2026-09-15T10:00:00.000Z",
"include_contact_ids": [],
"exclude_contact_ids": [
"7a7a7a7a-0000-4000-8000-0000000000f2"
]
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /audiences/{id}
Delete an audience
Removes the definition. Contacts are untouched — an audience is a saved query, so deleting one never deletes anybody. Refused with 409 CONFLICT, details.reason = audience_in_use (and details.campaigns) while a draft, scheduled, sending or paused campaign uses it — move that campaign to another audience or cancel it first. Sent and cancelled campaigns keep their recipient list and do not block. 404 for an id that is not there.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl -X DELETE https://api.vitrinadev.com/api/v1/audiences/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /audiences/{id}
Fetch one audience
The summary plus both override lists (for API round-trips) and the campaigns that will still send to it (usage.active_campaigns: draft, scheduled, sending or paused). Membership is not included — see /\{id\}/members and /\{id\}/counts.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/audiences/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"name": "Pacientes con control anual",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
]
},
"kind": "dynamic",
"include_count": 0,
"exclude_count": 0,
"labels": {
"tags": {
"7a7a7a7a-0000-4000-8000-0000000000e1": "Control anual"
},
"pipelines": {},
"stages": {},
"custom_attributes": {},
"interest_topics": {},
"missing": {
"tags": [],
"pipeline_id": null,
"stage_id": null,
"custom_attributes": [],
"interest_topics": []
}
},
"usage": {
"campaigns_count": 2,
"last_used_at": "2026-09-20T12:00:00.000Z",
"active_campaigns_count": 1,
"active_campaigns": [
{
"id": "7a7a7a7a-0000-4000-8000-0000000000c1",
"name": "Recordatorio de control",
"status": "sending",
"scheduled_at": null
}
]
},
"created_by": "7a7a7a7a-0000-4000-8000-000000000011",
"created_at": "2026-09-01T10:00:00.000Z",
"updated_at": "2026-09-15T10:00:00.000Z",
"include_contact_ids": [],
"exclude_contact_ids": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /audiences/{id}
Update an audience
Only the fields present are applied; at least one is required. filter, include_contact_ids and exclude_contact_ids are each REPLACED wholesale when sent, never merged — send filter: \{\} and the audience widens to every contact. To add or remove single people use POST /audiences/\{id\}/overrides instead.
Concurrency: send if_updated_at (the updated_at you loaded) and a newer edit answers 409 VERSION_CONFLICT with details.current_updated_at instead of being overwritten.
Editing takes effect on the next read. Campaigns that already sent are unaffected (they snapshot their recipients), but a scheduled campaign pointed at this audience will use the NEW definition when it runs.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
name | string | — | mín. 1, máx. 200 |
description | string | null | — | máx. 2000 |
filter | object | — | |
include_contact_ids | uuid[] | — | |
exclude_contact_ids | uuid[] | — | |
if_updated_at | string | — | date-time |
curl -X PATCH https://api.vitrinadev.com/api/v1/audiences/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pacientes con control anual (Chile)",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
],
"languages": [
"es"
]
},
"if_updated_at": "2026-09-15T10:00:00.000Z"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"name": "Pacientes con control anual (Chile)",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {
"lifecycle_stages": [
"customer"
],
"tag_ids_any": [
"7a7a7a7a-0000-4000-8000-0000000000e1"
],
"countries": [
"Chile"
],
"has_channel": [
"whatsapp"
],
"languages": [
"es"
]
},
"kind": "dynamic",
"include_count": 0,
"exclude_count": 0,
"labels": {
"tags": {
"7a7a7a7a-0000-4000-8000-0000000000e1": "Control anual"
},
"pipelines": {},
"stages": {},
"custom_attributes": {},
"interest_topics": {},
"missing": {
"tags": [],
"pipeline_id": null,
"stage_id": null,
"custom_attributes": [],
"interest_topics": []
}
},
"usage": {
"campaigns_count": 2,
"last_used_at": "2026-09-20T12:00:00.000Z",
"active_campaigns_count": 1,
"active_campaigns": [
{
"id": "7a7a7a7a-0000-4000-8000-0000000000c1",
"name": "Recordatorio de control",
"status": "sending",
"scheduled_at": null
}
]
},
"created_by": "7a7a7a7a-0000-4000-8000-000000000011",
"created_at": "2026-09-01T10:00:00.000Z",
"updated_at": "2026-10-01T14:00:00.000Z",
"include_contact_ids": [],
"exclude_contact_ids": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /audiences/{id}/counts
Size and reach per channel
total members; by_source (rule / manual / both); overrides (list sizes, includes that are not members, exclusions that would otherwise match); omitted (match the rule but are not members: merged, blocked, spam, archived, excluded by hand, import-only); and channels.whatsapp / channels.email: reachable, no_identifier, invalid_identifier (+ invalid_by), duplicate, opted_out (+ opted_out_by reason). Per channel the five add up to total, and reachable is the number to show — a campaign writes reachable + opted_out recipient rows and sends to reachable.
partial: true means the audience is larger than cap (the most a campaign sends to); the counts themselves are exact. Cached 5 minutes (computed_at); fresh=1 recomputes. whatsapp_eligible / whatsapp_suppressed / suppressed_is_partial are transitional and will be removed.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
fresh | query | 1 \ | true | no |
curl https://api.vitrinadev.com/api/v1/audiences/<id>/counts \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"total": 512,
"by_source": {
"rule": 500,
"manual": 8,
"rule_and_manual": 4
},
"overrides": {
"included": 12,
"included_not_member": 1,
"excluded": 3,
"excluded_would_match": 3
},
"omitted": {
"archived": 4,
"blocked": 1,
"spam": 0,
"merged": 2,
"import_only": 0,
"excluded": 3
},
"channels": {
"whatsapp": {
"reachable": 470,
"no_identifier": 0,
"invalid_identifier": 0,
"duplicate": 0,
"opted_out": 0,
"opted_out_by": {},
"invalid_by": {}
},
"email": {
"reachable": 301,
"no_identifier": 0,
"invalid_identifier": 0,
"duplicate": 0,
"opted_out": 0,
"opted_out_by": {},
"invalid_by": {}
}
},
"computed_at": "2026-10-01T15:00:00.000Z",
"partial": false,
"cap": 100000,
"whatsapp_eligible": 470,
"whatsapp_suppressed": 0,
"suppressed_is_partial": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /audiences/{id}/duplicate
Duplicate an audience
A copy named «Copia de {name}» (or name). freeze: true instead saves TODAY’s members as a fixed list (no filter, includes = the members) — up to 2.000 people; larger is a 422 with details.reason = freeze_too_large. Answers 201 with the new audience.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
name | string | — | mín. 1, máx. 200 |
freeze | boolean | — | por defecto false |
curl -X POST https://api.vitrinadev.com/api/v1/audiences/<id>/duplicate \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pacientes con control anual (congelada)",
"freeze": true
}'Ejemplo de respuesta (201)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a2",
"name": "Pacientes con control anual (congelada)",
"description": "Clientes con la etiqueta de control anual en Chile.",
"filter": {},
"kind": "static",
"include_count": 512,
"exclude_count": 0,
"labels": {
"tags": {
"7a7a7a7a-0000-4000-8000-0000000000e1": "Control anual"
},
"pipelines": {},
"stages": {},
"custom_attributes": {},
"interest_topics": {},
"missing": {
"tags": [],
"pipeline_id": null,
"stage_id": null,
"custom_attributes": [],
"interest_topics": []
}
},
"usage": {
"campaigns_count": 0,
"last_used_at": null,
"active_campaigns_count": 0,
"active_campaigns": []
},
"created_by": "7a7a7a7a-0000-4000-8000-000000000011",
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-01T14:20:00.000Z",
"include_contact_ids": [
"7a7a7a7a-0000-4000-8000-0000000000f1",
"7a7a7a7a-0000-4000-8000-0000000000f2"
],
"exclude_contact_ids": []
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /audiences/{id}/overrides
Add or remove people by hand
action: include (always in, even when the rule does not match; drops them from the exclusions), exclude (never in; drops them from the includes) or reset (back to "the rule decides"). Send contact_ids or identifiers (phones / emails of this workspace’s contacts; include/exclude only), up to 500 per call. Atomic against the stored lists. Each input gets an outcome: added, removed, already, not_found, moderated (merged/archived/blocked/spam — never a member, so not included) or already_member_by_rule (stored anyway, so they stay if the rule changes). A list past 2.000 ids is a 422 with details.reason = override_cap.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
action | include \ | exclude \ | reset |
contact_ids | uuid[] | — | |
identifiers | string[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/audiences/<id>/overrides \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "include",
"contact_ids": [
"7a7a7a7a-0000-4000-8000-0000000000f1",
"7a7a7a7a-0000-4000-8000-0000000000f2"
]
}'Ejemplo de respuesta (200)
{
"data": {
"applied": 1,
"outcomes": [
{
"contact_id": "7a7a7a7a-0000-4000-8000-0000000000f1",
"result": "added"
},
{
"contact_id": "7a7a7a7a-0000-4000-8000-0000000000f2",
"result": "already_member_by_rule"
}
],
"include_count": 1,
"exclude_count": 0,
"updated_at": "2026-10-01T14:10:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /audiences/condition-data
Which data-dependent conditions can match anyone
What the audience editor needs to decide which conditions to offer: vehicle is true when some live contact of the workspace carries its own vehicle (brand, model or year) — without that data the vehicle_* clauses can only match nobody. One indexed EXISTS; cheap enough to call when the editor opens.
curl https://api.vitrinadev.com/api/v1/audiences/condition-data \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"vehicle": true
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /audiences/overlap
Overlap of two audiences
ids=a,b → members in both, only in A, only in B. Useful to check that two A/B test groups do not share people.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
ids | query | uuid[] | sí |
curl https://api.vitrinadev.com/api/v1/audiences/overlap \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"a": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"total": 512
},
"b": {
"id": "7a7a7a7a-0000-4000-8000-0000000000a2",
"total": 340
},
"both": 120,
"a_only": 392,
"b_only": 220
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /audiences/stats
Sizes and reach for several audiences
For up to 50 comma-separated ids: total members and how many are reachable on WhatsApp and on email — the list page’s columns. Shares the 5-minute cache of /\{id\}/counts (fresh=1 bypasses it). Unknown or foreign ids are omitted.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
ids | query | uuid[] | sí | |
fresh | query | 1 \ | true | no |
curl https://api.vitrinadev.com/api/v1/audiences/stats \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-0000000000a1",
"total": 512,
"whatsapp_reachable": 470,
"email_reachable": 301,
"computed_at": "2026-10-01T15:00:00.000Z",
"partial": false
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429