---
title: "Vista previa de audiencia combinando múltiples definiciones (NUEVO)"
method: POST
path: "/segments/audience-preview"
tags: ["Segmentation"]
---

# Vista previa de audiencia combinando múltiples definiciones (NUEVO)

`POST /segments/audience-preview`

## Request

- Base URL: `http://localhost:8081`
- URL: `http://localhost:8081/segments/audience-preview`
- Auth: HTTP bearer

Combina hasta 20 definiciones DSL con **UNION DISTINCT** — un fan que
cumpla CUALQUIERA de las definiciones cuenta una sola vez en
`unique_total` — y devuelve el alcance de la audiencia combinada más su
contactabilidad por canal, para un `consent_mode` dado. Evita tener que
hacer N llamadas a `/segments/preview` cuando se quiere estimar el
alcance de varios segmentos combinados (p.ej. antes de lanzar una
campaña multi-segmento).

**Rate limit**: reutiliza el límite de `/segments/preview` (50 req/min)
— es un endpoint de solo lectura con el mismo perfil de coste (sin
paginación, sin exportación de datos), solo que evaluado sobre N
definiciones en lugar de una.

**`consent_mode`** (`MARKETING` por defecto, o `TRANSACTIONAL`):
- `MARKETING`: idéntico a `/segments/preview` — elegible por email
  significa `email_marketing_status = 'ok'`.
- `TRANSACTIONAL`: `opt_out` y `unknown` siguen siendo alcanzables para
  email transaccional/de servicio (solo `no_email`/`invalid_email`
  excluyen); `email_exclusions.opt_out` se omite en este modo.

El consentimiento se evalúa **solo contra el tenant con el que
autenticó la request**, nunca contra la jerarquía completa de tenants
(misma regla que `/segments/preview` y `/segments/execute`).

**Errores de validación (400)**: se reporta la primera violación
encontrada, con el índice de la definición ofensiva cuando aplica
(`definitions[i]`):
- `definitions` vacío, no-array, o con más de 20 elementos
- `channels` vacío, no-array, con duplicados, o con un valor fuera de
  `EMAIL`/`SMS`/`PUSH`
- `consent_mode` distinto de `MARKETING`/`TRANSACTIONAL`
- Cualquier `definitions[i]` con una estructura DSL inválida

## Request body

- AudiencePreviewRequest
  - `definitions` Definition[], required: De 1 a 20 definiciones DSL (mismo formato que `/segments/preview` y `/segments/execute`). Se combinan con UNION DISTINCT: un fan que cumpla CUALQUIERA de las definiciones cuenta una sola vez en `unique_total`.
    - `stanzas` Stanza[], required: Stanzas unidas con AND
      - `id` string, required: UUID para identificación en UI
      - `criteria` Criterion[], required: Criterios unidos con OR
        - `id` string, required: UUID para identificación en UI
        - `type` string: Tipo de criterio (obsoleto, usar criterion_id)
        - `criterion_id` string, required: ID del criterio global
        - `operator` 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'is_true' | 'is_false' | 'before' | 'after' | 'between' | 'is' | 'is_not' | 'is_set' | 'is_not_set', required: Operadores disponibles según tipo de dato: **TEXT**: eq, neq, contains, not_contains, starts_with, ends_with, is_set, is_not_set **NUMBER**: eq, neq, gt, gte, lt, lte, is_set, is_not_set **BOOLEAN**: is_true, is_false **DATE**: before, after, between, eq, is_set, is_not_set **ENTITY**: is, is_not, is_set, is_not_set **CURRENCY**: eq, neq, gt, gte, lt, lte, is_set, is_not_set
        - `value` union, required: Valor del criterio (tipo depende del data_type)
          - string
          - number
          - boolean
        - `timeframe` union: Marco temporal para el criterio
          - TimeFrameRelativeToNow
            - `type` 'relative_to_now', required
            - `unit` 'days' | 'hours' | 'weeks' | 'months', required
            - `value_start` integer, required: Inicio del rango (0 = ahora)
            - `value_end` integer, required: Fin del rango
          - TimeFrameRelativeToExperience
            - `type` 'relative_to_experience', required
            - `experienceId` string, required: ID del evento de referencia
            - `phase` 'before_start' | 'after_start' | 'before_end' | 'after_end', required
            - `unit` 'days' | 'hours' | 'weeks', required
            - `value` integer, required
          - TimeFrameAbsolute
            - `type` 'absolute', required
            - `start_date` string, date, required: Fecha de inicio (ISO 8601)
            - `end_date` string, date, required: Fecha de fin (ISO 8601)
        - `filters` object: Filtros adicionales (ej. property_name para custom fields)
        - `modifiers` Modifiers
          - `distance` DistanceModifier
            - `radius` number, required: Radio en kilómetros
            - `unit` 'km' | 'mi'
          - `frequency` FrequencyModifier
            - `count` integer, required: Número de veces
            - `operator` 'exactly' | 'at_least' | 'at_most', required: Operador de comparación
          - `time_range` TimeRangeModifier: Ventana relativa, recalculada en cada evaluación. En criterios cuya fecha está en otra tabla (`attended_event` → `events.start_ts`) se evalúa sobre el mismo registro que cumple el criterio, en días naturales de Europe/Madrid y con ambos límites incluidos: `past` = desde las 00:00 de hoy − N unidades hasta ahora (sin eventos futuros); `future` = desde ahora hasta el final de hoy + N unidades. Semanas = 7·N días; meses y años, aritmética de calendario.
            - `value` integer, required: Valor numérico
            - `unit` 'days' | 'weeks' | 'months' | 'years', required
            - `direction` 'past' | 'future'
        - `displayValue` unknown
        - `metadata` object: Metadata adicional para UI
          - `criteriaName` string
          - `dataType` string
          - `category` string
    - `limit` integer: Límite opcional de resultados (caso de uso warm-up de campañas). Máximo 100000. Si se especifica offset, limit es obligatorio.
    - `offset` integer: Saltar N resultados antes de aplicar limit (skip+limit para warm-up). Debe ser >= 0. Requiere que limit también esté definido. Orden de aplicación: groups → offset/limit → page/page_size HTTP.
  - `channels` string[], required: Subconjunto de canales a evaluar, sin duplicados.
  - `consent_mode` 'MARKETING' | 'TRANSACTIONAL': `MARKETING` (default): elegibilidad de email idéntica a `/segments/preview` (`email_marketing_status = 'ok'`). `TRANSACTIONAL`: `opt_out` y `unknown` siguen siendo alcanzables para email transaccional/de servicio; solo `no_email`/`invalid_email` excluyen. `email_exclusions.opt_out` se omite en este modo.

