VitrinaAPI

Audiencias

Segmentos de contactos definidos por condiciones, con sus conteos y su superposición.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

Descarga la proyección completa de la API pública: openapi.json.

MétodoRutaQué hace
GET/audiencesList saved audiences
POST/audiencesSave an audience
DELETE/audiences/{id}Delete an audience
GET/audiences/{id}Fetch one audience
PATCH/audiences/{id}Update an audience
GET/audiences/{id}/countsSize and reach per channel
POST/audiences/{id}/duplicateDuplicate an audience
POST/audiences/{id}/overridesAdd or remove people by hand
GET/audiences/condition-dataWhich data-dependent conditions can match anyone
GET/audiences/overlapOverlap of two audiences
GET/audiences/statsSizes 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ámetroEnTipoObligatorioRestricciones
qquerystringnomáx. 200
kindquerydynamic \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

CampoTipoObligatorioRestricciones
namestringsímín. 1, máx. 200
descriptionstring | null—máx. 2000
filterobject—
include_contact_idsuuid[]—
exclude_contact_idsuuid[]—
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
namestring—mín. 1, máx. 200
descriptionstring | null—máx. 2000
filterobject—
include_contact_idsuuid[]—
exclude_contact_idsuuid[]—
if_updated_atstring—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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
freshquery1 \trueno
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
namestring—mín. 1, máx. 200
freezeboolean—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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
actioninclude \exclude \reset
contact_idsuuid[]—
identifiersstring[]—
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ámetroEnTipoObligatorioRestricciones
idsqueryuuid[]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ámetroEnTipoObligatorioRestricciones
idsqueryuuid[]sí
freshquery1 \trueno
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

En esta página