## Response `200`

Audience preview generado exitosamente

- AudiencePreviewResponse
  - `success` boolean, required
  - `unique_total` integer, required: Fans distintos a través de todas las `definitions` (UNION, contados una vez).
  - `eligible` AudienceChannelCounts, required: Conteo de elegibles por canal solicitado (solo aparecen las claves de los canales pedidos).
    - `email` integer
    - `sms` integer
    - `push` integer
  - `eligible_any_channel` integer, nullable, required: Alcanzables por al menos uno de los canales solicitados, o `null` (ver `eligible`).
  - `ineligible_all_channels` integer, nullable, required: `unique_total - eligible_any_channel`, o `null` (ver `eligible`).
  - `email_exclusions` AudienceEmailExclusions: Solo presente cuando 'EMAIL' está entre los canales solicitados. `opt_out` se omite cuando `consent_mode` es `TRANSACTIONAL`.
    - `no_email` integer, required
    - `invalid_email` integer, required
    - `opt_out` integer: Omitido para consent_mode=TRANSACTIONAL
    - `unknown` integer, required
  - `computed_at` string, date-time, required
  - `data_as_of` string, date-time, nullable, required
  - `economic_data_as_of` string, date-time, nullable: Solo si la definición usa un criterio económico (#57). Cambio de origen más reciente observado a la vez en el dim de compras y en el raw (source_timestamp máximo): un máximo observado, NO una marca de agua de completitud. No garantiza que estén reflejados todos los cambios anteriores: una imagen que llega tarde o que el almacén retiene puede faltar aunque el as-of sea posterior. Hoy no existe señal de completitud. Preséntese como «último cambio observado», nunca como «datos completos hasta». null si no se sabe. En un periodo sin cambios no avanza aunque el pipeline esté vivo.
  - `economic_data_built_at` string, date-time, nullable: Solo con criterio económico. Construcción de purchase_economic_facts. Es vitalidad del pipeline; no es marca de agua de origen ni dice nada de completitud.
  - `economic_data_status` 'CURRENT' | 'STALE' | 'UNKNOWN': Solo con criterio económico. Estado de VITALIDAD, no de completitud. CURRENT: facts se reconstruyó dentro de la cadencia (no certifica que estén todos los cambios de origen); STALE si se han perdido dos o más ciclos de 15 min (el resultado se calculó igual, sobre la última foto); UNKNOWN si la frescura no se pudo leer.

## Other responses

- `400`: Solicitud inválida
- `403`: Acceso denegado antes de ejecutar ninguna consulta. Dos casos: - **Rol no admitido** (`error.code: FORBIDDEN`): las rutas de datos solo admiten tokens con rol ADMIN, OWNER o SUPERADMIN (los `campaign_service_token` de nev-api llevan rol ADMIN). Un token de fan (USER), STAFF, EXTERNAL o sin rol recibe 403. - **Selección ajena** (`code: ACTIVE_TENANT_FORBIDDEN`): la organización seleccionada (`activeTenantId`) no pertenece a la jerarquía actual de `tenantId`.
- `500`: Error interno del servidor
- `503`: No se pudo leer la jerarquía de tenants (MongoDB no disponible). Reintentable; nunca se sustituye por un ámbito parcial o global.

---

[API](https://skmtc.dev/nevent/apis/nevent-data-api.md) · [All operations](https://skmtc.dev/nevent/apis/nevent-data-api/llms.txt) · [OpenAPI document](https://skmtc.dev/nevent/apis/nevent-data-api/revisions/81035d32c1c1?raw)
