VitrinaAPI

Ads

Spend, ROAS and attributed revenue by campaign, creative ranking and attribution health, plus the single tag: the snippet you paste (or its Google Tag Manager installer) and whether it is already sending visits. Reads from Atribu with the workspace's delegated key.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

MethodPathWhat it does
GET/ads/actionsThe suggested actions for one Ads screen
POST/ads/actions/{key}/dismissHide an action for a few days
POST/ads/actions/{key}/executeConfirm and run a previewed action
POST/ads/actions/{key}/previewPreview an action before confirming it
GET/ads/actions/executionsThe actions journal («Lo que hice»)
GET/ads/actions/executions/{id}The status of an action execution
POST/ads/actions/executions/{id}/rollbackUndo an executed action
GET/ads/ads/{id}The ad sheet: creative, funnel and the people it brought (masked)
GET/ads/ads/{id}/videoA playable source for the ad’s video
GET/ads/attributed-salesOne campaign’s attributed conversions (the drill-down)
DELETE/ads/audiences/customer-listTurn off the paying-customers audience
GET/ads/audiences/customer-listThe paying-customers audience: state and consent
POST/ads/audiences/customer-listTurn on the paying-customers audience
GET/ads/booking-rateThe AI booking rate by conversation, per campaign, ad and hour
GET/ads/briefingThe briefing: ≤3 sentences about a screen’s figures
GET/ads/campaignsCampaign performance for a window
GET/ads/cohortsWhat a new ad patient costs and pays, by month of arrival
PUT/ads/consignaciones/condicionesSet the Condiciones de captación
GET/ads/consignaciones/configConsignment capture setup
GET/ads/consignaciones/landingThe «Vende tu auto» landing
PUT/ads/consignaciones/landingSwitch the «Vende tu auto» landing
GET/ads/consignaciones/metricsConsignment metrics
PUT/ads/consignaciones/ofertaSet the Oferta de consignación
PUT/ads/consignaciones/responsablesSet the Responsables de consignaciones
PUT/ads/consignaciones/stock-targetSet the stock target
GET/ads/conversion-leadsConversion Leads feed
PUT/ads/conversion-leadsConfigure the Conversion Leads feed
GET/ads/conversion-sourceWhere bookings, sales and payments come from
GET/ads/conversion-sync/catalogConversion Sync catalog: definitions, connections, destinations, privacy settings
GET/ads/conversion-sync/connections/{id}/pixelsThe Meta pixels on a Meta Ads connection's ad account
GET/ads/conversion-sync/data-sourcesThe Meta datasets (WhatsApp, pixel, Instagram): state, prerequisites and a 30-day preview
POST/ads/conversion-sync/data-sources/instagram/ensureCreate the Instagram dataset in Meta (idempotent)
POST/ads/conversion-sync/data-sources/instagram/turn-offStop sending to the Instagram dataset
POST/ads/conversion-sync/data-sources/website/create-pixelCreate a pixel in the ad account and connect it as the website pixel («Crear píxel»)
POST/ads/conversion-sync/data-sources/website/ensureConnect the website pixel as the destination of click-less outcomes
POST/ads/conversion-sync/data-sources/whatsapp/ensureCreate the WhatsApp dataset in Meta (idempotent)
POST/ads/conversion-sync/data-sources/whatsapp/turn-offStop sending to the WhatsApp dataset
GET/ads/conversion-sync/destinationsThe configured export destinations (pre-fills a re-run)
POST/ads/conversion-sync/destinationsCreate or update the Meta CAPI destination
GET/ads/conversion-sync/messaging-event-namesEach stage’s Meta event name on the website and WhatsApp lanes
PATCH/ads/conversion-sync/messaging-event-namesChoose a stage’s Meta event name on the WhatsApp lane
GET/ads/conversion-sync/preview-eventA real event, field by field, as Meta receives it
POST/ads/conversion-sync/rules/syncCreate or update one export rule per outcome Vitrina emits
POST/ads/conversion-sync/setupOne action: privacy mode (clinics) → destination → rules
GET/ads/conversion-sync/statusWhat reaches Meta, stage by stage («Enviar a Meta»)
POST/ads/conversion-sync/testSend one Meta TEST event (Test Events only)
GET/ads/conversion-sync/wiringWhether the ad sets optimize for the events Vitrina sends
POST/ads/conversion-sync/wiring/recheckRe-verify wiring and self-heal (creates a missing Custom Conversion)
GET/ads/creative-assetsLanzamiento videos
DELETE/ads/creative-assets/{id}Remove a Lanzamiento video
POST/ads/creative-assets/{id}/completeFinish a dealer video upload
POST/ads/creative-assets/uploadsStart a dealer video upload
GET/ads/creativesTop-performing ads (cohort-normalized creative scoring)
GET/ads/experimentsThe optimisation experiment: offer and history
POST/ads/experimentsRecord an optimisation experiment (draft)
GET/ads/experiments/{id}One optimisation experiment
PATCH/ads/experiments/{id}Record an arm’s ad sets, start, end or read early
GET/ads/experiments/{id}/readoutThe experiment readout, per arm
GET/ads/experiments/currentThe experiment in flight, with its status
GET/ads/feedThe live feed of recorded outcomes
GET/ads/feed/{id}/journeyOne feed row’s journey
GET/ads/funnelThe period’s people funnel (a cohort, not event counts)
DELETE/ads/goalClear the monthly Ads goal
GET/ads/goalThe workspace’s monthly Ads goal
PUT/ads/goalSet the monthly Ads goal
GET/ads/healthAttribution trust, UTM quality and attribution coverage
GET/ads/lanzamiento-proposalsList fatigue proposals
GET/ads/lanzamiento-proposals/{id}Get a fatigue proposal
POST/ads/lanzamiento-proposals/{id}/acceptAccept a fatigue proposal
POST/ads/lanzamiento-proposals/{id}/dismissDismiss a fatigue proposal
GET/ads/lanzamientosLanzamientos
POST/ads/lanzamientosCreate a Lanzamiento
GET/ads/lanzamientos/{id}One Lanzamiento
POST/ads/lanzamientos/{id}/confirmConfirm a built Lanzamiento
GET/ads/lanzamientos/{id}/metricsConsignment metrics of one Lanzamiento
POST/ads/lanzamientos/{id}/rollbackRoll back a Lanzamiento
GET/ads/lanzamientos/car-creative-setsPer-car creative sets
POST/ads/lanzamientos/car-creative-setsRender the creatives of one stock car
GET/ads/lanzamientos/car-creative-sets/{set_id}One per-car creative set
GET/ads/lanzamientos/creative-setsLanzamiento creative sets
POST/ads/lanzamientos/creative-setsRender the Lanzamiento static creatives
GET/ads/lanzamientos/creative-sets/{set_id}One Lanzamiento creative set
GET/ads/lanzamientos/creative-sets/{set_id}/videosThe Lanzamiento consignment videos of a set
POST/ads/lanzamientos/creative-sets/{set_id}/videosRender the Lanzamiento consignment videos
POST/ads/lanzamientos/preflightCheck a Lanzamiento before creating it
GET/ads/leadgen-formsInstant Form mappings
DELETE/ads/leadgen-forms/{form_id}Remove an Instant Form mapping
PUT/ads/leadgen-forms/{form_id}Map an Instant Form
GET/ads/leadgen-submissionsLatest Instant Form submissions
GET/ads/lineasAds by Línea: spend, people and attributed cash per Línea
GET/ads/measurementMeasurement incidents: what stopped measuring, since when
GET/ads/meta-sendWhen the workspace started sending its outcomes to Meta
GET/ads/meta-send/comparisonThe funnel before vs after sending outcomes to Meta
GET/ads/monthly-returnWhat the ad patients paid, month by month, against ad spend
GET/ads/overviewAd spend, attributed revenue, ROAS and outcomes for a window
GET/ads/paid-funnelPaid funnel: spend, leads, qualified, booked, held and paid per arm, campaign, ad set and ad
GET/ads/peopleWho is behind an Ads number (name, RUT, phone)
GET/ads/proyectosAds by proyecto: cost per lead, visita and reserva
GET/ads/reportReporte: campaigns, ad sets or ads with their funnel, costs and cash return
GET/ads/report/exportExport the Reporte (aggregates only)
GET/ads/scorecardThe one per-ad scorecard (ranking, verdicts, the four returns)
GET/ads/share/resumenOne calendar month as the Resumen share card (data)
GET/ads/share/resumen.pngOne calendar month as the Resumen share card (PNG)
GET/ads/stagesWhere the funnel leaks: M1 ads · M2 agenda · M3 clinic sale
GET/ads/stateThe tenant's Vitrina Ads entitlement and delegated-key state
GET/ads/tracking/gtmGoogle Tag Manager install: connection, container, install state
POST/ads/tracking/gtm/connectStart the Google consent for Tag Manager
POST/ads/tracking/gtm/connect/completeFinish the Google consent for Tag Manager
DELETE/ads/tracking/gtm/connectionDisconnect Google Tag Manager
PUT/ads/tracking/gtm/containerChoose the Tag Manager container to install into
GET/ads/tracking/gtm/containersThe Tag Manager web containers the Google account can reach
POST/ads/tracking/gtm/installWrite the Vitrina Ads tag into the container (unpublished)
POST/ads/tracking/gtm/previewPreview the tag on the site (Tag Assistant)
POST/ads/tracking/gtm/publishPublish the tag (new container version)
POST/ads/tracking/gtm/uninstallRemove the tag (new container version)
GET/ads/tracking/installers/gtmThe tag as a ready-to-paste Google Tag Manager installer
GET/ads/tracking/keyThe workspace's tracking key and the tag to paste
GET/ads/tracking/statusIs the tag installed? Last event seen, events in 24 h, domain
GET/ads/venta/chileautos-costWhat Chileautos costs
PUT/ads/venta/chileautos-costRecord what Chileautos costs
DELETE/ads/venta/chileautos-cost/{id}Remove a Chileautos cost entry
GET/ads/venta/comparisonMeta car ads vs Chileautos
GET/ads/venta/lanzamientosList car ads
POST/ads/venta/lanzamientosAdvertise cars (one Lanzamiento per car)
POST/ads/venta/preflightCheck car ads before creating them
GET/ads/venta/suggestionsSuggest stock cars to advertise

GET /ads/actions

The suggested actions for one Ads screen

Up to 6 suggested actions for a screen, primary first, derived from the screen’s figures and the open recommendations of the measurement engine: pause an ad that «se desgasta» (the observed fatigue_state: degraded — on 3 days in a row its trailing 7-day CTR at or under 70 % of its own previous 28 days, or its trailing 7-day cost per result at or over 1/0.7× (+42.86 %); never a learned tier or a probability) AND no longer pays for itself (no attributed value, or a return under 1× — a worn ad still returning 1× or more gets «Prepara una variante» instead, never the pause), pause the delivering ads of a campaign that spent with no results in the period (one card per ad), raise a winning ad set’s budget, re-check the conversion wiring, re-scan the ad links, correct the links of the ads whose «Parámetros de URL» do not carry the ad id (url_tags_fix, Salud: one fix over at most 25 ads, busiest first, each ad listed in target.ads and its exact change in params.ads — the six standard utm_ parameters take the standard values, replacing the ad’s own; it replaces each ad’s creative, so Meta may restart the learning phase — consent required, undoable per ad) — or, when Meta is not connected, only a nav card to reconnect it. Nothing here runs by itself: preview → confirm → execute.

nav cards only open a page and never execute (dismissable like any card): Campañas — «Mira quién llegó por …», the lowest cost per result among campaigns with 3 or more results, into the attributed list filtered by that campaign; Creativos — «Prepara una variante de …» for an ad that se desgasta and «Usa … como base» for the best-scored ad that does not, both opening the ad in Meta Ads Manager; Salud — «Instalar el tag en …» (never seen) or «El tag dejó de reportar en …» (installed, silent) when the tag sent nothing in the last 24 hours while Meta is connected, into the set-up’s tag step; «Corregir los enlaces de tus anuncios en Meta» instead of url_tags_fix when the change cannot be made from Vitrina (a read-only Meta connection, or a Vitrina connection that needs renewing): it opens those ads in Meta Ads Manager; params carries the parameters to paste in place of the current ones.

Every figure a why prints names its window: an ad’s spend is the creatives read’s rolling window («en los últimos 28 días» — what the gallery shows), a campaign’s spend is the requested period.

window (Creativos only) derives the rail on the same rolling creatives window as GET /ads/creatives; pass the same window to preview.

A key is <kind>:<target external id>:<YYYY-MM-DD> and is stable for the day. Dismissed actions stay on the list with dismissed_until set, sorted last. include=adset_duplicate_swap also lists the duplicate-and-swap actions (hidden by default). include=campaign_pause (Campañas, Resumen) marks a client that renders the whole-campaign pause card (campaign_pause: one atomic change in Meta, undone with «Reanudar»). Not offered yet — its eligibility rule is pending — so today the list never contains it, with or without the include, and a campaign that spent with no results keeps the per-ad cards.

Card state machine (client): proposed → previewing → confirming → running → done | failed | refused → (done) reverted; dismissed for 7 days; simulated replaces done in sample mode.

ParameterInTypeRequiredConstraints
screenqueryresumen \campanas \atribuidos \
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
windowquery7d \14d \28d \
periodquery0 \1 \true \
includequeryadset_duplicate_swap \campaign_pauseno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/actions \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "key": "pause_ad:120212000000000006:2026-09-23",
      "kind": "pause_ad",
      "screen": [
        "creativos",
        "resumen",
        "campanas"
      ],
      "primary": true,
      "title": "Pausar «Video testimonio · 30 s»",
      "why": "Se desgasta: en sus últimos 7 días, frente a sus 28 días anteriores, el CTR cayó 30 % o más o el costo por resultado subió 43 % o más, durante 3 días seguidos. Lleva **$184.300** de gasto en los últimos 28 días y no le trae presupuestos aceptados.",
      "effect": "Deja de mostrarse de inmediato; puedes reactivarlo cuando quieras.",
      "target": {
        "level": "ad",
        "external_id": "120212000000000006",
        "name": "Video testimonio · 30 s",
        "campaign_external_id": null,
        "campaign": {
          "external_id": "120210000000000003",
          "name": "Blanqueamiento · Septiembre"
        }
      },
      "params": {},
      "reversible": true,
      "risk": "reversible",
      "engine_ref": {
        "op": "ads.pause"
      },
      "facts": [
        "critical_creative_name",
        "critical_creative_spend",
        "fatigued_creatives"
      ],
      "dismissed_until": null
    },
    {
      "key": "budget_change:120211000000000077:2026-09-23",
      "kind": "budget_change",
      "screen": [
        "campanas",
        "resumen"
      ],
      "primary": false,
      "title": "Subir 20 % el presupuesto de «Implantes · Conjunto principal»",
      "why": "Está trayendo tratamientos a buen costo y tiene espacio para crecer. Es parte de «Implantes · Septiembre», tu campaña con mejor retorno (**4,1×**).",
      "effect": "El presupuesto diario sube 20 %; puedes deshacerlo.",
      "target": {
        "level": "ad_set",
        "external_id": "120211000000000077",
        "name": "Implantes · Conjunto principal",
        "campaign_external_id": null,
        "campaign": {
          "external_id": "120210000000000001",
          "name": "Implantes · Septiembre"
        }
      },
      "params": {
        "budget_change_pct": 20
      },
      "reversible": true,
      "risk": "reversible",
      "engine_ref": {
        "op": "recommendations.apply",
        "recommendation_id": "a0a0a0a0-0000-4000-8000-000000000001"
      },
      "facts": [
        "best_campaign_name",
        "best_campaign_roas"
      ],
      "dismissed_until": null
    },
    {
      "key": "budget_change:120210000000000002:2026-09-23",
      "kind": "budget_change",
      "screen": [
        "campanas",
        "resumen"
      ],
      "primary": false,
      "title": "Bajar 20 % el presupuesto de la campaña «Blanqueamiento · Octubre»",
      "why": "Se lleva el **46 %** del gasto de tus campañas (**$412.000** en septiembre) y agenda el **2,1 %** (3 de 140); «Implantes · Septiembre» agenda el **9,8 %** (12 de 122). La campaña reparte su presupuesto sola, así que se baja ahí: de **$25.000** a **$20.000** al día; 20 % es el paso máximo, y el siguiente, en 3 días.",
      "effect": "El presupuesto diario de la campaña baja 20 %; puedes deshacerlo.",
      "target": {
        "level": "campaign",
        "external_id": "120210000000000002",
        "name": "Blanqueamiento · Octubre",
        "campaign_external_id": null,
        "campaign": {
          "external_id": "120210000000000002",
          "name": "Blanqueamiento · Octubre"
        }
      },
      "params": {
        "budget_change_pct": -20,
        "budget_level": "campaign",
        "budget_type": "daily",
        "daily_budget": 25000,
        "new_daily_budget": 20000,
        "step_pct": 20
      },
      "reversible": true,
      "risk": "reversible",
      "engine_ref": {
        "op": "campaigns.budget"
      },
      "facts": [],
      "dismissed_until": null
    },
    {
      "key": "nav:cheapest:120210000000000002:2026-09-23",
      "kind": "nav",
      "screen": [
        "campanas"
      ],
      "primary": false,
      "title": "Mira quién llegó por «Ortodoncia invisible · Sept»",
      "why": "Es la campaña con el costo más bajo por tratamiento del período: **$19.800** por tratamiento, con 9 tratamientos. Vale la pena ver en qué etapa están antes de moverle presupuesto.",
      "effect": null,
      "target": {
        "level": "campaign",
        "external_id": "120210000000000002",
        "name": "Ortodoncia invisible · Sept",
        "campaign": {
          "external_id": "120210000000000002",
          "name": "Ortodoncia invisible · Sept"
        }
      },
      "params": {},
      "reversible": false,
      "risk": null,
      "engine_ref": null,
      "href": "/anuncios/atribuidos?c=120210000000000002",
      "cta": "Ver tratamientos",
      "facts": [],
      "dismissed_until": null
    },
    {
      "key": "url_tags_fix:profile:2026-09-23",
      "kind": "url_tags_fix",
      "screen": [
        "salud"
      ],
      "primary": false,
      "title": "Corregir los enlaces de tus anuncios",
      "why": "Así sabrás qué anuncio trajo cada paciente: hoy los clics de **2** anuncios llegan sin decir de cuál vienen.",
      "effect": "Meta puede reiniciar el aprendizaje de los conjuntos afectados; los anuncios siguen activos. Puedes deshacerlo.",
      "target": {
        "level": "profile",
        "external_id": null,
        "name": null,
        "ads": [
          {
            "external_id": "120212000000000011",
            "name": "Carrusel · Antes y después",
            "ad_set_name": "Implantes · Las Condes",
            "campaign_name": "Implantes · Septiembre"
          },
          {
            "external_id": "120212000000000012",
            "name": "Historia · Evaluación gratis",
            "ad_set_name": "Implantes · Providencia",
            "campaign_name": "Implantes · Septiembre"
          }
        ]
      },
      "params": {
        "ads": [
          {
            "external_id": "120212000000000011",
            "expected_url_tags": "utm_source=facebook&utm_campaign={{campaign.name}}&utm_content={{ad.name}}",
            "url_tags": "utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.id}}&utm_term={{adset.id}}&utm_content={{ad.id}}&utm_id={{campaign.id}}"
          },
          {
            "external_id": "120212000000000012",
            "expected_url_tags": null,
            "url_tags": "utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.id}}&utm_term={{adset.id}}&utm_content={{ad.id}}&utm_id={{campaign.id}}"
          }
        ]
      },
      "reversible": true,
      "risk": "learning_reset",
      "engine_ref": {
        "op": "quality.url_tags_apply"
      },
      "facts": [],
      "dismissed_until": null
    }
  ]
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/actions/{key}/dismiss

Hide an action for a few days

Hides the action from the rail for days (default 7, max 30): it stays on GET /ads/actions with dismissed_until set and sorted last. Stored in the workspace’s Ads settings; not persisted in sample mode (persisted: false).

ParameterInTypeRequiredConstraints
keypathstringyesmín. 5, máx. 200, patrón ^[a-z_]+:.+:\d{4}-\d{2}-\d{2}$

Body

FieldTypeRequiredConstraints
daysinteger—≥ 1, ≤ 30, por defecto 7
sampleboolean—
curl -X POST https://api.vitrinadev.com/api/v1/ads/actions/<id>/dismiss \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "days": 7
  }'

Example response (200)

{
  "data": {
    "dismissed_until": "2026-09-30T15:00:00.000Z",
    "persisted": true
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/actions/{key}/execute

Confirm and run a previewed action

Spends the preview’s single-use approval and queues the engine write; poll GET /ads/actions/executions/\{id\}. Idempotency-Key is required (400 without): a replay with the same key answers the first execution and writes nothing.

409 ADS_ACTION_REPLAY_BLOCKED when the approval cannot be spent — details.reason is expired (preview older than 10 minutes), consumed (already confirmed, e.g. from another tab), not_found or binding_mismatch (another person’s preview, or another action — incl. a real preview sent with sample: true). In sample mode nothing reaches the engine: the execution is born succeeded with simulated: true, and only a sample preview can be spent.

campaign_pause, duplicate-and-swap and url_tags_fix need the owner’s acceptance of the preview’s consent_terms (the verbatim consent text): send consent: \{ version, text_hash, accepted_at? \}. Missing, different or older than 24 h → 409 ADS_ACTION_CONSENT_REQUIRED (details.reason missing|mismatch|stale, details.consent = the current version and hash) — nothing is spent and nothing reaches the engine.

If the job cannot be queued the answer is still 202, with status: failed: the execution exists and says why (error.code: queue_unavailable).

A url_tags_fix corrects each ad on its own: outcome.summary reads «{k} de {n} anuncios corregidos», outcome.partial is true when k < n, and outcome.ads[] says what happened to each ad. None corrected → failed/refused with the first ad’s reason, still with outcome.ads[].

A campaign_pause is ONE change in Meta. Each execution is its own request: a second confirm (another tab, an agent) finds the campaign already paused and answers outcome.engine_outcome: no_change — nothing written, no journal line, nothing to undo. Otherwise engine_outcome is applied, or replayed (a retry of this same execution); outcome.affected_ads_count is how many ads stopped.

ParameterInTypeRequiredConstraints
keypathstringyesmín. 5, máx. 200, patrón ^[a-z_]+:.+:\d{4}-\d{2}-\d{2}$

Body

FieldTypeRequiredConstraints
preview_iduuidyes
sampleboolean—
consentobject—
curl -X POST https://api.vitrinadev.com/api/v1/ads/actions/<id>/execute \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "preview_id": "b1b1b1b1-0000-4000-8000-000000000001"
  }'

Example response (202)

{
  "data": {
    "execution_id": "e5e5e5e5-0000-4000-8000-000000000001",
    "status": "queued"
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/actions/{key}/preview

Preview an action before confirming it

Reads the target’s live state and returns what will change (rows: before → after), the warnings, and — for a duplicate-and-swap or a URL-parameter fix — the consent lines the owner must accept. A url_tags_fix preview lists untagged_ads («{n} → 0») first, then one row per ad (ad:&lt;id&gt;): its current parameters → exactly what it gets; an ad changed in Meta after the preview is skipped at execute, never rewritten blind. A campaign_pause preview reads the campaign live ONCE and returns everything its sheet and card draw: summary («Pausar «{campaign}» · detiene {n} anuncios · ~$X al día»), campaign (status, the ads it stops, the mean daily spend and the budget as exact decimal strings with their currency, and where the budget lives) and the rows status, delivering_ads, avg_daily_spend, budget, plus its consent. It also records ONE single-use approval bound to the caller, the action and its exact parameters, valid for 10 minutes (expires_at): preview_id is what execute spends. No engine write happens here.

409 ADS_ACTION_NOT_AVAILABLE when the action is no longer on today’s list (or is a nav card, which has nothing to execute). A campaign_pause key answers it with details.reason: not_offered while that action is not offered; once it is, it also answers it when the live read says the pause cannot run now — details.reason: not_active (already paused), not_pausable (archived or deleted), campaign_not_found, meta_connection_missing, permission_gap, reconnect_required, not_available (any other refusal of the read), too_recent (less than 7 complete days of spend) — with details.explanation, Spanish copy for the owner; an already-paused, archived or missing campaign also hides the card for 7 days, and a too-recent one until the day it can be judged. 503 ADS_ACTION_READ_UNAVAILABLE (details.reason: meta_unavailable) when the campaign could not be read right now — retry in a moment. The same 503 with details.reason: proposal_timeout when ANY proposal takes more than 8 seconds to prepare: nothing was recorded, retry (the actions a list just returned are kept for 10 minutes, so a review opened from a listed card answers from them instead of re-deriving the list).

ParameterInTypeRequiredConstraints
keypathstringyesmín. 5, máx. 200, patrón ^[a-z_]+:.+:\d{4}-\d{2}-\d{2}$

Body

FieldTypeRequiredConstraints
fromstringyespatrón ^\d{4}-\d{2}-\d{2}$
tostringyespatrón ^\d{4}-\d{2}-\d{2}$
window7d \14d \28d \
sampleboolean—
curl -X POST https://api.vitrinadev.com/api/v1/ads/actions/<id>/preview \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "2026-08-24",
    "to": "2026-09-23"
  }'

Example response (200)

{
  "data": {
    "preview_id": "b1b1b1b1-0000-4000-8000-000000000001",
    "expires_at": "2026-09-23T15:10:00.000Z",
    "action_key": "budget_change:120211000000000077:2026-09-23",
    "rows": [
      {
        "key": "daily_budget",
        "label": "Presupuesto diario",
        "before": "$10.000",
        "after": "$12.000"
      }
    ],
    "warnings": [
      "Solo se permite cambiar el presupuesto hasta 50 % por vez."
    ],
    "consent": [],
    "consent_terms": null,
    "reversible": true,
    "engine_op": "recommendations.apply"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 503

GET /ads/actions/executions

The actions journal («Lo que hice»)

Every action execution of the workspace (execute and rollback rows), newest first, each with what it touched (target) and who confirmed it (created_by, a display name — only for a caller holding memberships:read). from/to bound created_at by America/Santiago day; page with before=&lt;meta.next_before&gt;. Real mode lists real executions only; with sample=1 it lists what was simulated plus two example rows of today (also readable on GET /ads/actions/executions/\{id\}; not undoable: rollback_available: false).

ParameterInTypeRequiredConstraints
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
beforequerystringnomáx. 120, patrón ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z(,[0-9a-f-]{36})?$
limitqueryintegerno≥ 1, ≤ 50
screenqueryenviono
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/actions/executions \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "e5e5e5e5-0000-4000-8000-000000000003",
      "action_key": "pause_ad:120212000000000006:2026-09-23",
      "kind": "pause_ad",
      "status": "succeeded",
      "outcome": {
        "summary": "Anuncio pausado en Meta.",
        "engine_action_id": "f7f7f7f7-0000-4000-8000-000000000002",
        "applied_at": "2026-09-23T16:20:04.000Z"
      },
      "error": null,
      "rollback_available": true,
      "rollback_of": null,
      "simulated": false,
      "created_at": "2026-09-23T16:20:01.000Z",
      "finished_at": "2026-09-23T16:20:04.000Z",
      "target": {
        "level": "ad",
        "external_id": "120212000000000006",
        "name": "Blanqueamiento · Sonrisa",
        "campaign_name": "Blanqueamiento · Septiembre"
      },
      "created_by": "Dueña Ejemplo"
    },
    {
      "id": "e5e5e5e5-0000-4000-8000-000000000004",
      "action_key": "url_tags_fix:profile:2026-09-23",
      "kind": "url_tags_fix",
      "status": "succeeded",
      "outcome": {
        "summary": "1 de 2 anuncios corregidos.",
        "engine_action_id": "f7f7f7f7-0000-4000-8000-000000000003",
        "applied_at": "2026-09-23T15:40:09.000Z",
        "partial": true,
        "corrected_count": 1,
        "requested_count": 2,
        "ads": [
          {
            "external_id": "120212000000000011",
            "name": "Carrusel · Antes y después",
            "result": "corrected",
            "code": null,
            "message": null,
            "url_tags_after": "utm_source={{site_source_name}}&utm_medium=paid_social&utm_campaign={{campaign.id}}&utm_term={{adset.id}}&utm_content={{ad.id}}&utm_id={{campaign.id}}",
            "engine_action_id": "f7f7f7f7-0000-4000-8000-000000000003",
            "undo_available": true
          },
          {
            "external_id": "120212000000000012",
            "name": "Historia · Evaluación gratis",
            "result": "skipped",
            "code": "url_tags_changed_since_preview",
            "message": "Los parámetros de URL del anuncio cambiaron en Meta después de la vista previa, así que no lo tocamos. Revisa la propuesta de nuevo.",
            "url_tags_after": null,
            "engine_action_id": null,
            "undo_available": false
          }
        ]
      },
      "error": null,
      "rollback_available": true,
      "rollback_of": null,
      "simulated": false,
      "created_at": "2026-09-23T15:40:01.000Z",
      "finished_at": "2026-09-23T15:40:09.000Z",
      "target": {
        "level": "profile",
        "external_id": null,
        "name": null,
        "campaign_name": null,
        "ads": [
          {
            "external_id": "120212000000000011",
            "name": "Carrusel · Antes y después"
          },
          {
            "external_id": "120212000000000012",
            "name": "Historia · Evaluación gratis"
          }
        ]
      },
      "created_by": "Dueña Ejemplo"
    },
    {
      "id": "e5e5e5e5-0000-4000-8000-000000000001",
      "action_key": "budget_change:120211000000000077:2026-09-23",
      "kind": "budget_change",
      "status": "succeeded",
      "outcome": {
        "summary": "Cambio aplicado en Meta.",
        "engine_action_id": "f7f7f7f7-0000-4000-8000-000000000001",
        "applied_at": "2026-09-23T15:01:12.000Z"
      },
      "error": null,
      "rollback_available": true,
      "rollback_of": null,
      "simulated": false,
      "created_at": "2026-09-23T15:01:10.000Z",
      "finished_at": "2026-09-23T15:01:12.000Z",
      "target": {
        "level": "ad_set",
        "external_id": "120211000000000077",
        "name": "Implantes · Las Condes",
        "campaign_name": "Implantes · Septiembre"
      },
      "created_by": null
    }
  ],
  "meta": {
    "next_before": "2026-09-23T15:01:10.000000Z,e5e5e5e5-0000-4000-8000-000000000001"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/actions/executions/{id}

The status of an action execution

queued → running → succeeded | failed | refused; a succeeded execution becomes rolled_back when its rollback succeeds. error.reason is one of promoted_object_frozen (Meta froze the ad set’s conversion — the duplicate-and-swap is the alternative), reconnect_required, permission_gap (Meta connected without permission to manage ads — reconnect keeping it), rate_limited, not_reversible, forbidden (no person could be put on the record for the change — not a Meta grant problem), consent_required (the duplicate-and-swap consent is missing or stale — preview again), engine_error (incl. code: budget_change_out_of_bounds — outside the ±50 % allowed per change — code: not_active and code: no_recommendation). Campaign pause codes: campaign_not_found / campaign_not_pausable (archived or deleted — never written) / meta_refused (engine_error), campaign_not_resumable (not_reversible), a dead Meta token → reconnect_required, meta_permission_denied (permission_gap — Meta refused for a missing permission), applied_by_not_allowed (forbidden), meta_rate_limited (rate_limited, retried first). error.message is Spanish copy ready for the card. target is what the action touched (with campaign_name, the campaign the card named, on an ad-set budget change).

ParameterInTypeRequiredConstraints
idpathuuidyes
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/actions/executions/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "e5e5e5e5-0000-4000-8000-000000000001",
    "action_key": "budget_change:120211000000000077:2026-09-23",
    "kind": "budget_change",
    "status": "succeeded",
    "target": {
      "level": "ad_set",
      "external_id": "120211000000000077",
      "name": "Implantes · Las Condes",
      "campaign_name": "Implantes · Septiembre"
    },
    "outcome": {
      "summary": "Cambio aplicado en Meta.",
      "engine_action_id": "f7f7f7f7-0000-4000-8000-000000000001",
      "applied_at": "2026-09-23T15:01:12.000Z"
    },
    "error": null,
    "rollback_available": true,
    "rollback_of": null,
    "simulated": false,
    "created_at": "2026-09-23T15:01:10.000Z",
    "finished_at": "2026-09-23T15:01:12.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/actions/executions/{id}/rollback

Undo an executed action

Queues the engine’s rollback of a succeeded execution with rollback_available: true and answers the NEW execution (rollback_of = the original). The engine restores the pre-change state only while the object still carries what the action wrote; otherwise the rollback is refused with reason: not_reversible (code: state_drifted). A url_tags_fix is undone ad by ad (each ad back on its previous parameters): an ad changed in Meta since is left alone (final), an ad whose undo got no answer stays pending — the original keeps rollback_available: true and the next rollback sends only those ads; the original reads rolled_back once nothing is pending. A campaign_pause is undone with «Reanudar»: one resume in Meta, with a fresh key per request; ads that were paused on their own stay paused. A campaign already active answers outcome.engine_outcome: no_change (the pause is then no longer undoable, never marked as undone by Vitrina); an archived or deleted one is refused not_reversible (code: campaign_not_resumable), and so is one paused again by someone else since (code: paused_since): «Reanudar» only ever undoes this pause. Idempotency-Key is required. 409 ADS_ACTION_NOT_REVERSIBLE when the execution has nothing to undo.

ParameterInTypeRequiredConstraints
idpathuuidyes
samplequery1 \trueno
curl -X POST https://api.vitrinadev.com/api/v1/ads/actions/executions/<id>/rollback \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (202)

{
  "data": {
    "execution_id": "e5e5e5e5-0000-4000-8000-000000000002",
    "status": "queued"
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/ads/{id}

The ad sheet: creative, funnel and the people it brought (masked)

One ad: the creative and delivery metrics, the SAME funnel/costs/cash as its Reporte row for the window, and the conversations it opened (a label, the furthest stage, its value). Never a phone or an e-mail; for healthcare the label is always initials. There is no export of people.

ParameterInTypeRequiredConstraints
idpathstringyespatrón ^[0-9]{1,32}$
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/ads/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "ad": {
      "external_id": "120212000000000009",
      "name": "Video testimonio 15s",
      "ad_set": {
        "external_id": "120213000000000004",
        "name": "INTERESES"
      },
      "campaign": {
        "external_id": "120211000000000001"
      },
      "thumbnail_url": null,
      "title": "Implantes en un día",
      "body": "Agenda tu evaluación.",
      "has_video": true,
      "video_external_id": "120214000000000009",
      "ads_manager_url": "https://www.facebook.com/adsmanager/manage/ads?act=<account-id>&selected_ad_ids=120212000000000009",
      "spend": 168000,
      "impressions": 24100,
      "clicks": 337,
      "ctr": 1.4,
      "cpm": 6971,
      "cpc": 498,
      "cash_count": 0,
      "cash_value": 0,
      "meta_conversations_started": 22
    },
    "window": {
      "from": "2026-08-30",
      "to": "2026-09-28"
    },
    "row": {
      "spend": 412300,
      "funnel": {
        "conversations": 120,
        "replied": 110,
        "appointments": 20,
        "attended": 12,
        "closes": 2
      },
      "cost_per": {
        "conversation": 3436,
        "appointment": 20615,
        "attended": 34358,
        "close": 206150
      },
      "cash": 0,
      "return": null,
      "meta": {
        "conversations_started": 131,
        "cost_per_conversation": 3147
      }
    },
    "cash_recorded": false,
    "people": [
      {
        "conversation_id": "00000000-0000-4000-8000-00000000c0a1",
        "contact_id": "00000000-0000-4000-8000-00000000c0a2",
        "label": "M. R.",
        "masked": true,
        "stage": "closed_won",
        "stage_at": "2026-09-25T15:00:00.000Z",
        "opened_at": "2026-09-20T13:10:00.000Z",
        "value": 1250000
      }
    ],
    "people_more": false,
    "people_hidden": false
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/ads/{id}/video

A playable source for the ad’s video

Resolves the ad’s video for playing in the product. type: source is a direct URL for a &lt;video&gt;; type: embed is the Facebook embed iframe plus the public watch link (common — not an error). Meta signs the URL and it expires within minutes: it is answered with Cache-Control: no-store, so ask when the person presses play, never ahead of time. 404 when the ad has no video (or is not yours).

ParameterInTypeRequiredConstraints
idpathstringyespatrón ^[0-9]{1,32}$
curl https://api.vitrinadev.com/api/v1/ads/ads/<id>/video \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "type": "source",
    "source": "https://<video-cdn-host>/<signed-path>",
    "embed_url": null,
    "watch_url": "https://www.facebook.com/watch/?v=120214000000000009"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/attributed-sales

One campaign’s attributed conversions (the drill-down)

The attribution engine’s conversions for one campaign: every conversion credited to campaign_id under the window/model, each carrying its credited share (credited_value/credited_weight), its position in the touch path, and whether the credit is is_inherited (bridged lead→cash, never counted in ROAS).

customer_name/customer_email are null unless the tenant’s delegated key also carries customers:read — every other field is visible under campaigns:read alone.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
campaign_idquerystringyesmín. 1
ad_idquerystringnopatrón ^[0-9]{1,32}$
curl https://api.vitrinadev.com/api/v1/ads/attributed-sales \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "conversion_id": "a8a8a8a8-0000-4000-8000-000000000001",
        "event_type": "closed_won",
        "event_time": "2026-09-12T16:40:00.000Z",
        "customer_profile_id": "a9a9a9a9-0000-4000-8000-000000000001",
        "customer_key": null,
        "customer_name": "María José",
        "customer_email": null,
        "value_amount": 1200000,
        "revenue_type": "accepted_value",
        "credited_value": 1200000,
        "credited_weight": 1,
        "touch_count": 3,
        "path_length": 3,
        "first_touch_time": "2026-09-06T21:14:00.000Z",
        "last_touch_time": "2026-09-11T13:02:00.000Z",
        "is_inherited": false
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

DELETE /ads/audiences/customer-list

Turn off the paying-customers audience

Stops the refresh and deletes the list and its lookalike from the Meta account. The owner’s decision is kept (who, when).

curl -X DELETE https://api.vitrinadev.com/api/v1/ads/audiences/customer-list \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "state": "live",
    "reason": null,
    "names": {
      "audience": "Vitrina · Clientes que pagaron",
      "lookalike": "Vitrina · Parecidos a clientes que pagaron (1 %)"
    },
    "size": {
      "lower": 1000,
      "upper": 1200
    },
    "last_refreshed_at": "2026-10-03T06:40:00.000Z",
    "lookalike": {
      "state": "ready",
      "ratio": 0.01
    },
    "optin": {
      "accepted_at": "2026-10-01T21:10:00.000Z",
      "consent_version": "2026-10-04.customer-list.v2",
      "current": true,
      "lookalike": true
    },
    "consent": {
      "version": "2026-10-04.customer-list.v2",
      "text": "Vitrina creará en tu cuenta de Meta una lista llamada «Vitrina · Clientes que pagaron»…",
      "text_hash": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/audiences/customer-list

The paying-customers audience: state and consent

A Meta customer list named «Vitrina · Clientes que pagaron»: the people who paid in the last year, as hashed e-mail, phone, first name, last name and country (never RUT, amounts, treatments or clinical data; a paying guardian is sent as the guardian), marketing opt-outs and minors with a known birth date always excluded, refreshed daily by the measurement engine; usable in Meta for a 1 % lookalike (optional), exclusion or retargeting. Google Customer Match is never used for clinics. The read is also the capability check: coming_soon until the engine can build it.

curl https://api.vitrinadev.com/api/v1/ads/audiences/customer-list \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "state": "live",
    "reason": null,
    "names": {
      "audience": "Vitrina · Clientes que pagaron",
      "lookalike": "Vitrina · Parecidos a clientes que pagaron (1 %)"
    },
    "size": {
      "lower": 1000,
      "upper": 1200
    },
    "last_refreshed_at": "2026-10-03T06:40:00.000Z",
    "lookalike": {
      "state": "ready",
      "ratio": 0.01
    },
    "optin": {
      "accepted_at": "2026-10-01T21:10:00.000Z",
      "consent_version": "2026-10-04.customer-list.v2",
      "current": true,
      "lookalike": true
    },
    "consent": {
      "version": "2026-10-04.customer-list.v2",
      "text": "Vitrina creará en tu cuenta de Meta una lista llamada «Vitrina · Clientes que pagaron»…",
      "text_hash": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/audiences/customer-list

Turn on the paying-customers audience

A Meta customer list named «Vitrina · Clientes que pagaron»: the people who paid in the last year, as hashed e-mail, phone, first name, last name and country (never RUT, amounts, treatments or clinical data; a paying guardian is sent as the guardian), marketing opt-outs and minors with a known birth date always excluded, refreshed daily by the measurement engine; usable in Meta for a 1 % lookalike (optional), exclusion or retargeting. Google Customer Match is never used for clinics. Needs the CURRENT consent (consent.version + consent.text_hash from the read); 409 otherwise, or while the engine cannot build it.

Body

FieldTypeRequiredConstraints
consent_versionstringyesmín. 1, máx. 80
consent_text_hashstringyespatrón ^[0-9a-f]{64}$
lookalikeboolean—por defecto true
curl -X POST https://api.vitrinadev.com/api/v1/ads/audiences/customer-list \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "consent_version": "2026-10-04.customer-list.v2",
    "consent_text_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "lookalike": true
  }'

Example response (200)

{
  "data": {
    "state": "live",
    "reason": null,
    "names": {
      "audience": "Vitrina · Clientes que pagaron",
      "lookalike": "Vitrina · Parecidos a clientes que pagaron (1 %)"
    },
    "size": {
      "lower": 1000,
      "upper": 1200
    },
    "last_refreshed_at": "2026-10-03T06:40:00.000Z",
    "lookalike": {
      "state": "ready",
      "ratio": 0.01
    },
    "optin": {
      "accepted_at": "2026-10-01T21:10:00.000Z",
      "consent_version": "2026-10-04.customer-list.v2",
      "current": true,
      "lookalike": true
    },
    "consent": {
      "version": "2026-10-04.customer-list.v2",
      "text": "Vitrina creará en tu cuenta de Meta una lista llamada «Vitrina · Clientes que pagaron»…",
      "text_hash": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/booking-rate

The AI booking rate by conversation, per campaign, ad and hour

Of the conversations Vitrina answered ALONE in the window (the person wrote, the AI agent replied, no team member wrote before the booking), the share where the agent booked a cita — overall, per campaign and ad (the Meta ad that opened the conversation; campaign through the engine’s ad-level map) and per hour of the person’s first message (America/Santiago). The window starts no earlier than the day Vitrina first booked. Rows under min_conversations are flagged low_volume and ranked after the others. overall equals conversation_rate on GET /clinic/insights/resultados for the same days. Counts only — no person is identified.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/booking-rate \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "state": "measured",
    "requested": {
      "from": "2026-09-01",
      "to": "2026-10-06"
    },
    "window": {
      "from": "2026-09-21",
      "to": "2026-10-06"
    },
    "starts_at_first_booking": true,
    "first_ai_booked_on": "2026-09-21",
    "timezone": "America/Santiago",
    "min_conversations": 20,
    "overall": {
      "conversations": 588,
      "booked_conversations": 45,
      "ai_bookings": 54,
      "rate_pct": 7.7,
      "team_helped": 60,
      "existing": {
        "conversations": 214,
        "booked_conversations": 38,
        "ai_bookings": 41
      },
      "cancelled": 6
    },
    "from_ads": {
      "conversations": 461,
      "booked_conversations": 33,
      "ai_bookings": 40,
      "rate_pct": 7.2
    },
    "without_ad": {
      "conversations": 127,
      "booked_conversations": 12,
      "ai_bookings": 14,
      "rate_pct": 9.4
    },
    "campaigns_state": "ok",
    "by_campaign": [
      {
        "campaign_external_id": "120211000000000001",
        "campaign_name": "Implantes · Septiembre",
        "conversations": 212,
        "booked_conversations": 19,
        "ai_bookings": 23,
        "rate_pct": 9,
        "low_volume": false
      },
      {
        "campaign_external_id": "120211000000000002",
        "campaign_name": "Endodoncia",
        "conversations": 12,
        "booked_conversations": 2,
        "ai_bookings": 2,
        "rate_pct": 16.7,
        "low_volume": true
      }
    ],
    "unmapped_ad_conversations": 3,
    "by_ad": [
      {
        "ad_external_id": "120212000000000009",
        "campaign_external_id": "120211000000000001",
        "conversations": 140,
        "booked_conversations": 14,
        "ai_bookings": 17,
        "rate_pct": 10,
        "low_volume": false
      }
    ],
    "by_hour": [
      {
        "hour": 0,
        "conversations": 9,
        "booked_conversations": 1,
        "ai_bookings": 1,
        "rate_pct": 11.1
      }
    ],
    "truncated": false,
    "rule": "Conversaciones que el asistente de Vitrina respondió solo y en las que agendó al menos una cita ÷ conversaciones que respondió solo."
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/briefing

The briefing: ≤3 sentences about a screen’s figures

A short Spanish briefing for one Ads screen, written from PINNED aggregate facts: every figure in lead/lines is quoted verbatim from facts[].display, and facts is returned alongside so a client can show «¿Por qué?». Facts are aggregates only — campaign and ad names, never a person.

source says who wrote the sentences: model (a narrator bound by a guard that rejects any number, name or sentence outside the rules), template (built from the facts where the model is never used — first week, Meta disconnected, no spend reading, nothing to report, sample mode — or the model was rejected) or facts (built from the facts because no model briefing was cached yet). All read identically.

The read never waits on the model: a model briefing is cached per facts hash for 24 h (two tabs and a reload read the same text) and precomputed daily for the default windows; on a miss the answer is source: facts at once and the model briefing is written in the background, so a later read returns it. wait=1 writes it live instead (up to ~8 s). Sample mode never calls the model.

lead.figure names the number the lead is built around (display is its exact substring in lead.text). lines[].ask_key is the follow-up question a line invites, by recipe key (best_campaign, goal_pace, worst_spend, missing_signals, …) — derived from the facts the line quotes, each at most once — and lines[].ask its label. A fact that rests on a rule carries it as threshold. from/to are required except for creativos, which takes the rolling window.

ParameterInTypeRequiredConstraints
screenqueryresumen \campanas \atribuidos \
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
windowquery7d \14d \28d \
periodquery0 \1 \true \
modelquerystringno
waitquery0 \1 \true \
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/briefing \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "screen": "resumen",
    "window": {
      "from": "2026-08-24",
      "to": "2026-09-22"
    },
    "lead": {
      "text": "Tus anuncios devolvieron **3,7×** lo invertido: $845.200 se convirtieron en $3.120.000 en presupuestos aceptados.",
      "fact_keys": [
        "roas",
        "spend",
        "revenue"
      ],
      "figure": {
        "fact_key": "roas",
        "value": 3.69,
        "format": "times",
        "display": "3,7×",
        "offset": 27
      }
    },
    "lines": [
      {
        "text": "La mejor campaña fue «Evaluación sin costo · Meta», con 5,4× de retorno, y el 36,4% de tus tratamientos llegó desde anuncios.",
        "fact_keys": [
          "best_campaign_name",
          "best_campaign_roas",
          "ads_share"
        ],
        "ask_key": "best_campaign",
        "ask": "¿Qué campaña me conviene subir?"
      },
      {
        "text": "Revisa «Promoción de temporada · Stories»: gastó $118.400 sin resultados.",
        "fact_keys": [
          "worst_spend_campaign_name",
          "worst_spend_campaign_spend"
        ],
        "ask_key": "worst_spend",
        "ask": "¿Cuánto gasta sin resultados?"
      }
    ],
    "facts": [
      {
        "key": "roas",
        "label": "Retorno",
        "value": 3.69,
        "display": "3,7×",
        "format": "times",
        "source": "overview.current.roas",
        "privacy": "aggregate"
      },
      {
        "key": "spend",
        "label": "Inversión en anuncios",
        "value": 845200,
        "display": "$845.200",
        "format": "clp",
        "source": "overview.current.spend",
        "privacy": "aggregate"
      }
    ],
    "actions": [],
    "source": "model",
    "generated_at": "2026-09-22T16:40:12.000Z",
    "vocabulary": "healthcare",
    "sample": false
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/campaigns

Campaign performance for a window

The attribution engine’s campaign ranking (the ranked top-N shape — always the campaign rollup, never the per-ad-set/per-ad ranking). One row per campaign: spend, attributed outcome count/value and ROAS, plus the direct/inherited cash split when the engine can compute it.

campaign_external_id on each row is the PLATFORM id — pass it as campaign_id to /ads/attributed-sales for the drill-down.

appointments_booked / cost_per_appointment (spend ÷ citas, over metric_windows.appointments) and paying_patients / cost_per_paying_patient (spend ÷ distinct ad patients who paid, over metric_windows.paying_patients) are per row, null (never 0) with no spend or no cita / payment; totals is the footer (its paying_patients is the account-level figure of /ads/overview).

best_campaign is THE «best campaign» for every screen: the highest total return of its ad patients (first payment + cuotas ÷ spend) among campaigns with spend and at least 3 payments in the covered window, ties to the one with more paying patients. rule states the metric, threshold and window; reason says why none is named. Display it as is; never rank the rows again.

closed_outcome_count is the period’s closes credited to the campaign (recorded at the closed stage, matched to an ad) — a period-true count that does not depend on how much of /ads/feed was loaded; null while that credit is unknown.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
limitqueryintegerno> 0, ≤ 500
seriesquery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/campaigns \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "campaign_id": "a7a7a7a7-0000-4000-8000-000000000001",
        "campaign_name": "Leads · Septiembre",
        "campaign_external_id": "120211000000000001",
        "spend": 845200,
        "outcome_count": 31,
        "outcome_value": 3120000,
        "roas": 3.69,
        "direct_cash_value": 2100000,
        "inherited_cash_value": 600000,
        "closed_outcome_count": 9,
        "top_ad": {
          "external_id": "120212000000000009",
          "name": "Video testimonio 15s",
          "creative_thumbnail_url": "https://cdn.vitrinadev.com/examples/ads/video-testimonio-15s.jpg",
          "video_external_id": "120212000000000019",
          "ads_manager_url": "https://business.facebook.com/adsmanager/manage/ads?act=1234567890&selected_ad_ids=120212000000000009"
        },
        "series": [
          {
            "date": "2026-08-24",
            "spend": 31800,
            "outcome_count": 1
          },
          {
            "date": "2026-08-25",
            "spend": 35700,
            "outcome_count": 2
          },
          {
            "date": "2026-08-26",
            "spend": 28700,
            "outcome_count": 1
          },
          {
            "date": "2026-08-27",
            "spend": 33400,
            "outcome_count": 1
          },
          {
            "date": "2026-08-28",
            "spend": 30300,
            "outcome_count": 1
          },
          {
            "date": "2026-08-29",
            "spend": 19700,
            "outcome_count": 1
          },
          {
            "date": "2026-08-30",
            "spend": 22100,
            "outcome_count": 1
          },
          {
            "date": "2026-08-31",
            "spend": 28700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-01",
            "spend": 33400,
            "outcome_count": 1
          },
          {
            "date": "2026-09-02",
            "spend": 30300,
            "outcome_count": 1
          },
          {
            "date": "2026-09-03",
            "spend": 31800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-04",
            "spend": 35700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-05",
            "spend": 17800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-06",
            "spend": 20700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-07",
            "spend": 30300,
            "outcome_count": 1
          },
          {
            "date": "2026-09-08",
            "spend": 31800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-09",
            "spend": 35700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-10",
            "spend": 28700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-11",
            "spend": 33400,
            "outcome_count": 1
          },
          {
            "date": "2026-09-12",
            "spend": 18800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-13",
            "spend": 19700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-14",
            "spend": 35700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-15",
            "spend": 28700,
            "outcome_count": 1
          },
          {
            "date": "2026-09-16",
            "spend": 33400,
            "outcome_count": 1
          },
          {
            "date": "2026-09-17",
            "spend": 30200,
            "outcome_count": 1
          },
          {
            "date": "2026-09-18",
            "spend": 31800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-19",
            "spend": 22100,
            "outcome_count": 1
          },
          {
            "date": "2026-09-20",
            "spend": 17800,
            "outcome_count": 1
          },
          {
            "date": "2026-09-21",
            "spend": 33400,
            "outcome_count": 1
          },
          {
            "date": "2026-09-22",
            "spend": 13600,
            "outcome_count": 1
          }
        ]
      }
    ],
    "gaps": [
      {
        "from": "2026-09-12",
        "to": "2026-09-15",
        "cause": "meta_disconnected",
        "metrics": [
          "spend"
        ],
        "incident_id": "b1b1b1b1-0000-4000-8000-000000000002"
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/cohorts

What a new ad patient costs and pays, by month of arrival

The Informe’s cohort tables. A cohort is the ad patients whose earliest first payment credited to an ad fell in one America/Santiago month of the window. Per cohort: that month’s ad spend, the new patients, the cost per new patient, and the average paid per patient in the arrival month (m0), cumulatively by month +1/+2/+3/+5 and to date. A cell for a month that has not come yet is null — render «—», never the to-date value. Months before measurement started (coverage.measured_from) are null throughout.

The same computation as /ads/overview’s current.patient_return and /ads/monthly-return over the same covered window. comparison sets the ad patients who paid in the window beside every other patient; days_to_first_payment is the click → first payment median and quartiles. At most 366 days. Aggregates only — no patient is identified.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/cohorts \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-07-01",
      "to": "2026-09-30"
    },
    "cohorts": [
      {
        "month": "2026-07",
        "measured": true,
        "spend": 2745532,
        "patients": 17,
        "cost_per_new_patient": 161502,
        "m0": 171988,
        "m1": 288422,
        "m2": 292018,
        "m3": null,
        "m5": null,
        "to_date": 292018
      },
      {
        "month": "2026-08",
        "measured": true,
        "spend": 2710434,
        "patients": 25,
        "cost_per_new_patient": 108417,
        "m0": 134571,
        "m1": 180276,
        "m2": null,
        "m3": null,
        "m5": null,
        "to_date": 180276
      },
      {
        "month": "2026-09",
        "measured": true,
        "spend": 1752236,
        "patients": 11,
        "cost_per_new_patient": 159294,
        "m0": 137657,
        "m1": null,
        "m2": null,
        "m3": null,
        "m5": null,
        "to_date": 137657
      }
    ],
    "comparison": {
      "ads": {
        "patients": 64,
        "avg_paid": 222124,
        "avg_payments": 1.9
      },
      "other": {
        "patients": 171,
        "avg_paid": 341560,
        "avg_payments": 2.6
      }
    },
    "days_to_first_payment": {
      "median": 5,
      "p25": 2,
      "p75": 11,
      "patients": 53
    },
    "pipeline_uncollected": {
      "count": 6,
      "value": 3512400
    },
    "joins_state": "ready",
    "coverage": {
      "measured_from": "2026-02-19",
      "touches_from": "2026-02-03",
      "from": "2026-07-01",
      "clipped": false,
      "previous_measured": true
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/consignaciones/condiciones

Set the Condiciones de captación

Replaces the Condiciones. A lead outside them is marked «fuera de criterio» and never counts as qualified for Meta.

Body

FieldTypeRequiredConstraints
min_yearinteger | nullyes≥ 1950, ≤ 2100
max_kminteger | nullyes≥ 1, ≤ 2000000
zone_region_codesstring[]—por defecto []
excluded_makesstring[]—por defecto []
curl -X PUT https://api.vitrinadev.com/api/v1/ads/consignaciones/condiciones \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "min_year": 2015,
    "max_km": 150000,
    "zone_region_codes": [
      "13"
    ],
    "excluded_makes": [
      "Chery"
    ]
  }'

Example response (200)

{
  "data": {
    "oferta": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "claims": [
      {
        "key": "commission",
        "text": "5% de comisión"
      },
      {
        "key": "solo_si_vende",
        "text": "Solo pagas si se vende"
      },
      {
        "key": "precio_minimo",
        "text": "Tú pones el precio mínimo"
      },
      {
        "key": "compra_inmediata",
        "text": "Si prefieres, te lo compramos de inmediato"
      },
      {
        "key": "test_drives_at_lot",
        "text": "Coordinamos los test drive en nuestro local"
      },
      {
        "key": "transfer_help",
        "text": "Te ayudamos con la transferencia"
      },
      {
        "key": "photography",
        "text": "Fotos profesionales de tu auto"
      },
      {
        "key": "contract_length",
        "text": "Contrato por 60 días"
      }
    ],
    "oferta_problems": [],
    "condiciones": {
      "min_year": 2015,
      "max_km": 150000,
      "zone_region_codes": [
        "13"
      ],
      "excluded_makes": [
        "Chery"
      ]
    },
    "responsables": {
      "user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "effective_user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "source": "configured"
    },
    "stock_target": 35,
    "updated_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/consignaciones/config

Consignment capture setup

The dealer setup Lanzamientos are built from: the Oferta de consignación (the claims a creative may say), the Condiciones de captación (the cars the dealer accepts), the Responsables de consignaciones (who receives qualified sellers) and the stock target. Empty until configured.

curl https://api.vitrinadev.com/api/v1/ads/consignaciones/config \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "oferta": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "claims": [
      {
        "key": "commission",
        "text": "5% de comisión"
      },
      {
        "key": "solo_si_vende",
        "text": "Solo pagas si se vende"
      },
      {
        "key": "precio_minimo",
        "text": "Tú pones el precio mínimo"
      },
      {
        "key": "compra_inmediata",
        "text": "Si prefieres, te lo compramos de inmediato"
      },
      {
        "key": "test_drives_at_lot",
        "text": "Coordinamos los test drive en nuestro local"
      },
      {
        "key": "transfer_help",
        "text": "Te ayudamos con la transferencia"
      },
      {
        "key": "photography",
        "text": "Fotos profesionales de tu auto"
      },
      {
        "key": "contract_length",
        "text": "Contrato por 60 días"
      }
    ],
    "oferta_problems": [],
    "condiciones": {
      "min_year": 2015,
      "max_km": 150000,
      "zone_region_codes": [
        "13"
      ],
      "excluded_makes": [
        "Chery"
      ]
    },
    "responsables": {
      "user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "effective_user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "source": "configured"
    },
    "stock_target": 35,
    "updated_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/consignaciones/landing

The «Vende tu auto» landing

Whether the consignment landing is on and where the landing lane points: the Sitio page (https://<host>/vende-tu-auto) for a dealer with an active Sitio, else the dealer's own page carrying the embed snippet. Its copy comes only from the Oferta de consignación.

curl https://api.vitrinadev.com/api/v1/ads/consignaciones/landing \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "enabled": true,
    "enabled_at": "2026-10-26T15:00:00.000Z",
    "enabled_source": "lanzamiento",
    "url": "https://upautos.cl/vende-tu-auto",
    "kind": "sitio",
    "external_url": null,
    "embed_snippet": "<div id=\"vitrina-vende-tu-auto\"></div>\n<script src=\"https://api.vitrinadev.com/vende-tu-auto.js\"\n        data-key=\"pk_live_…\"\n        data-container=\"#vitrina-vende-tu-auto\" async></script>"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/consignaciones/landing

Switch the «Vende tu auto» landing

Turns the consignment landing on or off and sets the dealer’s own page for the embed. A Lanzamiento with a landing lane turns it on by itself.

Body

FieldTypeRequiredConstraints
enabledbooleanyes
external_urlstring | null—uri, máx. 2048
curl -X PUT https://api.vitrinadev.com/api/v1/ads/consignaciones/landing \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true
  }'

Example response (200)

{
  "data": {
    "enabled": true,
    "enabled_at": "2026-10-26T15:00:00.000Z",
    "enabled_source": "lanzamiento",
    "url": "https://upautos.cl/vende-tu-auto",
    "kind": "sitio",
    "external_url": null,
    "embed_snippet": "<div id=\"vitrina-vende-tu-auto\"></div>\n<script src=\"https://api.vitrinadev.com/vende-tu-auto.js\"\n        data-key=\"pk_live_…\"\n        data-container=\"#vitrina-vende-tu-auto\" async></script>"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/consignaciones/metrics

Consignment metrics

The consignment funnel per Lanzamiento, per lane and per ad: spend → contacts (landing form + click-to-WhatsApp chat) → qualified sellers (the SellerQualified / AddToCart fact; a lead outside the Condiciones never counts) → inspection booked (SellerVisitScheduled) → Consignación captada (consignment or compra) → sold (the captured car sold from stock). Each step counts what happened in the window. Headline: cost_per_captada, with cost_per_qualified_seller beside it. Spend comes from the attribution engine; unknown spend is null (never 0) and so is every cost built on it, as is a cost over zero. Plus stock target progress: nothing pauses, Vitrina only suggests lowering the budget once reached.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
lanzamiento_idqueryuuidno
curl https://api.vitrinadev.com/api/v1/ads/consignaciones/metrics \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "period": {
      "from": "2026-10-01",
      "to": "2026-10-26",
      "timezone": "America/Santiago"
    },
    "spend_status": "ok",
    "totals": {
      "spend": 412000,
      "contacts": 58,
      "contacts_form": 21,
      "contacts_chat": 37,
      "qualified_sellers": 19,
      "outside_criteria": 9,
      "inspections_booked": 11,
      "captadas": 4,
      "captadas_consignacion": 3,
      "captadas_compra": 1,
      "sold": 1,
      "cost_per_captada": 103000,
      "cost_per_qualified_seller": 21684,
      "cost_per_contact": 7103
    },
    "lanzamientos": [
      {
        "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
        "plantilla": "captacion_consignaciones",
        "status": "live",
        "launched_at": "2026-10-02T13:00:00.000Z",
        "spend_status": "ok",
        "funnel": {
          "spend": 412000,
          "contacts": 58,
          "contacts_form": 21,
          "contacts_chat": 37,
          "qualified_sellers": 19,
          "outside_criteria": 9,
          "inspections_booked": 11,
          "captadas": 4,
          "captadas_consignacion": 3,
          "captadas_compra": 1,
          "sold": 1,
          "cost_per_captada": 103000,
          "cost_per_qualified_seller": 21684,
          "cost_per_contact": 7103
        },
        "lanes": [
          {
            "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
            "lane": "landing",
            "funnel": {
              "spend": 206000,
              "contacts": 21,
              "contacts_form": 21,
              "contacts_chat": 0,
              "qualified_sellers": 8,
              "outside_criteria": 3,
              "inspections_booked": 5,
              "captadas": 2,
              "captadas_consignacion": 2,
              "captadas_compra": 0,
              "sold": 0,
              "cost_per_captada": 103000,
              "cost_per_qualified_seller": 25750,
              "cost_per_contact": 9810
            }
          }
        ],
        "ads": [
          {
            "ad_object_id": "9d0e1f2a-3b4c-4d5e-8f60-718293a4b5c6",
            "meta_ad_id": "120211234567890123",
            "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
            "lane": "landing",
            "angle": "comision",
            "format": "4:5",
            "headline_mode": "commission",
            "media_kind": "static",
            "outside_vitrina": false,
            "funnel": {
              "spend": 0,
              "contacts": 0,
              "contacts_form": 0,
              "contacts_chat": 0,
              "qualified_sellers": 0,
              "outside_criteria": 0,
              "inspections_booked": 0,
              "captadas": 0,
              "captadas_consignacion": 0,
              "captadas_compra": 0,
              "sold": 0,
              "cost_per_captada": null,
              "cost_per_qualified_seller": null,
              "cost_per_contact": null
            }
          }
        ]
      }
    ],
    "stock_target": {
      "in_stock": 31,
      "captured_this_month": 4,
      "target": 35,
      "reached": false,
      "progress": 0.886,
      "suggest_lower_budget": false
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/consignaciones/oferta

Set the Oferta de consignación

Replaces the Oferta. A headline can only name a claim that is switched on (400 otherwise); creatives, the landing and the video say only what the Oferta switches on.

Body

FieldTypeRequiredConstraints
commission_pctnumber | nullyes> 0, ≤ 30
commission_plus_ivaboolean—por defecto false
headlinesarray | null—
headline_modecommission \solo_si_vende \both
solo_pagas_si_se_vendeboolean—por defecto false
precio_minimoboolean—
compra_inmediataboolean—por defecto false
test_drives_at_lotboolean—por defecto false
transfer_helpboolean—por defecto false
photographyboolean—por defecto false
contract_length_daysinteger | null—≥ 1, ≤ 730, por defecto null
cars_sold_per_monthinteger | null—≥ 1, ≤ 10000, por defecto null
curl -X PUT https://api.vitrinadev.com/api/v1/ads/consignaciones/oferta \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "commission_pct": 5,
    "commission_plus_iva": false,
    "headlines": [
      "commission",
      "precio_minimo"
    ],
    "headline_mode": "commission",
    "solo_pagas_si_se_vende": true,
    "precio_minimo": true,
    "compra_inmediata": true,
    "test_drives_at_lot": true,
    "transfer_help": true,
    "photography": true,
    "contract_length_days": 60,
    "cars_sold_per_month": null
  }'

Example response (200)

{
  "data": {
    "oferta": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "claims": [
      {
        "key": "commission",
        "text": "5% de comisión"
      },
      {
        "key": "solo_si_vende",
        "text": "Solo pagas si se vende"
      },
      {
        "key": "precio_minimo",
        "text": "Tú pones el precio mínimo"
      },
      {
        "key": "compra_inmediata",
        "text": "Si prefieres, te lo compramos de inmediato"
      },
      {
        "key": "test_drives_at_lot",
        "text": "Coordinamos los test drive en nuestro local"
      },
      {
        "key": "transfer_help",
        "text": "Te ayudamos con la transferencia"
      },
      {
        "key": "photography",
        "text": "Fotos profesionales de tu auto"
      },
      {
        "key": "contract_length",
        "text": "Contrato por 60 días"
      }
    ],
    "oferta_problems": [],
    "condiciones": {
      "min_year": 2015,
      "max_km": 150000,
      "zone_region_codes": [
        "13"
      ],
      "excluded_makes": [
        "Chery"
      ]
    },
    "responsables": {
      "user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "effective_user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "source": "configured"
    },
    "stock_target": 35,
    "updated_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/consignaciones/responsables

Set the Responsables de consignaciones

Replaces the members who receive qualified sellers. Only active members (400 otherwise). Empty = the designated sellers receive them.

Body

FieldTypeRequiredConstraints
user_idsuuid[]yes
curl -X PUT https://api.vitrinadev.com/api/v1/ads/consignaciones/responsables \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_ids": [
      "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
    ]
  }'

Example response (200)

{
  "data": {
    "oferta": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "claims": [
      {
        "key": "commission",
        "text": "5% de comisión"
      },
      {
        "key": "solo_si_vende",
        "text": "Solo pagas si se vende"
      },
      {
        "key": "precio_minimo",
        "text": "Tú pones el precio mínimo"
      },
      {
        "key": "compra_inmediata",
        "text": "Si prefieres, te lo compramos de inmediato"
      },
      {
        "key": "test_drives_at_lot",
        "text": "Coordinamos los test drive en nuestro local"
      },
      {
        "key": "transfer_help",
        "text": "Te ayudamos con la transferencia"
      },
      {
        "key": "photography",
        "text": "Fotos profesionales de tu auto"
      },
      {
        "key": "contract_length",
        "text": "Contrato por 60 días"
      }
    ],
    "oferta_problems": [],
    "condiciones": {
      "min_year": 2015,
      "max_km": 150000,
      "zone_region_codes": [
        "13"
      ],
      "excluded_makes": [
        "Chery"
      ]
    },
    "responsables": {
      "user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "effective_user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "source": "configured"
    },
    "stock_target": 35,
    "updated_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/consignaciones/stock-target

Set the stock target

The number of cars in stock the dealer aims for. Shown with progress; Vitrina suggests lowering the budget once reached and never pauses anything by itself. null clears it.

Body

FieldTypeRequiredConstraints
stock_targetinteger | nullyes≥ 1, ≤ 10000
curl -X PUT https://api.vitrinadev.com/api/v1/ads/consignaciones/stock-target \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stock_target": 35
  }'

Example response (200)

{
  "data": {
    "oferta": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "claims": [
      {
        "key": "commission",
        "text": "5% de comisión"
      },
      {
        "key": "solo_si_vende",
        "text": "Solo pagas si se vende"
      },
      {
        "key": "precio_minimo",
        "text": "Tú pones el precio mínimo"
      },
      {
        "key": "compra_inmediata",
        "text": "Si prefieres, te lo compramos de inmediato"
      },
      {
        "key": "test_drives_at_lot",
        "text": "Coordinamos los test drive en nuestro local"
      },
      {
        "key": "transfer_help",
        "text": "Te ayudamos con la transferencia"
      },
      {
        "key": "photography",
        "text": "Fotos profesionales de tu auto"
      },
      {
        "key": "contract_length",
        "text": "Contrato por 60 días"
      }
    ],
    "oferta_problems": [],
    "condiciones": {
      "min_year": 2015,
      "max_km": 150000,
      "zone_region_codes": [
        "13"
      ],
      "excluded_makes": [
        "Chery"
      ]
    },
    "responsables": {
      "user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "effective_user_ids": [
        "8f7c1c1e-2a5b-4c1e-9d10-0b2f3a4c5d6e"
      ],
      "source": "configured"
    },
    "stock_target": 35,
    "updated_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-leads

Conversion Leads feed

Whether the stages each Instant Form lead reaches in the CRM are sent back to Meta, and the Meta stage name of each. Meta uses them to optimise the form on lead quality instead of form fills. Off until configured; nothing is sent until the workspace has a Conversion Leads dataset in Meta.

curl https://api.vitrinadev.com/api/v1/ads/conversion-leads \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "dataset": {
      "state": "missing",
      "failed_7d": null,
      "sent_7d": null
    },
    "enabled": true,
    "lead_event_source": "Vitrina",
    "stage_map": {
      "lead_qualified": "qualified",
      "appointment_booked": "booked",
      "appointment_attended": "held",
      "first_payment": "paid"
    },
    "stages": [
      "lead_created",
      "lead_qualified",
      "appointment_booked",
      "appointment_attended",
      "first_payment"
    ],
    "updated_at": "2026-10-11T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/conversion-leads

Configure the Conversion Leads feed

Replaces the feed configuration: the switch, the CRM name Meta shows and the stage mapping. A stage with no entry is not sent.

Body

FieldTypeRequiredConstraints
enabledbooleanyes
lead_event_sourcestring—patrón ^[A-Za-z0-9][A-Za-z0-9 _-]{0,63}$, por defecto "Vitrina"
stage_mapobjectyes
curl -X PUT https://api.vitrinadev.com/api/v1/ads/conversion-leads \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "lead_event_source": "Vitrina",
    "stage_map": {
      "lead_qualified": "qualified",
      "appointment_booked": "booked",
      "appointment_attended": "held",
      "first_payment": "paid"
    }
  }'

Example response (200)

{
  "data": {
    "dataset": {
      "state": "missing",
      "failed_7d": null,
      "sent_7d": null
    },
    "enabled": true,
    "lead_event_source": "Vitrina",
    "stage_map": {
      "lead_qualified": "qualified",
      "appointment_booked": "booked",
      "appointment_attended": "held",
      "first_payment": "paid"
    },
    "stages": [
      "lead_created",
      "lead_qualified",
      "appointment_booked",
      "appointment_attended",
      "first_payment"
    ],
    "updated_at": "2026-10-11T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-source

Where bookings, sales and payments come from

The workspace’s Conversion source for Vitrina Ads, derived on every read from its live connections and never stored. crm: the CRM the workspace connected (GoHighLevel) while that connection is live — otherwise vitrina, Vitrina’s own bookings and sales, which take over the moment the CRM is disconnected. payments: the connected payment provider (Mercado Pago) while live — otherwise vitrina. A needs_reconnect connection is still the source; its data stops until it is reconnected. Connect or disconnect a provider with /integrations/connections/\{provider\}/…. The same object rides on /ads/overview (conversion_source) and /ads/feed (meta.conversion_source).

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/conversion-source \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "crm": {
      "source": "gohighlevel",
      "display_name": "GoHighLevel",
      "connection_state": "connected"
    },
    "payments": {
      "source": "mercadopago",
      "display_name": "Mercado Pago",
      "connection_state": "connected"
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/catalog

Conversion Sync catalog: definitions, connections, destinations, privacy settings

Step 0 of the setup. definitions[].id is a rule's source_ref_id; connections[].id is a destination's connection_id. Relayed verbatim from the conversion provider. Gated by the Add-on like every /ads/* route but /ads/state. Requires ads:read.

curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/catalog \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "definitions": [
      {
        "id": "f1f1f1f1-0000-4000-8000-000000000001",
        "conversion_key": "lead_created",
        "source_event_names": [
          "lead_created"
        ]
      },
      {
        "id": "f1f1f1f1-0000-4000-8000-000000000002",
        "conversion_key": "closed_won",
        "source_event_names": [
          "closed_won"
        ]
      }
    ],
    "connections": [
      {
        "id": "c0c0c0c0-0000-4000-8000-000000000001",
        "provider": "meta_ads"
      }
    ],
    "destinations": [
      {
        "id": "d0d0d0d0-0000-4000-8000-000000000001",
        "destination": "meta_capi",
        "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
        "meta_pixel_id": "1234567890123456",
        "enabled": true
      }
    ],
    "settings": {
      "privacy_mode": "platform_safe",
      "privacy_config": {
        "parameter_allowlist": [
          "value",
          "currency"
        ]
      },
      "export_meta_enabled": true,
      "export_google_enabled": false
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/connections/{id}/pixels

The Meta pixels on a Meta Ads connection's ad account

Where meta_pixel_id comes from. One live Meta Graph call per request. Requires ads:read.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/connections/<id>/pixels \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "1234567890123456",
      "name": "Píxel del sitio",
      "created_at": "2025-11-03T15:20:00.000Z",
      "last_fired_at": "2026-09-22T11:58:40.000Z",
      "is_unavailable": false
    }
  ]
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/data-sources

The Meta datasets (WhatsApp, pixel, Instagram): state, prerequisites and a 30-day preview

The Meta destinations of the workspace’s outcomes, each set up by its own explicit press, never as a side effect. Step 1 — the dataset linked to the WhatsApp Business Account — receives the outcomes carrying the click-to-WhatsApp ad click (ctwa_clid), as business_messaging; a click-to-WhatsApp ad set optimised on Purchase needs it. Step 2 — the website pixel — receives every other outcome, matched by the hashed phone and e-mail; without it those outcomes (most payments) are not sent at all. Step 3 — the dataset linked to the Instagram account — receives the outcomes of conversations an Instagram ad opened, as business_messaging with the customer’s Instagram-scoped id (measurement only: Meta offers no Purchase optimisation on Instagram); until it exists they reach the website pixel as before. Each sends from its press on (no history), and nothing flows until the stages are switched on in «Enviar a Meta». Meta cannot delete a dataset. Every state is the measurement engine’s read-back: sending only when it confirms an enabled destination AND an enabled Meta rule. coming_soon until the engine can create it. Each step carries the notice to show before its press; the pixel step lists the ad account’s pixels to choose. Requires ads:read.

curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "prerequisites": {
      "meta_ads_connected": true,
      "whatsapp_connected": true,
      "instagram_connected": true
    },
    "whatsapp": {
      "kind": "whatsapp",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-whatsapp-dataset-2026-10-02",
        "points": [
          "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "website": {
      "kind": "website",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-website-pixel-2026-10-02",
        "points": [
          "Tu píxel del sitio recibe los resultados que no traen el clic…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      },
      "candidates": [
        {
          "external_id": "<pixel_id>",
          "id_tail": "9748",
          "name": "Píxel del sitio",
          "last_fired_at": "2026-10-01T22:10:00.000Z",
          "recommended": true
        }
      ],
      "can_create_pixel": false
    },
    "instagram": {
      "kind": "instagram",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-instagram-dataset-2026-10-07",
        "points": [
          "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "click_less_not_sent": false,
    "preview": {
      "window_days": 30,
      "total": 120,
      "with_ad_click": 14,
      "without_ad_click": 106,
      "click_less_to": "not_sent",
      "enabled_meta_rules": 0,
      "source": "engine",
      "truncated": false,
      "stages": [
        {
          "stage": "lead_created",
          "label": "Contacto nuevo",
          "on": true,
          "total": 60,
          "with_ad_click": 9,
          "weekly": 14,
          "weekly_with_ad_click": 2.1,
          "measured_weeks": 4,
          "measured_days": 30
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/instagram/ensure

Create the Instagram dataset in Meta (idempotent)

The Meta destinations of the workspace’s outcomes, each set up by its own explicit press, never as a side effect. Step 1 — the dataset linked to the WhatsApp Business Account — receives the outcomes carrying the click-to-WhatsApp ad click (ctwa_clid), as business_messaging; a click-to-WhatsApp ad set optimised on Purchase needs it. Step 2 — the website pixel — receives every other outcome, matched by the hashed phone and e-mail; without it those outcomes (most payments) are not sent at all. Step 3 — the dataset linked to the Instagram account — receives the outcomes of conversations an Instagram ad opened, as business_messaging with the customer’s Instagram-scoped id (measurement only: Meta offers no Purchase optimisation on Instagram); until it exists they reach the website pixel as before. Each sends from its press on (no history), and nothing flows until the stages are switched on in «Enviar a Meta». Meta cannot delete a dataset. The measurement engine asks Meta to create the dataset linked to the connected Instagram account, and creates its Instagram destination; an existing destination is never repointed. Needs the CURRENT instagram.notice (version + hash from the read): 409 notice_required otherwise. A retry answers already_present; a turned-off dataset is turned back on. 409 details.reason: meta_ads_not_connected, instagram_not_connected, entitlement_required, duplicate_kind, dataset_unreadable, already_exists, engine_not_ready (the engine cannot create it yet: «Pronto»), meta_permission_missing (Meta refused for a missing app permission — instagram_manage_events), reconnect_required (the Instagram connection expired), instagram_facebook_login_required (the Instagram account is connected with Instagram Login; Meta creates the dataset only from a Facebook Login for Business connection linked to a Page — the read-back already carries it as the step's reason_code, with no press offered); nothing is created on any refusal. 502 when Meta failed (nothing was created). The answer carries the engine’s read-back. Requires ads:write.

Body

FieldTypeRequiredConstraints
notice_versionstringyesmín. 1, máx. 80
notice_hashstringyespatrón ^[0-9a-f]{64}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/instagram/ensure \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notice_version": "meta-instagram-dataset-2026-10-07",
    "notice_hash": "0000000000000000000000000000000000000000000000000000000000000000"
  }'

Example response (200)

{
  "data": {
    "outcome": "created",
    "status": {
      "prerequisites": {
        "meta_ads_connected": true,
        "whatsapp_connected": true,
        "instagram_connected": true
      },
      "whatsapp": {
        "kind": "whatsapp",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-whatsapp-dataset-2026-10-02",
          "points": [
            "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "website": {
        "kind": "website",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-website-pixel-2026-10-02",
          "points": [
            "Tu píxel del sitio recibe los resultados que no traen el clic…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        },
        "candidates": [
          {
            "external_id": "<pixel_id>",
            "id_tail": "9748",
            "name": "Píxel del sitio",
            "last_fired_at": "2026-10-01T22:10:00.000Z",
            "recommended": true
          }
        ],
        "can_create_pixel": false
      },
      "instagram": {
        "kind": "instagram",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<dataset_id>",
          "id_tail": "3307",
          "export_from": "2026-10-07T15:00:00.000Z",
          "destination_enabled": true,
          "access_granted": true,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": true,
        "notice": {
          "version": "meta-instagram-dataset-2026-10-07",
          "points": [
            "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "click_less_not_sent": false,
      "preview": {
        "window_days": 30,
        "total": 120,
        "with_ad_click": 14,
        "without_ad_click": 106,
        "click_less_to": "not_sent",
        "enabled_meta_rules": 0,
        "source": "engine",
        "truncated": false,
        "stages": [
          {
            "stage": "lead_created",
            "label": "Contacto nuevo",
            "on": true,
            "total": 60,
            "with_ad_click": 9,
            "weekly": 14,
            "weekly_with_ad_click": 2.1,
            "measured_weeks": 4,
            "measured_days": 30
          }
        ]
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/instagram/turn-off

Stop sending to the Instagram dataset

Turns the Instagram dataset’s destination off: nothing more reaches it, and the Instagram-ad outcomes go back to the website pixel. The dataset stays in the Meta account (Meta cannot delete one); creating it again turns it back on. 409 not_sending when there is nothing to turn off. Requires ads:write.

curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/instagram/turn-off \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "prerequisites": {
      "meta_ads_connected": true,
      "whatsapp_connected": true,
      "instagram_connected": true
    },
    "whatsapp": {
      "kind": "whatsapp",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-whatsapp-dataset-2026-10-02",
        "points": [
          "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "website": {
      "kind": "website",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-website-pixel-2026-10-02",
        "points": [
          "Tu píxel del sitio recibe los resultados que no traen el clic…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      },
      "candidates": [
        {
          "external_id": "<pixel_id>",
          "id_tail": "9748",
          "name": "Píxel del sitio",
          "last_fired_at": "2026-10-01T22:10:00.000Z",
          "recommended": true
        }
      ],
      "can_create_pixel": false
    },
    "instagram": {
      "kind": "instagram",
      "state": "off",
      "reason_code": null,
      "message": null,
      "dataset": {
        "external_id": "<dataset_id>",
        "id_tail": "3307",
        "export_from": "2026-10-07T15:00:00.000Z",
        "destination_enabled": false,
        "access_granted": true,
        "shared": false,
        "shared_count": null
      },
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-instagram-dataset-2026-10-07",
        "points": [
          "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "click_less_not_sent": false,
    "preview": {
      "window_days": 30,
      "total": 120,
      "with_ad_click": 14,
      "without_ad_click": 106,
      "click_less_to": "not_sent",
      "enabled_meta_rules": 0,
      "source": "engine",
      "truncated": false,
      "stages": [
        {
          "stage": "lead_created",
          "label": "Contacto nuevo",
          "on": true,
          "total": 60,
          "with_ad_click": 9,
          "weekly": 14,
          "weekly_with_ad_click": 2.1,
          "measured_weeks": 4,
          "measured_days": 30
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/website/create-pixel

Create a pixel in the ad account and connect it as the website pixel («Crear píxel»)

The Meta destinations of the workspace’s outcomes, each set up by its own explicit press, never as a side effect. Step 1 — the dataset linked to the WhatsApp Business Account — receives the outcomes carrying the click-to-WhatsApp ad click (ctwa_clid), as business_messaging; a click-to-WhatsApp ad set optimised on Purchase needs it. Step 2 — the website pixel — receives every other outcome, matched by the hashed phone and e-mail; without it those outcomes (most payments) are not sent at all. Step 3 — the dataset linked to the Instagram account — receives the outcomes of conversations an Instagram ad opened, as business_messaging with the customer’s Instagram-scoped id (measurement only: Meta offers no Purchase optimisation on Instagram); until it exists they reach the website pixel as before. Each sends from its press on (no history), and nothing flows until the stages are switched on in «Enviar a Meta». Meta cannot delete a dataset. For an ad account with no pixel (website.can_create_pixel): the measurement engine creates ONE pixel named «<workspace> · Vitrina» in the ad account (or adopts a pixel already carrying that name) and connects it exactly as website/ensure does. Meta cannot delete a pixel; a retry replays (idempotent per workspace and name) and answers already_present. Needs the CURRENT website.notice (409 notice_required). 409 details.reason: website_pixel_connected (another pixel is already connected; nothing created), pixel_slot_taken (Meta allows one self-created pixel per ad account and it is used: connect it with website/ensure), pixel_already_attached, meta_ads_not_connected, meta_write_permission_missing (reconnect Meta Ads granting every permission), reconnect_required, entitlement_required, engine_not_ready («Pronto»). 502 when Meta failed (nothing was created). The answer carries the engine’s read-back. Requires ads:write.

Body

FieldTypeRequiredConstraints
notice_versionstringyesmín. 1, máx. 80
notice_hashstringyespatrón ^[0-9a-f]{64}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/website/create-pixel \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notice_version": "meta-website-pixel-2026-10-02",
    "notice_hash": "0000000000000000000000000000000000000000000000000000000000000000"
  }'

Example response (200)

{
  "data": {
    "outcome": "created",
    "status": {
      "prerequisites": {
        "meta_ads_connected": true,
        "whatsapp_connected": true,
        "instagram_connected": true
      },
      "whatsapp": {
        "kind": "whatsapp",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<dataset_id>",
          "id_tail": "4821",
          "export_from": "2026-10-02T15:00:00.000Z",
          "destination_enabled": true,
          "access_granted": true,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": true,
        "notice": {
          "version": "meta-whatsapp-dataset-2026-10-02",
          "points": [
            "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "website": {
        "kind": "website",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<new_pixel_id>",
          "id_tail": "7890",
          "export_from": "2026-10-02T15:05:00.000Z",
          "destination_enabled": true,
          "access_granted": null,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": false,
        "notice": {
          "version": "meta-website-pixel-2026-10-02",
          "points": [
            "Tu píxel del sitio recibe los resultados que no traen el clic…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        },
        "candidates": [],
        "can_create_pixel": false
      },
      "instagram": {
        "kind": "instagram",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-instagram-dataset-2026-10-07",
          "points": [
            "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "click_less_not_sent": false,
      "preview": {
        "window_days": 30,
        "total": 120,
        "with_ad_click": 14,
        "without_ad_click": 106,
        "click_less_to": "not_sent",
        "enabled_meta_rules": 0,
        "source": "engine",
        "truncated": false,
        "stages": [
          {
            "stage": "lead_created",
            "label": "Contacto nuevo",
            "on": true,
            "total": 60,
            "with_ad_click": 9,
            "weekly": 14,
            "weekly_with_ad_click": 2.1,
            "measured_weeks": 4,
            "measured_days": 30
          }
        ]
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/website/ensure

Connect the website pixel as the destination of click-less outcomes

The Meta destinations of the workspace’s outcomes, each set up by its own explicit press, never as a side effect. Step 1 — the dataset linked to the WhatsApp Business Account — receives the outcomes carrying the click-to-WhatsApp ad click (ctwa_clid), as business_messaging; a click-to-WhatsApp ad set optimised on Purchase needs it. Step 2 — the website pixel — receives every other outcome, matched by the hashed phone and e-mail; without it those outcomes (most payments) are not sent at all. Step 3 — the dataset linked to the Instagram account — receives the outcomes of conversations an Instagram ad opened, as business_messaging with the customer’s Instagram-scoped id (measurement only: Meta offers no Purchase optimisation on Instagram); until it exists they reach the website pixel as before. Each sends from its press on (no history), and nothing flows until the stages are switched on in «Enviar a Meta». Meta cannot delete a dataset. external_id must be one of website.candidates (409 not_a_pixel otherwise). Needs the CURRENT website.notice (409 notice_required). 409 engine_not_ready while the measurement engine cannot wire a pixel yet («Pronto»). Turning the pixel off is «Enviar a Meta»’s send_stop. The answer carries the engine’s read-back. Requires ads:write.

Body

FieldTypeRequiredConstraints
notice_versionstringyesmín. 1, máx. 80
notice_hashstringyespatrón ^[0-9a-f]{64}$
external_idstringyespatrón ^\d{6,20}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/website/ensure \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notice_version": "meta-website-pixel-2026-10-02",
    "notice_hash": "0000000000000000000000000000000000000000000000000000000000000000",
    "external_id": "<pixel_id>"
  }'

Example response (200)

{
  "data": {
    "outcome": "created",
    "status": {
      "prerequisites": {
        "meta_ads_connected": true,
        "whatsapp_connected": true,
        "instagram_connected": true
      },
      "whatsapp": {
        "kind": "whatsapp",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<dataset_id>",
          "id_tail": "4821",
          "export_from": "2026-10-02T15:00:00.000Z",
          "destination_enabled": true,
          "access_granted": true,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": true,
        "notice": {
          "version": "meta-whatsapp-dataset-2026-10-02",
          "points": [
            "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "website": {
        "kind": "website",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<pixel_id>",
          "id_tail": "9748",
          "export_from": "2026-10-02T15:05:00.000Z",
          "destination_enabled": true,
          "access_granted": null,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": false,
        "notice": {
          "version": "meta-website-pixel-2026-10-02",
          "points": [
            "Tu píxel del sitio recibe los resultados que no traen el clic…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        },
        "candidates": [],
        "can_create_pixel": false
      },
      "instagram": {
        "kind": "instagram",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-instagram-dataset-2026-10-07",
          "points": [
            "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "click_less_not_sent": false,
      "preview": {
        "window_days": 30,
        "total": 120,
        "with_ad_click": 14,
        "without_ad_click": 106,
        "click_less_to": "not_sent",
        "enabled_meta_rules": 0,
        "source": "engine",
        "truncated": false,
        "stages": [
          {
            "stage": "lead_created",
            "label": "Contacto nuevo",
            "on": true,
            "total": 60,
            "with_ad_click": 9,
            "weekly": 14,
            "weekly_with_ad_click": 2.1,
            "measured_weeks": 4,
            "measured_days": 30
          }
        ]
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/whatsapp/ensure

Create the WhatsApp dataset in Meta (idempotent)

The Meta destinations of the workspace’s outcomes, each set up by its own explicit press, never as a side effect. Step 1 — the dataset linked to the WhatsApp Business Account — receives the outcomes carrying the click-to-WhatsApp ad click (ctwa_clid), as business_messaging; a click-to-WhatsApp ad set optimised on Purchase needs it. Step 2 — the website pixel — receives every other outcome, matched by the hashed phone and e-mail; without it those outcomes (most payments) are not sent at all. Step 3 — the dataset linked to the Instagram account — receives the outcomes of conversations an Instagram ad opened, as business_messaging with the customer’s Instagram-scoped id (measurement only: Meta offers no Purchase optimisation on Instagram); until it exists they reach the website pixel as before. Each sends from its press on (no history), and nothing flows until the stages are switched on in «Enviar a Meta». Meta cannot delete a dataset. Needs the CURRENT whatsapp.notice (version + hash from the read): 409 notice_required otherwise. A retry answers already_present; a turned-off dataset is turned back on. 409 details.reason: meta_ads_not_connected, whatsapp_not_connected, entitlement_required, duplicate_kind, dataset_unreadable, already_exists, engine_not_ready. 502 when Meta failed (nothing was created). The answer carries the engine’s read-back. Requires ads:write.

Body

FieldTypeRequiredConstraints
notice_versionstringyesmín. 1, máx. 80
notice_hashstringyespatrón ^[0-9a-f]{64}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/whatsapp/ensure \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notice_version": "meta-whatsapp-dataset-2026-10-02",
    "notice_hash": "0000000000000000000000000000000000000000000000000000000000000000"
  }'

Example response (200)

{
  "data": {
    "outcome": "created",
    "status": {
      "prerequisites": {
        "meta_ads_connected": true,
        "whatsapp_connected": true,
        "instagram_connected": true
      },
      "whatsapp": {
        "kind": "whatsapp",
        "state": "created",
        "reason_code": null,
        "message": null,
        "dataset": {
          "external_id": "<dataset_id>",
          "id_tail": "4821",
          "export_from": "2026-10-02T15:00:00.000Z",
          "destination_enabled": true,
          "access_granted": true,
          "shared": false,
          "shared_count": null
        },
        "can_create": false,
        "can_turn_off": true,
        "notice": {
          "version": "meta-whatsapp-dataset-2026-10-02",
          "points": [
            "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "website": {
        "kind": "website",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-website-pixel-2026-10-02",
          "points": [
            "Tu píxel del sitio recibe los resultados que no traen el clic…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        },
        "candidates": [
          {
            "external_id": "<pixel_id>",
            "id_tail": "9748",
            "name": "Píxel del sitio",
            "last_fired_at": "2026-10-01T22:10:00.000Z",
            "recommended": true
          }
        ],
        "can_create_pixel": false
      },
      "instagram": {
        "kind": "instagram",
        "state": "ready",
        "reason_code": null,
        "message": null,
        "dataset": null,
        "can_create": true,
        "can_turn_off": false,
        "notice": {
          "version": "meta-instagram-dataset-2026-10-07",
          "points": [
            "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
          ],
          "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
      },
      "click_less_not_sent": true,
      "preview": {
        "window_days": 30,
        "total": 120,
        "with_ad_click": 14,
        "without_ad_click": 106,
        "click_less_to": "not_sent",
        "enabled_meta_rules": 0,
        "source": "engine",
        "truncated": false,
        "stages": [
          {
            "stage": "lead_created",
            "label": "Contacto nuevo",
            "on": true,
            "total": 60,
            "with_ad_click": 9,
            "weekly": 14,
            "weekly_with_ad_click": 2.1,
            "measured_weeks": 4,
            "measured_days": 30
          }
        ]
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/data-sources/whatsapp/turn-off

Stop sending to the WhatsApp dataset

Turns the dataset’s destination off: nothing more reaches it. The dataset stays in the Meta account (Meta cannot delete one); creating it again turns it back on. 409 not_sending when there is nothing to turn off. Requires ads:write.

curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/data-sources/whatsapp/turn-off \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "prerequisites": {
      "meta_ads_connected": true,
      "whatsapp_connected": true,
      "instagram_connected": true
    },
    "whatsapp": {
      "kind": "whatsapp",
      "state": "off",
      "reason_code": null,
      "message": null,
      "dataset": {
        "external_id": "<dataset_id>",
        "id_tail": "4821",
        "export_from": "2026-10-02T15:00:00.000Z",
        "destination_enabled": false,
        "access_granted": true,
        "shared": false,
        "shared_count": null
      },
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-whatsapp-dataset-2026-10-02",
        "points": [
          "Meta crea un conjunto de datos conectado a tu WhatsApp Business…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "website": {
      "kind": "website",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-website-pixel-2026-10-02",
        "points": [
          "Tu píxel del sitio recibe los resultados que no traen el clic…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      },
      "candidates": [
        {
          "external_id": "<pixel_id>",
          "id_tail": "9748",
          "name": "Píxel del sitio",
          "last_fired_at": "2026-10-01T22:10:00.000Z",
          "recommended": true
        }
      ],
      "can_create_pixel": false
    },
    "instagram": {
      "kind": "instagram",
      "state": "ready",
      "reason_code": null,
      "message": null,
      "dataset": null,
      "can_create": true,
      "can_turn_off": false,
      "notice": {
        "version": "meta-instagram-dataset-2026-10-07",
        "points": [
          "Meta crea un conjunto de datos conectado a tu cuenta de Instagram…"
        ],
        "hash": "0000000000000000000000000000000000000000000000000000000000000000"
      }
    },
    "click_less_not_sent": false,
    "preview": {
      "window_days": 30,
      "total": 120,
      "with_ad_click": 14,
      "without_ad_click": 106,
      "click_less_to": "not_sent",
      "enabled_meta_rules": 0,
      "source": "engine",
      "truncated": false,
      "stages": [
        {
          "stage": "lead_created",
          "label": "Contacto nuevo",
          "on": true,
          "total": 60,
          "with_ad_click": 9,
          "weekly": 14,
          "weekly_with_ad_click": 2.1,
          "measured_weeks": 4,
          "measured_days": 30
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/destinations

The configured export destinations (pre-fills a re-run)

Relayed verbatim from the conversion provider, including each destination's routing. Requires ads:read.

curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/destinations \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "privacy_mode": "platform_safe",
    "export_meta_enabled": true,
    "export_google_enabled": false,
    "destinations": [
      {
        "id": "d0d0d0d0-0000-4000-8000-000000000001",
        "destination": "meta_capi",
        "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
        "meta_pixel_id": "1234567890123456",
        "enabled": true
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/destinations

Create or update the Meta CAPI destination

Creates the meta_capi destination (or updates the existing one on a re-run). For a clinic (vertical = healthcare) the profile is first switched to privacy_mode = platform_safe and the write is refused unless the read-back confirms it. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

Body

FieldTypeRequiredConstraints
connection_iduuidyes
meta_pixel_idstringyespatrón ^\d{5,32}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/destinations \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
    "meta_pixel_id": "1234567890123456"
  }'

Example response (200)

{
  "data": {
    "action": "created",
    "destination": {
      "id": "d0d0d0d0-0000-4000-8000-000000000001",
      "destination": "meta_capi",
      "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
      "meta_pixel_id": "1234567890123456",
      "enabled": true
    },
    "privacy": {
      "required": "platform_safe",
      "actual": "platform_safe",
      "applied": true
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/messaging-event-names

Each stage’s Meta event name on the website and WhatsApp lanes

Per stage the workspace sends: the website pixel lane’s name (website_event, fixed), the WhatsApp (business_messaging) lane’s default (messaging_default) and the name in force (messaging_event: the workspace’s choice, else the default; null = not sent on that lane). accepted_events are the 14 names Meta’s WhatsApp dataset accepts. Change them with PATCH on this path. sample=1 (or a sandbox) answers the defaults. Requires ads:read.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/messaging-event-names \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "overrides": {
      "lead_qualified": "InitiateCheckout"
    },
    "accepted_events": [
      "Purchase",
      "LeadSubmitted",
      "InitiateCheckout",
      "AddToCart",
      "ViewContent",
      "OrderCreated",
      "OrderShipped",
      "OrderDelivered",
      "OrderCanceled",
      "OrderReturned",
      "CartAbandoned",
      "QualifiedLead",
      "RatingProvided",
      "ReviewProvided"
    ],
    "stages": [
      {
        "stage": "lead_created",
        "label": "Contacto nuevo",
        "website_event": "Lead",
        "messaging_default": "LeadSubmitted",
        "messaging_event": "LeadSubmitted",
        "overridden": false
      },
      {
        "stage": "lead_qualified",
        "label": "Cotizó",
        "website_event": "QualifiedLead",
        "messaging_default": "QualifiedLead",
        "messaging_event": "InitiateCheckout",
        "overridden": true
      },
      {
        "stage": "appointment_booked",
        "label": "Visita agendada",
        "website_event": "Schedule",
        "messaging_default": "QualifiedLead",
        "messaging_event": "QualifiedLead",
        "overridden": false
      },
      {
        "stage": "checkout_started",
        "label": "Abrió el link de pago",
        "website_event": "InitiateCheckout",
        "messaging_default": "InitiateCheckout",
        "messaging_event": "InitiateCheckout",
        "overridden": false
      },
      {
        "stage": "deposit_paid",
        "label": "Abono pagado",
        "website_event": "vitrina_step_8",
        "messaging_default": "OrderCreated",
        "messaging_event": "OrderCreated",
        "overridden": false
      },
      {
        "stage": "closed_won",
        "label": "Venta o compra",
        "website_event": "Purchase",
        "messaging_default": "Purchase",
        "messaging_event": "Purchase",
        "overridden": false
      },
      {
        "stage": "payment_received",
        "label": "Pago recibido",
        "website_event": "Purchase",
        "messaging_default": "Purchase",
        "messaging_event": "Purchase",
        "overridden": false
      },
      {
        "stage": "seller_qualified",
        "label": "Vendedor calificado",
        "website_event": "SellerQualified",
        "messaging_default": "AddToCart",
        "messaging_event": "AddToCart",
        "overridden": false
      },
      {
        "stage": "seller_visit_booked",
        "label": "Vendedor con visita agendada",
        "website_event": "SellerVisitScheduled",
        "messaging_default": null,
        "messaging_event": null,
        "overridden": false
      },
      {
        "stage": "seller_deal_closed",
        "label": "Compra cerrada a un vendedor",
        "website_event": "SellerDealClosed",
        "messaging_default": "OrderDelivered",
        "messaging_event": "OrderDelivered",
        "overridden": false
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PATCH /ads/conversion-sync/messaging-event-names

Choose a stage’s Meta event name on the WhatsApp lane

Merges onto the stored choices: overrides sets a stage’s name (one of accepted_events, or null = do not send it on the WhatsApp lane), reset drops a choice (back to messaging_default). The website lane never changes. The RESULT is validated, and nothing is stored on a 422 (field_errors.overrides.&lt;stage&gt;, details.problems.&lt;stage&gt;): unknown_stage (the workspace does not send it), stage_not_sent (instalments), name_not_accepted, purchase_needs_value (Purchase only on a stage that carries a value), seller_name_shared (a seller-side stage never shares its name with a buyer stage, so an ad set that optimises on buyers never learns from sellers; buyer stages may share). Then the engine’s live rules are converged at once (sync.state: converged, sync.updated = the stages rewritten); pending = stored, the daily sweep retries the write. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

curl -X PATCH https://api.vitrinadev.com/api/v1/ads/conversion-sync/messaging-event-names \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "overrides": {
      "lead_qualified": "InitiateCheckout"
    }
  }'

Example response (200)

{
  "data": {
    "overrides": {
      "lead_qualified": "InitiateCheckout"
    },
    "accepted_events": [
      "Purchase",
      "LeadSubmitted",
      "InitiateCheckout",
      "AddToCart",
      "ViewContent",
      "OrderCreated",
      "OrderShipped",
      "OrderDelivered",
      "OrderCanceled",
      "OrderReturned",
      "CartAbandoned",
      "QualifiedLead",
      "RatingProvided",
      "ReviewProvided"
    ],
    "stages": [
      {
        "stage": "lead_created",
        "label": "Contacto nuevo",
        "website_event": "Lead",
        "messaging_default": "LeadSubmitted",
        "messaging_event": "LeadSubmitted",
        "overridden": false
      },
      {
        "stage": "lead_qualified",
        "label": "Cotizó",
        "website_event": "QualifiedLead",
        "messaging_default": "QualifiedLead",
        "messaging_event": "InitiateCheckout",
        "overridden": true
      },
      {
        "stage": "appointment_booked",
        "label": "Visita agendada",
        "website_event": "Schedule",
        "messaging_default": "QualifiedLead",
        "messaging_event": "QualifiedLead",
        "overridden": false
      },
      {
        "stage": "checkout_started",
        "label": "Abrió el link de pago",
        "website_event": "InitiateCheckout",
        "messaging_default": "InitiateCheckout",
        "messaging_event": "InitiateCheckout",
        "overridden": false
      },
      {
        "stage": "deposit_paid",
        "label": "Abono pagado",
        "website_event": "vitrina_step_8",
        "messaging_default": "OrderCreated",
        "messaging_event": "OrderCreated",
        "overridden": false
      },
      {
        "stage": "closed_won",
        "label": "Venta o compra",
        "website_event": "Purchase",
        "messaging_default": "Purchase",
        "messaging_event": "Purchase",
        "overridden": false
      },
      {
        "stage": "payment_received",
        "label": "Pago recibido",
        "website_event": "Purchase",
        "messaging_default": "Purchase",
        "messaging_event": "Purchase",
        "overridden": false
      },
      {
        "stage": "seller_qualified",
        "label": "Vendedor calificado",
        "website_event": "SellerQualified",
        "messaging_default": "AddToCart",
        "messaging_event": "AddToCart",
        "overridden": false
      },
      {
        "stage": "seller_visit_booked",
        "label": "Vendedor con visita agendada",
        "website_event": "SellerVisitScheduled",
        "messaging_default": null,
        "messaging_event": null,
        "overridden": false
      },
      {
        "stage": "seller_deal_closed",
        "label": "Compra cerrada a un vendedor",
        "website_event": "SellerDealClosed",
        "messaging_default": "OrderDelivered",
        "messaging_event": "OrderDelivered",
        "overridden": false
      }
    ],
    "sync": {
      "state": "converged",
      "updated": [
        "lead_qualified"
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/preview-event

A real event, field by field, as Meta receives it

Built from the workspace’s own last event of the stage (else an example, source: example). Each field says how it reaches Meta — sent, hashed (matchable, not readable), withheld or absent — and identifiers never carry a value or a hash. never_sent lists what never leaves Vitrina. Requires ads:read.

ParameterInTypeRequiredConstraints
stagequerylead_created \lead_qualified \appointment_booked \
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/preview-event \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "stage": "first_payment",
    "source": "own",
    "vitrina": {
      "stage_label": "Primer pago de un tratamiento nuevo",
      "occurred_at": "2026-09-25T08:40:00.000Z",
      "value_clp": 1200000,
      "contact_tail": "a3f2"
    },
    "meta": {
      "event": "Purchase",
      "value_clp": 1038075,
      "capped": true,
      "value_cap_clp": 1038075,
      "reaches_meta": true,
      "not_sent_reason": null,
      "fields": [
        {
          "key": "event",
          "label": "Evento",
          "state": "sent",
          "display": "Purchase"
        },
        {
          "key": "time",
          "label": "Cuándo",
          "state": "sent",
          "display": "2026-09-25T08:40:00.000Z"
        },
        {
          "key": "value",
          "label": "Valor",
          "state": "sent",
          "display": "1.038.075 CLP"
        },
        {
          "key": "action_source",
          "label": "Dónde pasó",
          "state": "sent",
          "display": "business_messaging",
          "display_channel": "whatsapp"
        },
        {
          "key": "email",
          "label": "Correo",
          "state": "hashed"
        },
        {
          "key": "phone",
          "label": "Teléfono",
          "state": "hashed"
        },
        {
          "key": "contact_ref",
          "label": "Id del contacto",
          "state": "hashed"
        },
        {
          "key": "ad_click",
          "label": "Clic del anuncio",
          "state": "sent"
        }
      ]
    },
    "never_sent": [
      "national_id",
      "clinical_data",
      "notes",
      "treatment_name",
      "site_urls"
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/rules/sync

Create or update one export rule per outcome Vitrina emits

Automotive: lead_created→Lead, appointment_booked→Schedule, payment_received→Purchase (event value). Clinic: lead_created→Lead, appointment_booked→Schedule, appointment_attended and quote_presented→generic custom events without value, payment_received with its Meta destination DISABLED (instalments never reach Meta), and the Purchase on the stage the clinic chose: the accepted presupuesto (closed_won, with its accepted value; the default) or — after the owner’s consented switch in «Enviar a Meta» (send_stage:first_payment:on) — the first payment of each accepted plan (first_payment, once per plan), closed_won then being a custom event without value. A clinic that keeps NO plans (no presupuesto, no mirrored plan — its data decides, never a setting) has the same first_payment rule carry each PERSON’s first payment instead: the first payment > 0 of a person, once ever, at the amount paid. This route NEVER moves the Purchase, and a stage turned off from «Enviar a Meta» stays off. Idempotent: matched to existing rules by definition; a rule that already holds its body is unchanged (nothing written). A definition missing from the catalog is reported as skipped. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/rules/sync \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "vertical": "healthcare",
    "rules": [
      {
        "stage": "lead_created",
        "action": "created",
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000001",
        "meta_enabled": true,
        "meta_event_name": "Lead"
      },
      {
        "stage": "appointment_booked",
        "action": "created",
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000002",
        "meta_enabled": true,
        "meta_event_name": "Schedule"
      },
      {
        "stage": "closed_won",
        "action": "created",
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000003",
        "meta_enabled": true,
        "meta_event_name": "Purchase"
      },
      {
        "stage": "payment_received",
        "action": "created",
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000004",
        "meta_enabled": false,
        "meta_event_name": "Purchase"
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/setup

One action: privacy mode (clinics) → destination → rules

The setup in one call — the same work as the destination write followed by the rules sync. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

Body

FieldTypeRequiredConstraints
connection_iduuidyes
meta_pixel_idstringyespatrón ^\d{5,32}$
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/setup \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
    "meta_pixel_id": "1234567890123456"
  }'

Example response (200)

{
  "data": {
    "destination": {
      "action": "created",
      "destination": {
        "id": "d0d0d0d0-0000-4000-8000-000000000001",
        "destination": "meta_capi",
        "connection_id": "c0c0c0c0-0000-4000-8000-000000000001",
        "meta_pixel_id": "1234567890123456",
        "enabled": true
      },
      "privacy": {
        "required": "platform_safe",
        "actual": "platform_safe",
        "applied": true
      }
    },
    "rules": {
      "vertical": "healthcare",
      "rules": [
        {
          "stage": "lead_created",
          "action": "created",
          "rule_id": "e1e1e1e1-0000-4000-8000-000000000001",
          "meta_enabled": true,
          "meta_event_name": "Lead"
        },
        {
          "stage": "appointment_booked",
          "action": "created",
          "rule_id": "e1e1e1e1-0000-4000-8000-000000000002",
          "meta_enabled": true,
          "meta_event_name": "Schedule"
        },
        {
          "stage": "closed_won",
          "action": "created",
          "rule_id": "e1e1e1e1-0000-4000-8000-000000000003",
          "meta_enabled": true,
          "meta_event_name": "Purchase"
        },
        {
          "stage": "payment_received",
          "action": "created",
          "rule_id": "e1e1e1e1-0000-4000-8000-000000000004",
          "meta_enabled": false,
          "meta_event_name": "Purchase"
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/status

What reaches Meta, stage by stage («Enviar a Meta»)

One read for the «Enviar a Meta» page and the wizard step: the state (not_started | on | partial | off | blocked, wizard_done = the step is resolved), the datasets of the connected ad account with the recommended one (the dataset active ad sets already optimise on, else the most recent), one lane per stage (the Meta event, its value, on/off, a typed wiring verdict, active ad sets, deliveries over 7 days with a per-day series), the Meta grant, the health-data protection, the owner’s consent and Meta links built on the server. The Meta connection is resolved server-side. A stage the workspace cannot send yet because its definition is missing heals on its own. sample=1 (or a sandbox) answers the healthy sample page with a scripted arrival per lane (sample_arrival_ms). Poll-safe (Cache-Control: no-store). Changes go through GET /ads/actions?screen=envio (send_setup, send_stage, send_dataset, send_stop). Requires ads:read.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/status \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "state": "partial",
    "blocked_reason": null,
    "wizard_done": true,
    "meta_connection": "connected",
    "dataset": {
      "external_id": "1234567890123456",
      "id_tail": "3456",
      "name": "Clínica Dental",
      "last_fired_at": "2026-09-25T11:40:00.000Z",
      "is_unavailable": false
    },
    "datasets": [
      {
        "external_id": "1234567890123456",
        "id_tail": "3456",
        "name": "Clínica Dental",
        "last_fired_at": "2026-09-25T11:40:00.000Z",
        "is_unavailable": false,
        "recommended": true,
        "recommended_reason": "used_by_ad_sets",
        "selected": true
      }
    ],
    "whatsapp_dataset": null,
    "stages": [
      {
        "stage": "lead_created",
        "label": "Contacto nuevo",
        "meta_event": "Lead",
        "meta_event_names": [
          "Lead"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "standard",
        "meta_event_neutral": false,
        "value": "none",
        "enabled": true,
        "switchable": true,
        "lock": null,
        "awaiting_switch": false,
        "source": "available",
        "ready": true,
        "ready_reason": null,
        "count_28d": 212,
        "weekly_28d": 49.5,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 212,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 212,
        "verdict": "below_threshold",
        "active_ad_sets": 0,
        "last_sent_at": "2026-09-25T12:40:00.000Z",
        "sent_7d": 12,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "appointment_booked",
        "label": "Cita agendada",
        "meta_event": "Schedule",
        "meta_event_names": [
          "Schedule"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "standard",
        "meta_event_neutral": false,
        "value": "none",
        "enabled": true,
        "switchable": true,
        "lock": null,
        "awaiting_switch": false,
        "source": "available",
        "ready": true,
        "ready_reason": null,
        "count_28d": 96,
        "weekly_28d": 22.4,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 96,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 96,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": "2026-09-25T11:40:00.000Z",
        "sent_7d": 11,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "appointment_attended",
        "label": "Cita atendida",
        "meta_event": "vitrina_step_3",
        "meta_event_names": [
          "vitrina_step_3"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "custom",
        "meta_event_neutral": true,
        "value": "none",
        "enabled": false,
        "switchable": true,
        "lock": null,
        "awaiting_switch": false,
        "source": "available",
        "ready": false,
        "ready_reason": "off",
        "count_28d": 71,
        "weekly_28d": 16.6,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 71,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 71,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": "2026-09-25T10:40:00.000Z",
        "sent_7d": 10,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": "2026-09-23T13:40:00.000Z",
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "quote_presented",
        "label": "Presupuesto presentado",
        "meta_event": "vitrina_step_4",
        "meta_event_names": [
          "vitrina_step_4"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "custom",
        "meta_event_neutral": true,
        "value": "none",
        "enabled": true,
        "switchable": true,
        "lock": null,
        "awaiting_switch": false,
        "source": "available",
        "ready": true,
        "ready_reason": null,
        "count_28d": 40,
        "weekly_28d": 9.3,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 40,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 40,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": "2026-09-25T09:40:00.000Z",
        "sent_7d": 9,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "closed_won",
        "label": "Presupuesto aceptado",
        "meta_event": "vitrina_step_5",
        "meta_event_names": [
          "vitrina_step_5"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "custom",
        "meta_event_neutral": true,
        "value": "none",
        "enabled": true,
        "switchable": true,
        "lock": null,
        "awaiting_switch": false,
        "source": "available",
        "ready": true,
        "ready_reason": null,
        "count_28d": 22,
        "weekly_28d": 5.1,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 22,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 22,
        "verdict": "wired",
        "active_ad_sets": 2,
        "last_sent_at": "2026-09-25T08:40:00.000Z",
        "sent_7d": 8,
        "meta_volume_7d": 14,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 1
          },
          {
            "date": "2026-09-24",
            "count": 1
          },
          {
            "date": "2026-09-25",
            "count": 1
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "first_payment",
        "label": "Primer pago de un tratamiento nuevo",
        "meta_event": "Purchase",
        "meta_event_names": [
          "Purchase"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "standard",
        "meta_event_neutral": false,
        "value": "accepted_total",
        "enabled": false,
        "switchable": false,
        "lock": null,
        "awaiting_switch": false,
        "source": "definition_missing",
        "ready": false,
        "ready_reason": "definition_missing",
        "count_28d": 64,
        "weekly_28d": 14.9,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 0,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 0,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": null,
        "sent_7d": 0,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "returning_first_payment",
        "label": "Primer pago de un tratamiento de un paciente que vuelve",
        "meta_event": "vitrina_step_7",
        "meta_event_names": [
          "vitrina_step_7"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "custom",
        "meta_event_neutral": true,
        "value": "none",
        "enabled": false,
        "switchable": false,
        "lock": null,
        "awaiting_switch": false,
        "source": "definition_missing",
        "ready": false,
        "ready_reason": "definition_missing",
        "count_28d": 0,
        "weekly_28d": 0,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 0,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 0,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": null,
        "sent_7d": 0,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "payment_received",
        "label": "Pago recibido",
        "meta_event": "Purchase",
        "meta_event_names": [
          "Purchase"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "standard",
        "meta_event_neutral": false,
        "value": "payment",
        "enabled": false,
        "switchable": false,
        "lock": "installments",
        "awaiting_switch": false,
        "source": "available",
        "ready": false,
        "ready_reason": "installments",
        "count_28d": 58,
        "weekly_28d": 13.5,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 58,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 58,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": "2026-09-25T07:40:00.000Z",
        "sent_7d": 7,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "checkout_started",
        "label": "Abrió el link de pago",
        "meta_event": "InitiateCheckout",
        "meta_event_names": [
          "InitiateCheckout"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "standard",
        "meta_event_neutral": false,
        "value": "payment",
        "enabled": false,
        "switchable": false,
        "lock": null,
        "awaiting_switch": false,
        "source": "definition_missing",
        "ready": false,
        "ready_reason": "definition_missing",
        "count_28d": 0,
        "weekly_28d": 0,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 0,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 0,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": null,
        "sent_7d": 0,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      },
      {
        "stage": "deposit_paid",
        "label": "Abono pagado",
        "meta_event": "vitrina_step_8",
        "meta_event_names": [
          "vitrina_step_8"
        ],
        "meta_event_messaging": null,
        "meta_event_kind": "custom",
        "meta_event_neutral": true,
        "value": "payment",
        "enabled": false,
        "switchable": false,
        "lock": null,
        "awaiting_switch": false,
        "source": "definition_missing",
        "ready": false,
        "ready_reason": "definition_missing",
        "count_28d": 0,
        "weekly_28d": 0,
        "measured_weeks": 4,
        "measured_days": 30,
        "window_days": 30,
        "events_7d": 0,
        "with_ad_click_7d": 0,
        "events_30d": 0,
        "with_ad_click_30d": 0,
        "without_ad_click_7d": 0,
        "without_ad_click_30d": 0,
        "verdict": "not_checked",
        "active_ad_sets": 0,
        "last_sent_at": null,
        "sent_7d": 0,
        "meta_volume_7d": null,
        "series_7d": [
          {
            "date": "2026-09-19",
            "count": 0
          },
          {
            "date": "2026-09-20",
            "count": 0
          },
          {
            "date": "2026-09-21",
            "count": 0
          },
          {
            "date": "2026-09-22",
            "count": 0
          },
          {
            "date": "2026-09-23",
            "count": 0
          },
          {
            "date": "2026-09-24",
            "count": 0
          },
          {
            "date": "2026-09-25",
            "count": 0
          }
        ],
        "weekly_learning_target": 50,
        "changed_by": null,
        "changed_at": null,
        "value_capped_7d": 0,
        "value_cap_clp": null,
        "too_old_28d": 0,
        "purchases_reversed_28d": 0
      }
    ],
    "enabled_count": 4,
    "waiting_count": 0,
    "stage_count": 9,
    "first_payment_candidates": {
      "basis": "plan",
      "count_28d": 64,
      "weekly_28d": 14.9,
      "measured_weeks": 4,
      "measured_days": 30,
      "mirror_count_28d": null,
      "mirror_weekly_28d": null
    },
    "first_payment_click_match": {
      "stage": "first_payment",
      "required_7d": 50,
      "with_ad_click_7d": 0,
      "remaining_7d": 50,
      "reached": false,
      "without_ad_click_7d": 0,
      "with_ad_click_30d": 0,
      "without_ad_click_30d": 0,
      "held_back_30d": null
    },
    "restriction": {
      "kind": "unknown",
      "since": null
    },
    "protected_dataset": null,
    "recommended_optimization_stage": {
      "stage": "appointment_attended",
      "reason": "rate_band",
      "rate": 0.33490566037735847
    },
    "purchase_stage": "first_payment",
    "purchase_switch_available": false,
    "grant": "full",
    "protection_required": true,
    "protected": true,
    "consent": {
      "version": "2026-10-08.send-to-meta.v3",
      "current": true,
      "accepted_at": "2026-09-22T13:40:00.000Z"
    },
    "meta_links": {
      "create_dataset": "https://business.facebook.com/events_manager2/overview?act=1000000000000001",
      "events_manager": "https://business.facebook.com/events_manager2/list/pixel/1234567890123456/overview?act=1000000000000001",
      "test_events": "https://business.facebook.com/events_manager2/list/pixel/1234567890123456/test_events?act=1000000000000001",
      "ad_sets": "https://www.facebook.com/adsmanager/manage/adsets?act=1000000000000001"
    },
    "checked_at": "2026-09-25T10:40:00.000Z",
    "stale": false,
    "sample": false,
    "ad_set_goals": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/test

Send one Meta TEST event (Test Events only)

One synthetic event to the dataset Meta receives the workspace’s events in, always with a Test Events code (generated when omitted), so it shows in Events Manager → Test Events and never counts as a real event. result: received, rejected (Meta refused it) or blocked (the privacy protection held it back). 409 CONFLICT (details.reason: not_set_up) before the send is set up; 429 when Meta limits test events (5 a minute). sample simulates it. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

Body

FieldTypeRequiredConstraints
stagelead_created \lead_qualified \appointment_booked \
test_event_codestring—patrón ^[A-Za-z0-9_-]{4,64}$
channelwebsite \business_messaging—
sampleboolean—
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/test \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stage": "closed_won"
  }'

Example response (200)

{
  "data": {
    "result": "received",
    "test_event_code": "TEST04821",
    "event_id": "a0a0a0a0-0000-4000-8000-000000000001",
    "stage": "closed_won",
    "meta_event": "Purchase",
    "quality_warning": false,
    "test_events_link": "https://business.facebook.com/events_manager2/list/pixel/1234567890123456/test_events?act=1000000000000001",
    "simulated": false
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/conversion-sync/wiring

Whether the ad sets optimize for the events Vitrina sends

The conversion provider's per-rule verdicts and remediation, verbatim (served from a 6-hour cache; force=1 re-verifies live — still a read). privacy reports whether a clinic profile carries platform_safe. sample=1 (or a sandbox workspace) answers the sample dataset's verdicts — one wired row per rule the vertical sends to Meta, fresh, no entitlement required and nothing read — like every other Ads read. Requires ads:read.

ParameterInTypeRequiredConstraints
forcequery0 \1 \true \
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/conversion-sync/wiring \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "results": [
      {
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000001",
        "destination_id": "d0d0d0d0-0000-4000-8000-000000000001",
        "verdict": "wired",
        "event_name": "Purchase",
        "checks": {
          "connection": {
            "ok": true
          },
          "pixel": {
            "ok": true,
            "isUnavailable": false,
            "lastFiredAt": "2026-09-22T11:53:00.000Z"
          },
          "eventsArriving": {
            "ok": true,
            "volume7d": 64
          },
          "customConversion": {
            "exists": false,
            "required": false
          },
          "optimizationGoal": {
            "usedByActiveAdSets": [
              {
                "adSetName": "Implantes · Las Condes",
                "campaignName": "Implantes · Septiembre",
                "objective": "OUTCOME_SALES",
                "optimizationGoal": "OFFSITE_CONVERSIONS",
                "customEventType": "PURCHASE",
                "via": "custom_event_type"
              }
            ]
          }
        },
        "remediation": [],
        "checked_at": "2026-09-22T12:00:00.000Z",
        "stale": false
      }
    ],
    "checked_at": "2026-09-22T12:00:00.000Z",
    "stale": false,
    "ttl_ms": 21600000,
    "privacy": {
      "required": "platform_safe",
      "actual": "platform_safe",
      "applied": true
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/conversion-sync/wiring/recheck

Re-verify wiring and self-heal (creates a missing Custom Conversion)

A WRITE: the self-heal creates a Custom Conversion in the dealer's Meta ad account. Bind it to an explicit operator action only — never a page load or a poll. Requires ads:write. Errors: 402 ENTITLEMENT_NOT_ACTIVE (details.feature: vitrina_ads, with hint) when the Vitrina Ads Add-on is not active — never retried; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating, or still lacks exports:write after one automatic re-mint (the entitlement rail converges it; see GET /ads/state).

Body

FieldTypeRequiredConstraints
rule_iduuid—
curl -X POST https://api.vitrinadev.com/api/v1/ads/conversion-sync/wiring/recheck \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rule_id": "e1e1e1e1-0000-4000-8000-000000000001"
  }'

Example response (200)

{
  "data": {
    "results": [
      {
        "rule_id": "e1e1e1e1-0000-4000-8000-000000000001",
        "destination_id": "d0d0d0d0-0000-4000-8000-000000000001",
        "verdict": "wired",
        "event_name": "Purchase",
        "checks": {
          "connection": {
            "ok": true
          },
          "pixel": {
            "ok": true,
            "isUnavailable": false,
            "lastFiredAt": "2026-09-22T11:53:00.000Z"
          },
          "eventsArriving": {
            "ok": true,
            "volume7d": 64
          },
          "customConversion": {
            "exists": false,
            "required": false
          },
          "optimizationGoal": {
            "usedByActiveAdSets": [
              {
                "adSetName": "Implantes · Las Condes",
                "campaignName": "Implantes · Septiembre",
                "objective": "OUTCOME_SALES",
                "optimizationGoal": "OFFSITE_CONVERSIONS",
                "customEventType": "PURCHASE",
                "via": "custom_event_type"
              }
            ]
          }
        },
        "remediation": [],
        "checked_at": "2026-09-22T12:00:00.000Z",
        "stale": false
      }
    ],
    "checked_at": "2026-09-22T12:00:00.000Z",
    "stale": false,
    "ttl_ms": 21600000,
    "privacy": {
      "required": "platform_safe",
      "actual": "platform_safe",
      "applied": true
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/creative-assets

Lanzamiento videos

The workspace’s Lanzamiento videos, newest first: the dealer’s uploads and the generated consignment videos, each with a playback URL valid 10 minutes. A withdrawn video (its presenter consent was revoked) has no playback.

ParameterInTypeRequiredConstraints
lanzamiento_idqueryuuidno
kindquerydealer_video \generated_videono
curl https://api.vitrinadev.com/api/v1/ads/creative-assets \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "assets": [
      {
        "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
        "kind": "dealer_video",
        "status": "ready",
        "lanzamiento_id": null,
        "title": "Nico en el patio",
        "mime_type": "video/mp4",
        "byte_size": 42000000,
        "duration_seconds": 23,
        "width": 1080,
        "height": 1920,
        "aspect": "9x16",
        "variant": null,
        "run_uncut": true,
        "created_at": "2026-10-26T15:00:00.000Z",
        "playback": {
          "url": "https://storage.example.com/tenants/…/ads-creatives/01926f3e.mp4?X-Amz-Signature=…",
          "expires_at": "2026-10-26T15:10:00.000Z"
        }
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

DELETE /ads/creative-assets/{id}

Remove a Lanzamiento video

Deletes the stored video and its record. An ad already running on Meta is not touched.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X DELETE https://api.vitrinadev.com/api/v1/ads/creative-assets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 204 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/creative-assets/{id}/complete

Finish a dealer video upload

Checks the uploaded object (present, size, type) and marks the video ready. Width and height set its aspect (9x16, 4x5, 1x1, 16x9 or other). 409 upload_missing before the bytes arrive. Calling it again is a no-op.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
duration_secondsnumber—> 0, ≤ 600
widthinteger—> 0, ≤ 8192
heightinteger—> 0, ≤ 8192
curl -X POST https://api.vitrinadev.com/api/v1/ads/creative-assets/<id>/complete \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "width": 1080,
    "height": 1920,
    "duration_seconds": 23
  }'

Example response (200)

{
  "data": {
    "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
    "kind": "dealer_video",
    "status": "ready",
    "lanzamiento_id": null,
    "title": "Nico en el patio",
    "mime_type": "video/mp4",
    "byte_size": 42000000,
    "duration_seconds": 23,
    "width": 1080,
    "height": 1920,
    "aspect": "9x16",
    "variant": null,
    "run_uncut": true,
    "created_at": "2026-10-26T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/creative-assets/uploads

Start a dealer video upload

Records a dealer video (MP4 or MOV, at most 500 MB) and returns a presigned PUT URL valid 15 minutes. Upload the raw bytes there, then call complete. The dealer’s own video always runs uncut, as its own ad (run_uncut: true).

Body

FieldTypeRequiredConstraints
mime_typevideo/mp4 \video/quicktimeyes
byte_sizeintegeryes> 0
titlestring—máx. 160
lanzamiento_iduuid—
curl -X POST https://api.vitrinadev.com/api/v1/ads/creative-assets/uploads \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mime_type": "video/mp4",
    "byte_size": 42000000,
    "title": "Nico en el patio"
  }'

Example response (201)

{
  "data": {
    "asset": {
      "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
      "kind": "dealer_video",
      "status": "pending_upload",
      "lanzamiento_id": null,
      "title": "Nico en el patio",
      "mime_type": "video/mp4",
      "byte_size": null,
      "duration_seconds": null,
      "width": null,
      "height": null,
      "aspect": null,
      "variant": null,
      "run_uncut": true,
      "created_at": "2026-10-26T15:00:00.000Z"
    },
    "upload": {
      "url": "https://storage.example.com/tenants/…/ads-creatives/01926f3e.mp4?X-Amz-Signature=…",
      "method": "PUT",
      "headers": {
        "content-type": "video/mp4"
      },
      "expires_at": "2026-10-26T15:15:00.000Z"
    }
  }
}

Answers: 201 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/creatives

Top-performing ads (cohort-normalized creative scoring)

The attribution engine’s top-performing ads — a ROLLING lookback (window), not a date range: that is the real endpoint’s own shape. Each ad carries three distinct measures, presented separately — composite_score (0-100, a transparent rule blend), top_performer_likelihood (0-1, an UNVALIDATED ranker output passed through as the engine sends it — never show it as a probability; Vitrina derives nothing from it, nor from fatigue_risk_tier: the wear verdict is the observed fatigue_state = degraded) and attributed_revenue/roas (real cash attribution, present when truth_grade is attributed).

This is the PROFILE-scoped read — every Vitrina tenant’s delegated key is minted for exactly one attribution profile, so it never reads the engine’s cross-profile ranking.

ParameterInTypeRequiredConstraints
windowquery7d \14d \28d \
limitqueryintegerno> 0, ≤ 100
seriesquery1 \trueno
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
linea_idqueryanyno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/creatives \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "data": [
      {
        "ad_external_id": "120212000000000009",
        "ad_name": "Video testimonio 15s",
        "campaign_name": "Leads · Septiembre",
        "campaign": {
          "external_id": "120211000000000001",
          "name": "Leads · Septiembre"
        },
        "ad_set_name": "Santiago 25-45",
        "creative_thumbnail_url": null,
        "video_external_id": "120214000000000009",
        "ads_manager_url": "https://www.facebook.com/adsmanager/manage/ads?act=<account-id>&selected_ad_ids=120212000000000009",
        "score_window": "28d",
        "composite_score": 0.82,
        "top_performer_likelihood": 0.71,
        "maturity_stage": "mature",
        "truth_grade": "attributed",
        "primary_outcome_kind": "closed_won",
        "spend": 150000,
        "impressions": 48200,
        "ctr": 1.9,
        "attributed_revenue": 480000,
        "roas": 3.2,
        "fatigue_state": "active",
        "fatigue_risk_tier": "low",
        "series": [
          {
            "date": "2026-09-09",
            "spend": 6300,
            "ctr": 1.79
          },
          {
            "date": "2026-09-10",
            "spend": 7000,
            "ctr": 1.8
          },
          {
            "date": "2026-09-11",
            "spend": 5600,
            "ctr": 1.82
          },
          {
            "date": "2026-09-12",
            "spend": 4100,
            "ctr": 1.84
          },
          {
            "date": "2026-09-13",
            "spend": 3700,
            "ctr": 1.86
          },
          {
            "date": "2026-09-14",
            "spend": 6300,
            "ctr": 1.87
          },
          {
            "date": "2026-09-15",
            "spend": 7000,
            "ctr": 1.89
          },
          {
            "date": "2026-09-16",
            "spend": 5600,
            "ctr": 1.91
          },
          {
            "date": "2026-09-17",
            "spend": 6600,
            "ctr": 1.93
          },
          {
            "date": "2026-09-18",
            "spend": 6000,
            "ctr": 1.94
          },
          {
            "date": "2026-09-19",
            "spend": 3900,
            "ctr": 1.96
          },
          {
            "date": "2026-09-20",
            "spend": 4300,
            "ctr": 1.98
          },
          {
            "date": "2026-09-21",
            "spend": 5600,
            "ctr": 2
          },
          {
            "date": "2026-09-22",
            "spend": 3000,
            "ctr": 2.01
          }
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/experiments

The optimisation experiment: offer and history

Whether the «Probar optimizar por primer pago» experiment can be started now (offer: only while outcomes reach Meta and the dataset is healthy, one experiment at a time) and the workspace’s experiments, newest first. An experiment compares ad sets that optimise for different events — today’s «conversaciones» against ad sets optimising on a Vitrina event such as the first payment.

curl https://api.vitrinadev.com/api/v1/ads/experiments \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "offer": {
      "available": false,
      "reason": "already_open",
      "suggested_test_event": "appointment_booked",
      "weekly_first_payments": 12,
      "first_payment_threshold": 50
    },
    "experiments": [
      {
        "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
        "name": "Conversaciones vs primer pago",
        "status": "running",
        "started_at": "2026-10-02T13:00:00.000Z",
        "ended_at": null,
        "readout_from": null,
        "read_early": false,
        "created_at": "2026-10-01T21:10:00.000Z",
        "arms": [
          {
            "arm_key": "control",
            "role": "control",
            "optimization_event": "conversations",
            "meta_campaign": null,
            "meta_ad_sets": [
              "120211000000000201"
            ],
            "budget_share": 50,
            "learning_status": "active",
            "learning_checked_at": "2026-10-03T12:00:00.000Z",
            "left_learning_at": "2026-10-02T18:00:00.000Z"
          },
          {
            "arm_key": "first_payment",
            "role": "test",
            "optimization_event": "first_payment",
            "meta_campaign": "120211000000000300",
            "meta_ad_sets": [
              "120211000000000301"
            ],
            "budget_share": 50,
            "learning_status": "learning",
            "learning_checked_at": "2026-10-03T12:00:00.000Z",
            "left_learning_at": null
          }
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/experiments

Record an optimisation experiment (draft)

Records the control (today’s «conversaciones» ad sets) and the test arm (ad sets optimising on test_event; default, D81: the offer’s suggested_test_event — «cita agendada» until ~50 first payments a week, then the first payment). No Meta write: Meta’s API cannot re-optimise a published click-to-WhatsApp ad set on a purchase (the Sales objective belongs to the campaign and a published ad set’s optimisation is frozen), so the test arm is built in Ads Manager and its ids recorded here (now or with PATCH). 409 when not offered (details.reason).

Body

FieldTypeRequiredConstraints
namestring—mín. 1, máx. 120
test_eventfirst_payment \closed_won \appointment_attended \
control_ad_setsstring[]yes
test_ad_setsstring[]—
test_campaignstring | null—patrón ^[0-9]{1,32}$
test_budget_sharenumber | null—> 0
curl -X POST https://api.vitrinadev.com/api/v1/ads/experiments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "control_ad_sets": [
      "120211000000000201"
    ],
    "test_ad_sets": [
      "120211000000000301"
    ],
    "test_campaign": "120211000000000300",
    "test_event": "first_payment",
    "test_budget_share": 50
  }'

Example response (201)

{
  "data": {
    "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
    "name": "Conversaciones vs primer pago",
    "status": "draft",
    "started_at": null,
    "ended_at": null,
    "readout_from": null,
    "read_early": false,
    "created_at": "2026-10-01T21:10:00.000Z",
    "arms": [
      {
        "arm_key": "control",
        "role": "control",
        "optimization_event": "conversations",
        "meta_campaign": null,
        "meta_ad_sets": [
          "120211000000000201"
        ],
        "budget_share": 50,
        "learning_status": "active",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": "2026-10-02T18:00:00.000Z"
      },
      {
        "arm_key": "first_payment",
        "role": "test",
        "optimization_event": "first_payment",
        "meta_campaign": "120211000000000300",
        "meta_ad_sets": [
          "120211000000000301"
        ],
        "budget_share": 50,
        "learning_status": "learning",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": null
      }
    ]
  }
}

Answers: 201 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/experiments/{id}

One optimisation experiment

The experiment with its arms and each arm’s Meta learning stage, read from Meta every 6 hours (unknown until Meta answers).

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/experiments/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
    "name": "Conversaciones vs primer pago",
    "status": "running",
    "started_at": "2026-10-02T13:00:00.000Z",
    "ended_at": null,
    "readout_from": null,
    "read_early": false,
    "created_at": "2026-10-01T21:10:00.000Z",
    "arms": [
      {
        "arm_key": "control",
        "role": "control",
        "optimization_event": "conversations",
        "meta_campaign": null,
        "meta_ad_sets": [
          "120211000000000201"
        ],
        "budget_share": 50,
        "learning_status": "active",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": "2026-10-02T18:00:00.000Z"
      },
      {
        "arm_key": "first_payment",
        "role": "test",
        "optimization_event": "first_payment",
        "meta_campaign": "120211000000000300",
        "meta_ad_sets": [
          "120211000000000301"
        ],
        "budget_share": 50,
        "learning_status": "learning",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": null
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PATCH /ads/experiments/{id}

Record an arm’s ad sets, start, end or read early

arm records the ad sets (and campaign) an arm runs on, as built in Ads Manager — their learning read restarts. status: running once the test arm is live (reads learning right away); ended closes it and keeps the record. read_from compares from a day before every arm left learning (the readout then says read_early: true).

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
armobject—
statusrunning \ended—
read_fromstring | null—patrón ^\d{4}-\d{2}-\d{2}$
curl -X PATCH https://api.vitrinadev.com/api/v1/ads/experiments/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "arm": {
      "arm_key": "first_payment",
      "ad_sets": [
        "120211000000000301"
      ],
      "campaign": "120211000000000300"
    },
    "status": "running"
  }'

Example response (200)

{
  "data": {
    "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
    "name": "Conversaciones vs primer pago",
    "status": "running",
    "started_at": "2026-10-02T13:00:00.000Z",
    "ended_at": null,
    "readout_from": null,
    "read_early": false,
    "created_at": "2026-10-01T21:10:00.000Z",
    "arms": [
      {
        "arm_key": "control",
        "role": "control",
        "optimization_event": "conversations",
        "meta_campaign": null,
        "meta_ad_sets": [
          "120211000000000201"
        ],
        "budget_share": 50,
        "learning_status": "active",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": "2026-10-02T18:00:00.000Z"
      },
      {
        "arm_key": "first_payment",
        "role": "test",
        "optimization_event": "first_payment",
        "meta_campaign": "120211000000000300",
        "meta_ad_sets": [
          "120211000000000301"
        ],
        "budget_share": 50,
        "learning_status": "learning",
        "learning_checked_at": "2026-10-03T12:00:00.000Z",
        "left_learning_at": null
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/experiments/{id}/readout

The experiment readout, per arm

Per arm: spend, people whose WhatsApp conversation the arm’s ads opened, «pacientes nuevos que pagaron» (a first payment), accepted presupuestos, and the two costs — cost per paying patient and cost per accepted presupuesto — side by side with the days of data. No winner and no probability. Numbers appear from the day the last arm left Meta’s learning phase (phase: learning until then), or from the day the owner chose to read early (read_early: true).

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/experiments/<id>/readout \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "experiment": {
      "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
      "name": "Conversaciones vs primer pago",
      "status": "running",
      "started_at": "2026-10-02T13:00:00.000Z",
      "ended_at": null,
      "readout_from": null,
      "read_early": false,
      "created_at": "2026-10-01T21:10:00.000Z",
      "arms": [
        {
          "arm_key": "control",
          "role": "control",
          "optimization_event": "conversations",
          "meta_campaign": null,
          "meta_ad_sets": [
            "120211000000000201"
          ],
          "budget_share": 50,
          "learning_status": "active",
          "learning_checked_at": "2026-10-03T12:00:00.000Z",
          "left_learning_at": "2026-10-02T18:00:00.000Z"
        },
        {
          "arm_key": "first_payment",
          "role": "test",
          "optimization_event": "first_payment",
          "meta_campaign": "120211000000000300",
          "meta_ad_sets": [
            "120211000000000301"
          ],
          "budget_share": 50,
          "learning_status": "learning",
          "learning_checked_at": "2026-10-03T12:00:00.000Z",
          "left_learning_at": null
        }
      ]
    },
    "readout": {
      "phase": "comparing",
      "from": "2026-10-09",
      "to": "2026-10-30",
      "days_of_data": 22,
      "day_number": 29,
      "planned_days": 28,
      "learning_finished": true,
      "read_early": false,
      "planned_end_from": "2026-10-30",
      "planned_end_to": "2026-11-13",
      "arms": [
        {
          "arm_key": "control",
          "role": "control",
          "optimization_event": "conversations",
          "ad_set_count": 1,
          "learning_status": "active",
          "left_learning_at": "2026-10-02T18:00:00.000Z",
          "spend_clp": 650000,
          "people": 210,
          "paying_patients": 6,
          "accepted_quotes": 9,
          "attended": 14,
          "cost_per_paying_patient": 108333,
          "cost_per_accepted_quote": 72222,
          "sales_objective": false
        },
        {
          "arm_key": "first_payment",
          "role": "test",
          "optimization_event": "first_payment",
          "ad_set_count": 1,
          "learning_status": "learning_limited",
          "left_learning_at": "2026-10-09T12:00:00.000Z",
          "spend_clp": 640000,
          "people": 150,
          "paying_patients": 8,
          "accepted_quotes": 10,
          "attended": 13,
          "cost_per_paying_patient": 80000,
          "cost_per_accepted_quote": 64000,
          "sales_objective": true
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/experiments/current

The experiment in flight, with its status

The workspace’s open experiment (draft or running) and its readout in one read — «Prueba en curso: día {day_number} de {planned_days}», spend and cost per paying patient on each side. experiment: null and readout: null when none is open (then the recommendations offer «Duplicar con el evento nuevo»).

curl https://api.vitrinadev.com/api/v1/ads/experiments/current \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "experiment": null,
    "readout": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/feed

The live feed of recorded outcomes

Every outcome Vitrina recorded for the workspace (a lead, a booking, an attended visit, a presented quote, a closed sale, a payment), newest first — Vitrina’s own ledger, so a row appears the moment it happens, not after the engine’s daily sync. A row names the campaign the attribution engine credited (ad, from a cache refreshed every 5 minutes over the requested window, up to 92 days back; null until then — meta.enrichment says whether it is complete — with matched: true that reads «anuncio por confirmar»).

contact.display_name is filled only for a caller that also holds contacts:read; everyone else gets null and contact.key (the last 4 hex of the contact id).

Polling: call with since=&lt;newest created_at seen&gt; (verbatim); the answer re-includes the 5 seconds before it, so dedupe by id. Paging: before=&lt;meta.next_before&gt; (verbatim; when a polling page comes back full, page down with before + the same since to close the gap). only_ads=1 / campaign_id keep the rows the engine credited (to that campaign). With sample=1 answers the sample dataset’s feed.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
stagequerylead_created \web_form \lead_qualified \
only_adsquery1 \trueno
campaign_idquerystringnomín. 1, máx. 64
sincequerystringnopatrón ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$
beforequerystringnomáx. 80
limitqueryintegerno≥ 1, ≤ 50
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/feed \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
      "stage": "closed_won",
      "occurred_at": "2026-09-22T19:40:12.000Z",
      "created_at": "2026-09-22T19:40:13.482913Z",
      "matched": true,
      "value": 1200000,
      "action_source": "business_messaging",
      "contact": {
        "id": "5b0c2f1e-8a3d-4c6b-9e7f-1a2b3c4d5e6f",
        "display_name": "Paciente Ejemplo",
        "key": "5e6f"
      },
      "ad": {
        "name": null,
        "ad_set_name": null,
        "creative_thumbnail_url": null,
        "campaign": {
          "external_id": "120211000000000001",
          "name": "Ortodoncia invisible · Septiembre"
        }
      },
      "credited_weight": 1,
      "touch_count": 3,
      "origin": {
        "platform": "whatsapp",
        "external_id": "120211000000000101",
        "name": "Reel · Antes y después",
        "ad_name": "Reel · Antes y después",
        "headline": "Evaluación sin costo",
        "campaign": {
          "external_id": "120211000000000001",
          "name": "Ortodoncia invisible · Septiembre"
        },
        "thumbnail_url": null,
        "media_archived": false,
        "first_message_at": "2026-09-20T13:02:30.000Z",
        "matches_credit": true
      },
      "origin_source": "engine",
      "attribution": "credited"
    },
    {
      "id": "01926f3a-1b2c-7d3e-8f40-5a6b7c8d9e0f",
      "stage": "appointment_booked",
      "occurred_at": "2026-09-22T18:02:55.000Z",
      "created_at": "2026-09-22T18:02:56.104220Z",
      "matched": true,
      "value": null,
      "action_source": "business_messaging",
      "contact": {
        "id": "7c1d3e2f-9b4a-4d5c-8e6f-2b3c4d5e6f70",
        "display_name": null,
        "key": "6f70"
      },
      "ad": null,
      "credited_weight": null,
      "touch_count": null,
      "origin": {
        "platform": "instagram",
        "external_id": "120211000000000205",
        "name": "Limpieza facial",
        "ad_name": null,
        "headline": "Limpieza facial",
        "campaign": null,
        "thumbnail_url": null,
        "media_archived": false,
        "first_message_at": "2026-09-22T17:40:05.000Z",
        "matches_credit": null
      },
      "origin_source": "conversation",
      "attribution": "pending"
    }
  ],
  "meta": {
    "next_before": "2026-09-22T18:02:56.104220Z,01926f3a-1b2c-7d3e-8f40-5a6b7c8d9e0f",
    "server_time": "2026-09-22T19:41:00.000Z",
    "enrichment": "ready"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/feed/{id}/journey

One feed row’s journey

The journey behind one recorded outcome, oldest first: the attribution engine’s touches (the ad — its name, ad set, campaign and thumbnail — the click that landed, site visits) interleaved with the steps Vitrina recorded itself (first message, booking, attended visit, presented quote, acceptance, payments). The credited touch is credited: true with its join_method; the row itself is current: true. Labels are ready to render in the workspace’s vocabulary, and stage carries the vocabulary key.

scope=conversion (default) ends at this outcome; scope=person is the whole person — every touch and every recorded step, across repeat visits and devices — plus person.attended_count («vino n veces») and person.conversion_count.

Never a 5xx for the engine: when its touches cannot be shown the journey is Vitrina’s own steps (source: vitrina) and reason says why (unmatched / pending / unavailable / scope). Never carries a URL, path, referrer, device, location, email or the engine’s own person ids. 404 when the id is not a live row of this workspace. With sample=1 answers the sample dataset’s journey (pass the feed’s from/to when it was not the last 30 days).

ParameterInTypeRequiredConstraints
idpathuuidyes
scopequeryconversion \personno
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/feed/<id>/journey \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "steps": [
      {
        "stage": null,
        "detail": "Ortodoncia invisible · Septiembre",
        "credited": false,
        "join_method": null,
        "click_id_kind": null,
        "ad": {
          "external_id": "120211000000000101",
          "name": "Reel · Antes y después",
          "ad_set_name": "Mujeres 30-45 · Providencia",
          "campaign": {
            "external_id": "120211000000000100",
            "name": "Ortodoncia invisible · Septiembre"
          },
          "creative_thumbnail_url": null
        },
        "value": null,
        "current": false,
        "id": "b2f0c1d4-1a2b-4c3d-8e4f-5a6b7c8d9e01",
        "kind": "ad",
        "origin": "engine",
        "label": "Reel · Antes y después",
        "at": "2026-09-19T21:10:00.000Z",
        "channel": "meta"
      },
      {
        "stage": null,
        "detail": "Reel · Antes y después",
        "credited": true,
        "join_method": "click_id",
        "click_id_kind": "fbclid",
        "ad": {
          "external_id": "120211000000000101",
          "name": "Reel · Antes y después",
          "ad_set_name": "Mujeres 30-45 · Providencia",
          "campaign": {
            "external_id": "120211000000000100",
            "name": "Ortodoncia invisible · Septiembre"
          },
          "creative_thumbnail_url": null
        },
        "value": null,
        "current": false,
        "id": "b2f0c1d4-1a2b-4c3d-8e4f-5a6b7c8d9e02",
        "kind": "click",
        "origin": "engine",
        "label": "Visita desde el anuncio",
        "at": "2026-09-20T13:02:00.000Z",
        "channel": "meta"
      },
      {
        "stage": null,
        "detail": "Ortodoncia invisible · Septiembre",
        "credited": false,
        "join_method": "ctwa_referral",
        "click_id_kind": "ctwa_clid",
        "ad": {
          "external_id": "120211000000000101",
          "name": "Reel · Antes y después",
          "ad_set_name": "Mujeres 30-45 · Providencia",
          "campaign": {
            "external_id": "120211000000000100",
            "name": "Ortodoncia invisible · Septiembre"
          },
          "creative_thumbnail_url": null
        },
        "value": null,
        "current": false,
        "id": "7d7d7d7d-1111-4222-8333-444455556666",
        "kind": "chat",
        "origin": "vitrina",
        "label": "Mensaje por el anuncio «Reel · Antes y después»",
        "at": "2026-09-20T13:02:30.000Z",
        "channel": "whatsapp"
      },
      {
        "stage": "lead_created",
        "detail": "Por WhatsApp",
        "credited": false,
        "join_method": null,
        "click_id_kind": null,
        "ad": null,
        "value": null,
        "current": false,
        "id": "01926f3e-1111-7b2e-9f10-3a5b7c9d0e10",
        "kind": "chat",
        "origin": "vitrina",
        "label": "Contacto nuevo",
        "at": "2026-09-20T13:03:00.000Z",
        "channel": "whatsapp"
      },
      {
        "stage": "appointment_attended",
        "detail": null,
        "credited": false,
        "join_method": null,
        "click_id_kind": null,
        "ad": null,
        "value": null,
        "current": false,
        "id": "01926f3e-2222-7b2e-9f10-3a5b7c9d0e11",
        "kind": "attended",
        "origin": "vitrina",
        "label": "Cita atendida",
        "at": "2026-09-21T14:00:00.000Z"
      },
      {
        "stage": "quote_presented",
        "detail": null,
        "credited": false,
        "join_method": null,
        "click_id_kind": null,
        "ad": null,
        "value": 1200000,
        "current": false,
        "id": "01926f3e-3333-7b2e-9f10-3a5b7c9d0e12",
        "kind": "quote",
        "origin": "vitrina",
        "label": "Presupuesto presentado",
        "at": "2026-09-21T15:20:00.000Z"
      },
      {
        "stage": "closed_won",
        "detail": null,
        "credited": false,
        "join_method": null,
        "click_id_kind": null,
        "ad": null,
        "value": 1200000,
        "current": true,
        "id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
        "kind": "accepted",
        "origin": "vitrina",
        "label": "Presupuesto aceptado",
        "at": "2026-09-22T19:40:12.000Z"
      }
    ],
    "touch_count": 2,
    "credited_weight": 1,
    "model": "last_touch",
    "scope": "conversion",
    "source": "mixed",
    "reason": null,
    "person": null,
    "truncated": false
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/funnel

The period’s people funnel (a cohort, not event counts)

The people whose FIRST contact falls in the window, and how many of them reached each step: booked → attended → quote presented → accepted → paid. Each step is a subset of the one before (conversion = the share that moved on). Split «desde anuncios» (ads) vs every other origin (other) by the same rule as the origin buckets. People who were already clients before the window are outside the funnel: earlier carries their accepted plans and payments in it. Counts only — no person is identified.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/funnel \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-09-01",
      "to": "2026-09-30"
    },
    "steps": [
      {
        "key": "contacted",
        "ads": 215,
        "other": 76,
        "total": 291,
        "conversion": null
      },
      {
        "key": "booked",
        "ads": 24,
        "other": 19,
        "total": 43,
        "conversion": 0.148
      },
      {
        "key": "attended",
        "ads": 15,
        "other": 14,
        "total": 29,
        "conversion": 0.674
      },
      {
        "key": "quote_presented",
        "ads": 11,
        "other": 10,
        "total": 21,
        "conversion": 0.724
      },
      {
        "key": "accepted",
        "ads": 5,
        "other": 6,
        "total": 11,
        "conversion": 0.524
      },
      {
        "key": "paid",
        "ads": 3,
        "other": 5,
        "total": 8,
        "conversion": 0.727
      }
    ],
    "earlier": {
      "people": 30,
      "accepted_count": 30,
      "accepted_value": 9412000,
      "paid_value": 10625399
    },
    "credit_known": true
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

DELETE /ads/goal

Clear the monthly Ads goal

Removes the goal. Idempotent. With sample=1 nothing is stored or removed.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl -X DELETE https://api.vitrinadev.com/api/v1/ads/goal \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 204 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/goal

The workspace’s monthly Ads goal

The goal the Ads hero measures pace against — results from ads, attributed value or return for the calendar month — or null when none is set. With sample=1 answers the sample dataset’s goal.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/goal \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "kind": "outcomes",
    "target": 30,
    "period": "month",
    "updated_at": "2026-09-01T12:00:00.000Z",
    "updated_by": "20000000-0000-4000-8000-000000000001"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/goal

Set the monthly Ads goal

Replaces the goal. target > 0; a roas goal is at most 100, a value goal at most 10^10 CLP, an outcomes goal a whole number. The server stamps updated_at/updated_by. With sample=1 the goal is validated and echoed with persisted: false, never stored.

ParameterInTypeRequiredConstraints
samplequery1 \trueno

Body

FieldTypeRequiredConstraints
kindoutcomes \value \roas
targetnumberyes> 0
periodmonthyes
curl -X PUT https://api.vitrinadev.com/api/v1/ads/goal \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "outcomes",
    "target": 30,
    "period": "month"
  }'

Example response (200)

{
  "data": {
    "kind": "outcomes",
    "target": 30,
    "period": "month",
    "updated_at": "2026-09-23T15:00:00.000Z",
    "updated_by": "20000000-0000-4000-8000-000000000001",
    "persisted": true
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/health

Attribution trust, UTM quality and attribution coverage

One combined read over three attribution-engine reads, so a dashboard panel needs one call, not three:

  • trust — how much of the profile’s cash is traceable to an ad (traceable_pct_by_value/_by_count), whether the instrumentation producing that number is sound (instrumentation.tracking_healthy), and which way the traceable half is moving (traceable_roas_trend). READ instrumentation BEFORE rendering traceable_pct_by_value as an indictment — a low number reads as "great organic" when tracking is healthy and "broken tracking" when it is not.
  • utm — the cached UTM-health badge (malformed/missing/dangling-ad-id anomaly counts over the traffic side) plus the dangling-ad-ids rollup (attributed cash that cannot be placed in the ad hierarchy).
  • attribution_coverage — the conversion side — how many of the window’s conversions carry a real ad id, an fbclid only, or nothing, partitioning total_conversions.
ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
daysqueryintegerno≥ 1, ≤ 30
window_daysqueryintegerno≥ 2, ≤ 366
curl https://api.vitrinadev.com/api/v1/ads/health \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "trust": {
      "model": "last_touch",
      "currency": "CLP",
      "traceable_pct_by_count": 41.5,
      "traceable_pct_by_value": 38.2,
      "total_count": 53,
      "total_value": 5000000,
      "instrumentation": {
        "score": 0.86,
        "verdict": "healthy_tracking",
        "tracking_healthy": true,
        "coverage_interpretation": "La mayoría de las ventas llegan con su origen publicitario.",
        "missing": [],
        "tracker": {
          "status": "live",
          "web_touches": 1842,
          "last_web_touch": "2026-08-31T21:14:00Z",
          "fresh": true
        }
      },
      "traceable_roas_trend": {
        "recent_roas": 3.4,
        "prior_roas": 2.9,
        "recent_spend": 845200,
        "recent_traceable_revenue": 2873680,
        "days_with_cash": 11
      }
    },
    "utm": {
      "scan_window_days": 14,
      "events_scanned": 312,
      "high_count": 3,
      "medium_count": 7,
      "refreshed_at": "2026-09-22T06:00:00.000Z",
      "dangling_ad_ids": {
        "window_days": 28,
        "model": "last_touch",
        "dangling_conversions": 2,
        "attributed_cash": 90000,
        "reporting_currency": "CLP",
        "distinct_ad_ids": 1
      }
    },
    "attribution_coverage": {
      "total_conversions": 44,
      "with_full_utms": 27,
      "with_fbclid_only": 5,
      "with_no_tracking": 12,
      "coverage_percent": 72.7
    },
    "outcomes_coverage": {
      "outcomes": 42,
      "attributed_outcomes": 31,
      "coverage_percent": 73.8
    },
    "signals": {
      "score": 72,
      "components": [
        {
          "key": "meta",
          "status": "ok",
          "earned": 30,
          "max": 30
        },
        {
          "key": "origin",
          "status": "partial",
          "earned": 29.5,
          "max": 40
        },
        {
          "key": "site_tag",
          "status": "partial",
          "earned": 12,
          "max": 30
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamiento-proposals

List fatigue proposals

Fresh creative variants Vitrina rendered for tired Lanzamiento ads: cost per qualified seller at 2x its own baseline, or a 7-day frequency of 4 or more. Never applied by itself: the dealer accepts or dismisses. Status rendering → ready → accepting → accepted; dismissed; failed (see error). Newest first.

ParameterInTypeRequiredConstraints
statusqueryrendering \ready \accepting \
lanzamiento_idqueryuuidno
limitqueryintegerno≥ 1, ≤ 100
curl https://api.vitrinadev.com/api/v1/ads/lanzamiento-proposals \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
      "lanzamiento_id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
      "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
      "ad_object_id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
      "meta_ad_id": "120211111111111111",
      "status": "ready",
      "reason": {
        "signals": [
          "cost"
        ],
        "cost_per_qualified_7d": 18000,
        "baseline_cost_per_qualified": 8000,
        "spend_7d": 72000,
        "qualified_7d": 4,
        "frequency_7d": 2.6
      },
      "angles": [
        "fotos",
        "opciones"
      ],
      "creative_set_id": "6b5a4f3e-2d1c-4b0a-9f8e-7d6c5b4a3f2e",
      "variants": [
        {
          "feed_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
          "vertical_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
          "angle": "fotos",
          "variant": "default",
          "feed_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1350.png",
          "vertical_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1920.png"
        }
      ],
      "new_ad_object_ids": [],
      "tired_ad_paused": null,
      "error": null,
      "decided_at": null,
      "created_at": "2026-11-10T12:00:00.000Z",
      "updated_at": "2026-11-10T12:01:00.000Z"
    }
  ]
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamiento-proposals/{id}

Get a fatigue proposal

One fatigue proposal with its fresh variants.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/lanzamiento-proposals/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "lanzamiento_id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
    "ad_object_id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
    "meta_ad_id": "120211111111111111",
    "status": "ready",
    "reason": {
      "signals": [
        "cost"
      ],
      "cost_per_qualified_7d": 18000,
      "baseline_cost_per_qualified": 8000,
      "spend_7d": 72000,
      "qualified_7d": 4,
      "frequency_7d": 2.6
    },
    "angles": [
      "fotos",
      "opciones"
    ],
    "creative_set_id": "6b5a4f3e-2d1c-4b0a-9f8e-7d6c5b4a3f2e",
    "variants": [
      {
        "feed_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "vertical_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "angle": "fotos",
        "variant": "default",
        "feed_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1350.png",
        "vertical_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1920.png"
      }
    ],
    "new_ad_object_ids": [],
    "tired_ad_paused": null,
    "error": null,
    "decided_at": null,
    "created_at": "2026-11-10T12:00:00.000Z",
    "updated_at": "2026-11-10T12:01:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamiento-proposals/{id}/accept

Accept a fatigue proposal

One click: in the background the fresh variants become new ads in the tired ad's ad set, they are switched on, and only then the tired ad is paused. 409 proposal_not_ready unless ready; 409 lanzamiento_not_live; 409 edited_outside_vitrina / deleted_on_meta when the ad was changed outside Vitrina. Poll the proposal until accepted or failed.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamiento-proposals/<id>/accept \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (202)

{
  "data": {
    "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "lanzamiento_id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
    "ad_object_id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
    "meta_ad_id": "120211111111111111",
    "status": "accepting",
    "reason": {
      "signals": [
        "cost"
      ],
      "cost_per_qualified_7d": 18000,
      "baseline_cost_per_qualified": 8000,
      "spend_7d": 72000,
      "qualified_7d": 4,
      "frequency_7d": 2.6
    },
    "angles": [
      "fotos",
      "opciones"
    ],
    "creative_set_id": "6b5a4f3e-2d1c-4b0a-9f8e-7d6c5b4a3f2e",
    "variants": [
      {
        "feed_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "vertical_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "angle": "fotos",
        "variant": "default",
        "feed_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1350.png",
        "vertical_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1920.png"
      }
    ],
    "new_ad_object_ids": [],
    "tired_ad_paused": null,
    "error": null,
    "decided_at": "2026-11-10T13:00:00.000Z",
    "created_at": "2026-11-10T12:00:00.000Z",
    "updated_at": "2026-11-10T12:01:00.000Z"
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamiento-proposals/{id}/dismiss

Dismiss a fatigue proposal

Leaves the ad as it is. Vitrina does not propose again for that ad for 14 days. 409 proposal_decided once accepted or already dismissed.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamiento-proposals/<id>/dismiss \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "lanzamiento_id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
    "ad_object_id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
    "meta_ad_id": "120211111111111111",
    "status": "dismissed",
    "reason": {
      "signals": [
        "cost"
      ],
      "cost_per_qualified_7d": 18000,
      "baseline_cost_per_qualified": 8000,
      "spend_7d": 72000,
      "qualified_7d": 4,
      "frequency_7d": 2.6
    },
    "angles": [
      "fotos",
      "opciones"
    ],
    "creative_set_id": "6b5a4f3e-2d1c-4b0a-9f8e-7d6c5b4a3f2e",
    "variants": [
      {
        "feed_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "vertical_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "angle": "fotos",
        "variant": "default",
        "feed_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1350.png",
        "vertical_url": "https://media.vitrina.cl/t/other/lanzamiento-set-fotos_1080x1920.png"
      }
    ],
    "new_ad_object_ids": [],
    "tired_ad_paused": null,
    "error": null,
    "decided_at": "2026-11-10T13:00:00.000Z",
    "created_at": "2026-11-10T12:00:00.000Z",
    "updated_at": "2026-11-10T12:01:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos

Lanzamientos

The Lanzamientos of the workspace, newest first: each is the set of Meta campaigns Vitrina created on the ad account from one Plantilla, with its lanes and status. The first Lanzamiento of a dealer waits for a Vitrina review before it can go live.

ParameterInTypeRequiredConstraints
statusquerydraft \building \paused_ready \
plantillaquerycaptacion_consignaciones \venta_autono
limitqueryintegerno≥ 1, ≤ 200
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
      "plantilla": "captacion_consignaciones",
      "status": "paused_ready",
      "requires_review": true,
      "reviewed_at": null,
      "budget_package": "recomendado",
      "daily_budget_clp": 60000,
      "geo": {
        "region_codes": [
          "13"
        ],
        "location_types": [
          "home"
        ]
      },
      "status_changed_at": "2026-10-26T15:10:00.000Z",
      "launched_at": null,
      "vehicle_id": null,
      "stock_paused_at": null,
      "stock_pause_reason": null,
      "created_at": "2026-10-26T15:00:00.000Z",
      "lanes": [
        {
          "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
          "lane": "landing",
          "daily_budget_clp": 30000,
          "destination_url": "https://upautos.cl/vende-tu-auto",
          "messaging_account_id": null,
          "optimization_goal": "lead",
          "ladder": null
        },
        {
          "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f602",
          "lane": "whatsapp",
          "daily_budget_clp": 30000,
          "destination_url": null,
          "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
          "optimization_goal": "conversation",
          "ladder": null
        }
      ]
    }
  ]
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos

Create a Lanzamiento

Creates the Lanzamiento from the creatives the dealer picked (by id: 4:5 + 9:16 static pairs, optional videos) and starts building it on Meta in the background: per lane one ABO testing campaign, a statics ad set (plus a video ad set when a video was picked), one ad per angle (both headline modes as separate ads when the Oferta runs both) and an empty paused CBO Winners campaign. EVERYTHING IS CREATED PAUSED. Answers 202 with status building; poll GET /ads/lanzamientos/{id} until paused_ready or failed (a failed build is rolled back and carries a failure). 400 with problems when it cannot be built.

Body

FieldTypeRequiredConstraints
budgetobjectyes
lanesobject[]yes
creative_mixmixed \separate—
region_codesstring[]—
page_idstring—máx. 40
dataset_idstring—máx. 40
creativesobjectyes
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "budget": {
      "package": "recomendado"
    },
    "lanes": [
      {
        "lane": "landing"
      },
      {
        "lane": "whatsapp",
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5"
      }
    ],
    "creatives": {
      "statics": [
        {
          "feed_id": "3f0c9a1e-1b2c-4d3e-8f40-5a6b7c8d9e01",
          "vertical_id": "3f0c9a1e-1b2c-4d3e-8f40-5a6b7c8d9e02"
        }
      ],
      "videos": []
    }
  }'

Example response (202)

{
  "data": {
    "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "plantilla": "captacion_consignaciones",
    "status": "building",
    "requires_review": true,
    "reviewed_at": null,
    "budget_package": "recomendado",
    "daily_budget_clp": 60000,
    "geo": {
      "region_codes": [
        "13"
      ],
      "location_types": [
        "home"
      ],
      "meta_region_keys": [
        "673"
      ],
      "label": "RM"
    },
    "status_changed_at": "2026-10-26T15:10:00.000Z",
    "launched_at": null,
    "vehicle_id": null,
    "stock_paused_at": null,
    "stock_pause_reason": null,
    "created_at": "2026-10-26T15:10:00.000Z",
    "lanes": [
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
        "lane": "landing",
        "daily_budget_clp": 30000,
        "destination_url": "https://www.tuautomotora.cl/vende-tu-auto",
        "messaging_account_id": null,
        "optimization_goal": "lead",
        "ladder": null
      },
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f602",
        "lane": "whatsapp",
        "daily_budget_clp": 30000,
        "destination_url": null,
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
        "optimization_goal": "conversation",
        "ladder": null
      }
    ],
    "offer_snapshot": null,
    "failure": null,
    "meta_objects": [],
    "activation": null
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/{id}

One Lanzamiento

One Lanzamiento with its lanes and every Meta object Vitrina created for it. An object changed outside Vitrina carries outside_edit_at and is skipped by every automatic step. activation follows a live Lanzamiento switching on (null | pending | done | failed).

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "plantilla": "captacion_consignaciones",
    "status": "paused_ready",
    "requires_review": true,
    "reviewed_at": null,
    "budget_package": "recomendado",
    "daily_budget_clp": 60000,
    "geo": {
      "region_codes": [
        "13"
      ],
      "location_types": [
        "home"
      ]
    },
    "status_changed_at": "2026-10-26T15:10:00.000Z",
    "launched_at": null,
    "vehicle_id": null,
    "stock_paused_at": null,
    "stock_pause_reason": null,
    "created_at": "2026-10-26T15:00:00.000Z",
    "lanes": [
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
        "lane": "landing",
        "daily_budget_clp": 30000,
        "destination_url": "https://upautos.cl/vende-tu-auto",
        "messaging_account_id": null,
        "optimization_goal": "lead",
        "ladder": null
      },
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f602",
        "lane": "whatsapp",
        "daily_budget_clp": 30000,
        "destination_url": null,
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
        "optimization_goal": "conversation",
        "ladder": null
      }
    ],
    "offer_snapshot": {
      "commission_pct": 5,
      "commission_plus_iva": false,
      "headlines": [
        "commission",
        "precio_minimo"
      ],
      "headline_mode": "commission",
      "solo_pagas_si_se_vende": true,
      "precio_minimo": true,
      "compra_inmediata": true,
      "test_drives_at_lot": true,
      "transfer_help": true,
      "photography": true,
      "contract_length_days": 60,
      "cars_sold_per_month": null
    },
    "failure": null,
    "meta_objects": [],
    "activation": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/{id}/confirm

Confirm a built Lanzamiento

The dealer confirms a paused_ready Lanzamiento. The dealer's first Lanzamiento goes to in_review and waits for Vitrina; otherwise it goes live and its ads, ad sets and campaigns are switched on in the background (activation). The CBO Winners campaign stays paused. 409 from any other status.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/<id>/confirm \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "plantilla": "captacion_consignaciones",
    "status": "in_review",
    "requires_review": true,
    "reviewed_at": null,
    "budget_package": "recomendado",
    "daily_budget_clp": 60000,
    "geo": {
      "region_codes": [
        "13"
      ],
      "location_types": [
        "home"
      ],
      "meta_region_keys": [
        "673"
      ],
      "label": "RM"
    },
    "status_changed_at": "2026-10-26T15:10:00.000Z",
    "launched_at": null,
    "vehicle_id": null,
    "stock_paused_at": null,
    "stock_pause_reason": null,
    "created_at": "2026-10-26T15:10:00.000Z",
    "lanes": [
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
        "lane": "landing",
        "daily_budget_clp": 30000,
        "destination_url": "https://www.tuautomotora.cl/vende-tu-auto",
        "messaging_account_id": null,
        "optimization_goal": "lead",
        "ladder": null
      },
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f602",
        "lane": "whatsapp",
        "daily_budget_clp": 30000,
        "destination_url": null,
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
        "optimization_goal": "conversation",
        "ladder": null
      }
    ],
    "offer_snapshot": null,
    "failure": null,
    "meta_objects": [],
    "activation": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/{id}/metrics

Consignment metrics of one Lanzamiento

The consignment funnel per Lanzamiento, per lane and per ad: spend → contacts (landing form + click-to-WhatsApp chat) → qualified sellers (the SellerQualified / AddToCart fact; a lead outside the Condiciones never counts) → inspection booked (SellerVisitScheduled) → Consignación captada (consignment or compra) → sold (the captured car sold from stock). Each step counts what happened in the window. Headline: cost_per_captada, with cost_per_qualified_seller beside it. Spend comes from the attribution engine; unknown spend is null (never 0) and so is every cost built on it, as is a cost over zero. Plus stock target progress: nothing pauses, Vitrina only suggests lowering the budget once reached.

ParameterInTypeRequiredConstraints
idpathuuidyes
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/<id>/metrics \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "period": {
      "from": "2026-10-01",
      "to": "2026-10-26",
      "timezone": "America/Santiago"
    },
    "spend_status": "ok",
    "totals": {
      "spend": 412000,
      "contacts": 58,
      "contacts_form": 21,
      "contacts_chat": 37,
      "qualified_sellers": 19,
      "outside_criteria": 9,
      "inspections_booked": 11,
      "captadas": 4,
      "captadas_consignacion": 3,
      "captadas_compra": 1,
      "sold": 1,
      "cost_per_captada": 103000,
      "cost_per_qualified_seller": 21684,
      "cost_per_contact": 7103
    },
    "lanzamientos": [
      {
        "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
        "plantilla": "captacion_consignaciones",
        "status": "live",
        "launched_at": "2026-10-02T13:00:00.000Z",
        "spend_status": "ok",
        "funnel": {
          "spend": 412000,
          "contacts": 58,
          "contacts_form": 21,
          "contacts_chat": 37,
          "qualified_sellers": 19,
          "outside_criteria": 9,
          "inspections_booked": 11,
          "captadas": 4,
          "captadas_consignacion": 3,
          "captadas_compra": 1,
          "sold": 1,
          "cost_per_captada": 103000,
          "cost_per_qualified_seller": 21684,
          "cost_per_contact": 7103
        },
        "lanes": [
          {
            "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
            "lane": "landing",
            "funnel": {
              "spend": 206000,
              "contacts": 21,
              "contacts_form": 21,
              "contacts_chat": 0,
              "qualified_sellers": 8,
              "outside_criteria": 3,
              "inspections_booked": 5,
              "captadas": 2,
              "captadas_consignacion": 2,
              "captadas_compra": 0,
              "sold": 0,
              "cost_per_captada": 103000,
              "cost_per_qualified_seller": 25750,
              "cost_per_contact": 9810
            }
          }
        ],
        "ads": [
          {
            "ad_object_id": "9d0e1f2a-3b4c-4d5e-8f60-718293a4b5c6",
            "meta_ad_id": "120211234567890123",
            "lane_id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
            "lane": "landing",
            "angle": "comision",
            "format": "4:5",
            "headline_mode": "commission",
            "media_kind": "static",
            "outside_vitrina": false,
            "funnel": {
              "spend": 0,
              "contacts": 0,
              "contacts_form": 0,
              "contacts_chat": 0,
              "qualified_sellers": 0,
              "outside_criteria": 0,
              "inspections_booked": 0,
              "captadas": 0,
              "captadas_consignacion": 0,
              "captadas_compra": 0,
              "sold": 0,
              "cost_per_captada": null,
              "cost_per_qualified_seller": null,
              "cost_per_contact": null
            }
          }
        ]
      }
    ],
    "stock_target": {
      "in_stock": 31,
      "captured_this_month": 4,
      "target": 35,
      "reached": false,
      "progress": 0.886,
      "suggest_lower_budget": false
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/{id}/rollback

Roll back a Lanzamiento

Deletes, in the background, every Meta object the Lanzamiento created (ads first, campaigns last) and moves it to rolled_back. Only before it is live (draft, paused_ready, in_review, failed); 409 while building or once live. An object someone switched on in Ads Manager since is kept, never forced.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/<id>/rollback \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (202)

{
  "data": {
    "id": "0b6f4c1a-6a9e-4f0e-8a51-3f1d2c7e9a10",
    "plantilla": "captacion_consignaciones",
    "status": "paused_ready",
    "requires_review": true,
    "reviewed_at": null,
    "budget_package": "recomendado",
    "daily_budget_clp": 60000,
    "geo": {
      "region_codes": [
        "13"
      ],
      "location_types": [
        "home"
      ],
      "meta_region_keys": [
        "673"
      ],
      "label": "RM"
    },
    "status_changed_at": "2026-10-26T15:10:00.000Z",
    "launched_at": null,
    "vehicle_id": null,
    "stock_paused_at": null,
    "stock_pause_reason": null,
    "created_at": "2026-10-26T15:10:00.000Z",
    "lanes": [
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f601",
        "lane": "landing",
        "daily_budget_clp": 30000,
        "destination_url": "https://www.tuautomotora.cl/vende-tu-auto",
        "messaging_account_id": null,
        "optimization_goal": "lead",
        "ladder": null
      },
      {
        "id": "5a1e2b3c-4d5e-4f60-8a71-b2c3d4e5f602",
        "lane": "whatsapp",
        "daily_budget_clp": 30000,
        "destination_url": null,
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
        "optimization_goal": "conversation",
        "ladder": null
      }
    ],
    "offer_snapshot": null,
    "failure": null,
    "meta_objects": [],
    "activation": null
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/car-creative-sets

Per-car creative sets

The latest per-car render sets, newest first, optionally for one vehicle, each with its PNGs.

ParameterInTypeRequiredConstraints
vehicle_idqueryuuidno
limitqueryintegerno≥ 1, ≤ 50, por defecto 20
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/car-creative-sets \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "sets": [
      {
        "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f12",
        "plantilla": "venta_auto",
        "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
        "status": "done",
        "error": null,
        "created_at": "2026-10-27T15:00:00.000Z",
        "completed_at": "2026-10-27T15:00:14.000Z",
        "vehicle": {
          "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
          "facts_version": "3f9a0c1d2e4b5a6c",
          "price_clp": 18990000,
          "make": "Kia",
          "model": "Sportage",
          "version": "EX 2.0 4x2 AT",
          "year": 2023,
          "odometer_value": 42000,
          "odometer_unit": "KM",
          "transmission": "automatica",
          "fuel": "gasolina",
          "offers_financing": false,
          "hero_photo": "https://media.example.com/vehicles/<tenant>/photo-1.jpg",
          "carousel_photos": [
            "https://media.example.com/vehicles/<tenant>/photo-1.jpg"
          ],
          "captured_at": "2026-10-27T15:00:00.000Z"
        },
        "plan": {
          "claims": [
            "price",
            "odometer",
            "transmission",
            "fuel",
            "google_rating"
          ],
          "shots": [
            {
              "key": "car_1080x1350",
              "variant": "hero",
              "format": "4x5",
              "card_index": null
            },
            {
              "key": "car_1080x1920",
              "variant": "hero",
              "format": "9x16",
              "card_index": null
            },
            {
              "key": "car-card-01_1080x1350",
              "variant": "carousel",
              "format": "4x5",
              "card_index": 1
            }
          ]
        },
        "creatives": [
          {
            "key": "car_1080x1350",
            "variant": "hero",
            "format": "4x5",
            "card_index": null,
            "width": 1080,
            "height": 1350,
            "url": "https://media.example.com/<tenant>/other/lanzamiento-<set>-car_1080x1350.png",
            "claims": [
              "price",
              "odometer",
              "transmission",
              "fuel",
              "google_rating"
            ]
          }
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/car-creative-sets

Render the creatives of one stock car

Queues a render of one car’s ads for the «Venta de un auto» Plantilla: a 4:5 (1080×1350) and a 9:16 (1080×1920) static, plus a carousel of up to 10 4:5 cards, one per photo, in order. Everything printed comes from the vehicle (make, model, year, version, price, km, transmission, fuel, its own photos) and the workspace (Branding logo and colour, the Sitio Google rating and address, «Financiamiento» only when the dealer offers financing). Nothing is invented: a car with no price answers 409 needs_price; a car that is reserved, sold or inactive answers 409 vehicle_not_available; one with no photos answers 400. vehicle is the snapshot the PNGs state; compare its price_clp / facts_version with the live vehicle to know when to regenerate. Answers 202; poll GET /ads/lanzamientos/car-creative-sets/\{set_id\} until status is done or failed.

Body

FieldTypeRequiredConstraints
vehicle_iduuidyes
hero_photostring—uri
carousel_photosstring[]—
carouselboolean—
overridesobject—
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/car-creative-sets \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e"
  }'

Example response (202)

{
  "data": {
    "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f12",
    "plantilla": "venta_auto",
    "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
    "status": "queued",
    "error": null,
    "created_at": "2026-10-27T15:00:00.000Z",
    "completed_at": null,
    "vehicle": {
      "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
      "facts_version": "3f9a0c1d2e4b5a6c",
      "price_clp": 18990000,
      "make": "Kia",
      "model": "Sportage",
      "version": "EX 2.0 4x2 AT",
      "year": 2023,
      "odometer_value": 42000,
      "odometer_unit": "KM",
      "transmission": "automatica",
      "fuel": "gasolina",
      "offers_financing": false,
      "hero_photo": "https://media.example.com/vehicles/<tenant>/photo-1.jpg",
      "carousel_photos": [
        "https://media.example.com/vehicles/<tenant>/photo-1.jpg"
      ],
      "captured_at": "2026-10-27T15:00:00.000Z"
    },
    "plan": {
      "claims": [
        "price",
        "odometer",
        "transmission",
        "fuel",
        "google_rating"
      ],
      "shots": [
        {
          "key": "car_1080x1350",
          "variant": "hero",
          "format": "4x5",
          "card_index": null
        },
        {
          "key": "car_1080x1920",
          "variant": "hero",
          "format": "9x16",
          "card_index": null
        },
        {
          "key": "car-card-01_1080x1350",
          "variant": "carousel",
          "format": "4x5",
          "card_index": 1
        }
      ]
    },
    "creatives": []
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/car-creative-sets/{set_id}

One per-car creative set

The set’s state, its vehicle snapshot and the PNGs rendered so far.

ParameterInTypeRequiredConstraints
set_idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/car-creative-sets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f12",
    "plantilla": "venta_auto",
    "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
    "status": "done",
    "error": null,
    "created_at": "2026-10-27T15:00:00.000Z",
    "completed_at": "2026-10-27T15:00:14.000Z",
    "vehicle": {
      "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
      "facts_version": "3f9a0c1d2e4b5a6c",
      "price_clp": 18990000,
      "make": "Kia",
      "model": "Sportage",
      "version": "EX 2.0 4x2 AT",
      "year": 2023,
      "odometer_value": 42000,
      "odometer_unit": "KM",
      "transmission": "automatica",
      "fuel": "gasolina",
      "offers_financing": false,
      "hero_photo": "https://media.example.com/vehicles/<tenant>/photo-1.jpg",
      "carousel_photos": [
        "https://media.example.com/vehicles/<tenant>/photo-1.jpg"
      ],
      "captured_at": "2026-10-27T15:00:00.000Z"
    },
    "plan": {
      "claims": [
        "price",
        "odometer",
        "transmission",
        "fuel",
        "google_rating"
      ],
      "shots": [
        {
          "key": "car_1080x1350",
          "variant": "hero",
          "format": "4x5",
          "card_index": null
        },
        {
          "key": "car_1080x1920",
          "variant": "hero",
          "format": "9x16",
          "card_index": null
        },
        {
          "key": "car-card-01_1080x1350",
          "variant": "carousel",
          "format": "4x5",
          "card_index": 1
        }
      ]
    },
    "creatives": [
      {
        "key": "car_1080x1350",
        "variant": "hero",
        "format": "4x5",
        "card_index": null,
        "width": 1080,
        "height": 1350,
        "url": "https://media.example.com/<tenant>/other/lanzamiento-<set>-car_1080x1350.png",
        "claims": [
          "price",
          "odometer",
          "transmission",
          "fuel",
          "google_rating"
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/creative-sets

Lanzamiento creative sets

The latest render sets, newest first, each with its rendered PNGs (angle × format).

ParameterInTypeRequiredConstraints
limitqueryintegerno≥ 1, ≤ 50, por defecto 20
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/creative-sets \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "sets": [
      {
        "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f11",
        "status": "done",
        "error": null,
        "created_at": "2026-10-26T15:00:00.000Z",
        "completed_at": "2026-10-26T15:00:21.000Z",
        "plan": {
          "angles": [
            {
              "angle": "comision",
              "renderable": true,
              "creatives": [
                {
                  "variant": "percent",
                  "claims": [
                    "commission_percent",
                    "only_pay_if_sold",
                    "google_rating",
                    "address"
                  ]
                }
              ]
            },
            {
              "angle": "opciones",
              "renderable": false,
              "reason": "needs_buy_outright"
            }
          ],
          "shots": [
            {
              "key": "comision-percent_1080x1350",
              "angle": "comision",
              "variant": "percent",
              "format": "4x5"
            },
            {
              "key": "comision-percent_1080x1920",
              "angle": "comision",
              "variant": "percent",
              "format": "9x16"
            }
          ]
        },
        "creatives": [
          {
            "key": "comision-percent_1080x1350",
            "angle": "comision",
            "variant": "percent",
            "format": "4x5",
            "width": 1080,
            "height": 1350,
            "url": "https://media.example.com/<tenant>/other/lanzamiento-<set>-comision-percent_1080x1350.png",
            "claims": [
              "commission_percent",
              "only_pay_if_sold",
              "google_rating",
              "address"
            ]
          }
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/creative-sets

Render the Lanzamiento static creatives

Queues a render of the consignment statics (six angles, each at 4:5 1080×1350 and 9:16 1080×1920) from the workspace’s own data: the Branding logo and colour, the Sitio address and Google rating, the connected portals, 1–3 of its own stock photos and the claims of its saved Oferta de consignación (PUT /ads/consignaciones/oferta; 409 oferta_not_configured until it is set up). An angle the Oferta does not back is not rendered, and plan.angles[].reason says why. Answers 202 with the set; poll GET /ads/lanzamientos/creative-sets/\{set_id\} until status is done or failed.

Body

FieldTypeRequiredConstraints
photosobject[]yes
anglestasacion \sin-riesgo \pasos \
overridesobject—
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/creative-sets \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "photos": [
      {
        "vehicle_id": "01d4822d-dc45-4b42-a78f-a0dfe402ee6e",
        "url": "https://media.example.com/vehicles/<tenant>/photo-1.jpg"
      }
    ]
  }'

Example response (202)

{
  "data": {
    "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f11",
    "status": "queued",
    "error": null,
    "created_at": "2026-10-26T15:00:00.000Z",
    "completed_at": null,
    "plan": {
      "angles": [
        {
          "angle": "comision",
          "renderable": true,
          "creatives": [
            {
              "variant": "percent",
              "claims": [
                "commission_percent",
                "only_pay_if_sold",
                "google_rating",
                "address"
              ]
            }
          ]
        },
        {
          "angle": "opciones",
          "renderable": false,
          "reason": "needs_buy_outright"
        }
      ],
      "shots": [
        {
          "key": "comision-percent_1080x1350",
          "angle": "comision",
          "variant": "percent",
          "format": "4x5"
        },
        {
          "key": "comision-percent_1080x1920",
          "angle": "comision",
          "variant": "percent",
          "format": "9x16"
        }
      ]
    },
    "creatives": []
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/creative-sets/{set_id}

One Lanzamiento creative set

The set’s state, its plan and the PNGs rendered so far.

ParameterInTypeRequiredConstraints
set_idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/creative-sets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f11",
    "status": "done",
    "error": null,
    "created_at": "2026-10-26T15:00:00.000Z",
    "completed_at": "2026-10-26T15:00:21.000Z",
    "plan": {
      "angles": [
        {
          "angle": "comision",
          "renderable": true,
          "creatives": [
            {
              "variant": "percent",
              "claims": [
                "commission_percent",
                "only_pay_if_sold",
                "google_rating",
                "address"
              ]
            }
          ]
        },
        {
          "angle": "opciones",
          "renderable": false,
          "reason": "needs_buy_outright"
        }
      ],
      "shots": [
        {
          "key": "comision-percent_1080x1350",
          "angle": "comision",
          "variant": "percent",
          "format": "4x5"
        },
        {
          "key": "comision-percent_1080x1920",
          "angle": "comision",
          "variant": "percent",
          "format": "9x16"
        }
      ]
    },
    "creatives": [
      {
        "key": "comision-percent_1080x1350",
        "angle": "comision",
        "variant": "percent",
        "format": "4x5",
        "width": 1080,
        "height": 1350,
        "url": "https://media.example.com/<tenant>/other/lanzamiento-<set>-comision-percent_1080x1350.png",
        "claims": [
          "commission_percent",
          "only_pay_if_sold",
          "google_rating",
          "address"
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lanzamientos/creative-sets/{set_id}/videos

The Lanzamiento consignment videos of a set

Each planned variant with its status (queued, rendering, ready, failed with the reason) and, once ready, the creative asset id and a short-lived playback URL. 404 until videos were requested for the set.

ParameterInTypeRequiredConstraints
set_idpathuuidyes
curl https://api.vitrinadev.com/api/v1/ads/lanzamientos/creative-sets/<id>/videos \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "9a3f6c1e-55b2-4f0e-8d7a-1b2c3d4e5f60",
    "creative_set_id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f11",
    "status": "rendering",
    "error": null,
    "created_at": "2026-10-27T15:00:00.000Z",
    "completed_at": null,
    "templates": [
      "asi-de-facil",
      "pov-chat",
      "tasacion"
    ],
    "voice_skipped": [],
    "renders": [
      {
        "id": "2b7e1d4a-8c3f-4a6e-9f10-3c4d5e6f7a81",
        "render_key": "tpl-asi-de-facil-20s-9x16-vo",
        "template": "asi-de-facil",
        "hook": null,
        "cut": 20,
        "aspect": "9x16",
        "voiced": true,
        "status": "ready",
        "error": null,
        "asset_id": "2b7e1d4a-8c3f-4a6e-9f10-3c4d5e6f7a81",
        "playback": {
          "url": "https://storage.example.com/<tenant>/ads-creatives/<id>.mp4?sig=…",
          "expires_at": "2026-10-27T15:10:00.000Z"
        }
      },
      {
        "id": "5d1c8e2f-3a4b-4c5d-8e6f-7a8b9c0d1e22",
        "render_key": "tpl-pov-chat-20s-9x16-vo",
        "template": "pov-chat",
        "hook": null,
        "cut": 20,
        "aspect": "9x16",
        "voiced": true,
        "status": "rendering",
        "error": null,
        "asset_id": null,
        "playback": null
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/creative-sets/{set_id}/videos

Render the Lanzamiento consignment videos

Queues the generated consignment videos for a render set: the same dealer facts, stock photos and Oferta as its statics, rendered as the templates in templates (default: «Así de fácil», «POV: vendes tu auto solo», «¿Cuánto vale tu auto hoy?», one 9:16 voiced video each) and/or m30, the original consignment video (hooks × cuts × 9:16/4:5 × voiced/music-only). Answers 202 with the video set; poll GET …/videos until status is done or failed. Idempotent: asking again returns the same set and re-queues only the variants that failed. 409 video_render_not_configured while the render service is not set up; 409 oferta_not_configured when the set’s Oferta cannot back a claim.

ParameterInTypeRequiredConstraints
set_idpathuuidyes

Body

FieldTypeRequiredConstraints
templatesasi-de-facil \pov-chat \tasacion \
hooksA \B \C[]
cuts12 \16 \20 \
aspects9x16 \4x5[]—
voiceboth \voiced \music
segment_labelstring—mín. 2, máx. 20
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/creative-sets/<id>/videos \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templates": [
      "asi-de-facil",
      "pov-chat",
      "tasacion"
    ]
  }'

Example response (202)

{
  "data": {
    "id": "9a3f6c1e-55b2-4f0e-8d7a-1b2c3d4e5f60",
    "creative_set_id": "6f1c2a8e-3b7d-4c55-9a10-2d4e8b9c0f11",
    "status": "queued",
    "error": null,
    "created_at": "2026-10-27T15:00:00.000Z",
    "completed_at": null,
    "templates": [
      "asi-de-facil",
      "pov-chat",
      "tasacion"
    ],
    "voice_skipped": [],
    "renders": []
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/lanzamientos/preflight

Check a Lanzamiento before creating it

Everything the wizard shows before the dealer presses «Crear»: the budget split per lane and per ad set, the ad account's daily spending limit and a warning when the total exceeds it (limit_status unknown means Meta could not be read: never treat it as 0), the Facebook Pages and the dataset the ads will use, the WhatsApp number, the default geo (the dealer's region: people who live in it or are regularly there) and every problem that would refuse the create. Reads only; creates nothing.

Body

FieldTypeRequiredConstraints
budgetobjectyes
lanesobject[]yes
with_videoboolean—por defecto false
creative_mixmixed \separate—
region_codesstring[]—
page_idstring—máx. 40
dataset_idstring—máx. 40
curl -X POST https://api.vitrinadev.com/api/v1/ads/lanzamientos/preflight \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "budget": {
      "package": "inicial"
    },
    "lanes": [
      {
        "lane": "landing"
      },
      {
        "lane": "whatsapp",
        "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5"
      }
    ],
    "with_video": true,
    "creative_mix": "mixed"
  }'

Example response (200)

{
  "data": {
    "budget": {
      "ok": true,
      "package": "inicial",
      "daily_total_clp": 40000,
      "per_lane_clp": {
        "landing": 20000,
        "whatsapp": 20000
      },
      "per_adset_clp": {
        "landing": 20000,
        "whatsapp": 20000
      },
      "message": null,
      "creative_mix": "mixed",
      "adsets_per_lane": {
        "landing": 1,
        "whatsapp": 1
      },
      "min_daily_total_clp": 40000,
      "packages": [
        {
          "package": "inicial",
          "daily_total_clp": 40000,
          "ok": true,
          "per_adset_clp": {
            "landing": 20000,
            "whatsapp": 20000
          }
        },
        {
          "package": "recomendado",
          "daily_total_clp": 60000,
          "ok": true,
          "per_adset_clp": {
            "landing": 30000,
            "whatsapp": 30000
          }
        },
        {
          "package": "intensivo",
          "daily_total_clp": 100000,
          "ok": true,
          "per_adset_clp": {
            "landing": 50000,
            "whatsapp": 50000
          }
        }
      ]
    },
    "ad_account": {
      "readable": true,
      "ad_account_id": "act_100000000000001",
      "currency": "CLP",
      "daily_spending_limit_clp": null,
      "limit_status": "unknown",
      "spend_today_clp": 0,
      "exceeds_limit": null,
      "warning": "No pudimos leer el límite de gasto diario de tu cuenta en Meta."
    },
    "pages": [
      {
        "page_id": "100000000000002",
        "name": "Tu Automotora"
      }
    ],
    "page_id": "100000000000002",
    "dataset": {
      "dataset_id": "100000000000003",
      "selectable": []
    },
    "whatsapp": {
      "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
      "phone_known": true,
      "page_link": "unknown",
      "page_link_warning": "Antes de lanzar por WhatsApp, el número tiene que estar vinculado a tu página «Tu Automotora» (en la página: Configuración, Cuentas vinculadas, WhatsApp). Desde aquí no podemos comprobarlo; si no lo está, Meta rechaza el canal de WhatsApp y solo se crea el Sitio."
    },
    "geo": {
      "region_codes": [
        "13"
      ],
      "label": "RM",
      "location_types": [
        "home"
      ],
      "supported": true
    },
    "headlines": [
      "commission",
      "solo_si_vende"
    ],
    "problems": []
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/leadgen-forms

Instant Form mappings

The Meta Instant Forms this workspace mapped, and the built-in presets. A form with no entry uses the default mapping (Meta’s standard fields — name, phone, email, company — and every other answer kept in the lead note) and the inbox intake. Submissions arrive once Meta approves lead access for the app.

curl https://api.vitrinadev.com/api/v1/ads/leadgen-forms \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "forms": [
      {
        "form_id": "1234567890123456",
        "meta_form": "1234567890123456",
        "name": "Vitrina · Automotoras · Higher intent v1",
        "intake": "demo",
        "enabled": true,
        "mapping": {
          "vertical": "automotora",
          "rules": []
        },
        "updated_at": "2026-10-11T15:00:00.000Z"
      }
    ],
    "presets": [
      "vitrina_automotora_v1",
      "vitrina_clinica_v1"
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

DELETE /ads/leadgen-forms/{form_id}

Remove an Instant Form mapping

Back to the default mapping and the inbox intake for that form. Leads already created are untouched.

ParameterInTypeRequiredConstraints
form_idpathstringyespatrón ^\d{1,32}$
curl -X DELETE https://api.vitrinadev.com/api/v1/ads/leadgen-forms/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 204 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/leadgen-forms/{form_id}

Map an Instant Form

Creates or replaces the mapping of one Instant Form: which question is the name, phone, email and company, how fit answers are read, which checkbox is the marketing opt-in and whether contacting the person is allowed by the submission itself (contact_consent). intake: demo feeds the founders’ demo intake (409 no_demo_booking for a workspace that takes none). A missing answer never blocks the lead.

ParameterInTypeRequiredConstraints
form_idpathstringyespatrón ^\d{1,32}$

Body

FieldTypeRequiredConstraints
namestring—máx. 160
intakeinbox \demo—
presetvitrina_automotora_v1 \vitrina_clinica_v1—
mappingobject—
enabledboolean—por defecto true
curl -X PUT https://api.vitrinadev.com/api/v1/ads/leadgen-forms/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Vitrina · Automotoras · Higher intent v1",
    "intake": "demo",
    "preset": "vitrina_automotora_v1"
  }'

Example response (200)

{
  "data": {
    "form_id": "1234567890123456",
    "meta_form": "1234567890123456",
    "name": "Vitrina · Automotoras · Higher intent v1",
    "intake": "demo",
    "enabled": true,
    "mapping": {
      "vertical": "automotora",
      "rules": []
    },
    "updated_at": "2026-10-11T15:00:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/leadgen-submissions

Latest Instant Form submissions

The latest submissions received, newest first: whether each was applied (processed), is being applied (received) or failed and will be retried (failed, with the error), which intake took it, and the lead and contact it produced. No answers.

ParameterInTypeRequiredConstraints
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
curl https://api.vitrinadev.com/api/v1/ads/leadgen-submissions \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "submissions": [
      {
        "leadgen_id": "444444444444",
        "meta_leadgen": "444444444444",
        "status": "processed",
        "route": "demo",
        "attempts": 1,
        "lead_id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e1f",
        "contact_id": "01926f3e-7c1a-7b2e-9f10-3a5b7c9d0e20",
        "error": null
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/lineas

Ads by Línea: spend, people and attributed cash per Línea

The per-ad scorecard (/ads/scorecard) summed by the Línea of each ad (D53). An ad belongs to the Línea the clinic’s own rules give its campaign, ad set and ad names (the same rules that set a lead’s Línea at first touch); an ad no rule places is «Sin línea» (linea_id: null). Same covered window, same numbers: the Líneas plus «Sin línea» add up to totals, which is the scorecard’s own sum. Every live Línea is listed, with zeros when no ad of the window belongs to it. Empty lineas for a workspace that is not a clinic. To list one Línea’s ads, send its id as linea_id to /ads/scorecard or /ads/creatives.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/lineas \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-09-20",
      "to": "2026-09-30",
      "days": 11
    },
    "coverage": null,
    "lineas": [
      {
        "linea_id": "0b1c2d3e-0000-4000-8000-0000000000b1",
        "nombre": "Implantes",
        "ads": 2,
        "spend": 100935,
        "people": 96,
        "booked": 4,
        "attended": 0,
        "accepted": {
          "count": 2,
          "value": 1357355
        },
        "cash": {
          "first": 18990,
          "recurring": 0,
          "total": 18990,
          "payments": 2
        },
        "ratios": {
          "accepted_return": 13.45,
          "cash_return": 0.19,
          "first_payment_return": 0.19,
          "cost_per_person": 1051
        }
      },
      {
        "linea_id": null,
        "nombre": null,
        "ads": 1,
        "spend": 120088,
        "people": 37,
        "booked": 0,
        "attended": 0,
        "accepted": null,
        "cash": null,
        "ratios": {
          "accepted_return": null,
          "cash_return": null,
          "first_payment_return": null,
          "cost_per_person": 3246
        }
      }
    ],
    "totals": {
      "ads": 3,
      "spend": 221023,
      "people": 133,
      "booked": 4,
      "attended": 0,
      "accepted": {
        "count": 2,
        "value": 1357355
      },
      "cash": {
        "first": 18990,
        "recurring": 0,
        "total": 18990,
        "payments": 2
      },
      "ratios": {
        "accepted_return": 6.14,
        "cash_return": 0.09,
        "first_payment_return": 0.09,
        "cost_per_person": 1662
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/measurement

Measurement incidents: what stopped measuring, since when

The measurement watchdog’s ledger (W1.1, «Vigilancia de la medición»). open are the legs broken right now — the Meta ad account read (disconnected or stale), the site tag’s heartbeat, the Conversions API sends, a WhatsApp channel, clinic facts not reaching the attribution — each with the day the data stopped and its ONE fix (action.path). recent are the ones resolved in the last 14 days, with who fixed them.

The owner and admins are told by WhatsApp and email at +0 h, +3 d and +7 d while an incident stays open. /ads/overview and /ads/campaigns carry the same incidents as gaps over their window.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/measurement \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "open": [
      {
        "id": "b1b1b1b1-0000-4000-8000-000000000001",
        "cause": "tag_silent",
        "label": "Etiqueta del sitio sin señal",
        "sentence": "La etiqueta de tu sitio web no envía visitas desde el 20 sep",
        "action": {
          "label": "Revisar la etiqueta",
          "path": "/anuncios/activar?paso=tag"
        },
        "since": "2026-09-20T14:05:00.000Z",
        "opened_at": "2026-09-27T15:00:00.000Z",
        "resolved_at": null,
        "resolution": null,
        "resolved_by_name": null,
        "metrics": []
      }
    ],
    "recent": [
      {
        "id": "b1b1b1b1-0000-4000-8000-000000000002",
        "cause": "meta_disconnected",
        "label": "Meta Ads desconectado",
        "sentence": "Tus anuncios de Meta dejaron de medirse el 12 sep",
        "action": {
          "label": "Reconectar Meta",
          "path": "/anuncios/activar?paso=meta"
        },
        "since": "2026-09-12T11:20:00.000Z",
        "opened_at": "2026-09-12T12:00:00.000Z",
        "resolved_at": "2026-09-15T13:42:00.000Z",
        "resolution": "reconnected",
        "resolved_by_name": "Camila",
        "metrics": [
          "spend"
        ]
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/meta-send

When the workspace started sending its outcomes to Meta

The experiment marker of «Antes y después de enviar a Meta». first_started_at is the first time outcomes started shipping to Meta (the owner’s «Empezar a enviar»): before it, Meta optimised the ads with no events from Vitrina. intervals is the full on/off history (sending can stop — «Solo medir», an undo — and start again), oldest first; stopped_at: null is the open one.

Recorded when a send action or a direct conversion-sync write changes what ships to Meta, and reconciled daily for changes made outside Vitrina (dated when Vitrina saw them). Dates only — no person, event or message. A fact about the workspace: the sample answers the real one. /ads/overview carries the same object as meta_send.

curl https://api.vitrinadev.com/api/v1/ads/meta-send \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "first_started_at": "2026-10-01T13:00:00.000Z",
    "sending": true,
    "intervals": [
      {
        "started_at": "2026-10-01T13:00:00.000Z",
        "stopped_at": "2026-10-20T15:30:00.000Z"
      },
      {
        "started_at": "2026-11-01T12:00:00.000Z",
        "stopped_at": null
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/meta-send/comparison

The funnel before vs after sending outcomes to Meta

Two windows of equal length either side of the day the workspace first started sending its outcomes to Meta (/ads/meta-send): from that day to yesterday, and the same number of days right before. Per window: the AI booking rate by conversation, spend, citas from ads, first-time paying patients from ads and the two costs (the Resumen’s own numbers). state: collecting (with reason) until both windows are long enough and have enough conversations — the windows are returned, delta stays null rather than a misleading difference. Counts and money only.

curl https://api.vitrinadev.com/api/v1/ads/meta-send/comparison \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "state": "collecting",
    "reason": "too_few_days",
    "started_on": "2026-10-04",
    "sending": true,
    "min_days": 14,
    "min_conversations": 20,
    "window_days": 3,
    "before": {
      "from": "2026-10-01",
      "to": "2026-10-03",
      "ai_booking_covered": true,
      "conversations": 118,
      "booked_conversations": 9,
      "rate_pct": 7.6,
      "spend_clp": 96400,
      "booked": 11,
      "paid": 2,
      "cost_per_booked": 8764,
      "cost_per_paid": 48200
    },
    "after": {
      "from": "2026-10-04",
      "to": "2026-10-06",
      "ai_booking_covered": true,
      "conversations": 104,
      "booked_conversations": 8,
      "rate_pct": 7.7,
      "spend_clp": 101300,
      "booked": 10,
      "paid": 1,
      "cost_per_booked": 10130,
      "cost_per_paid": 101300
    },
    "delta": null,
    "rule": "Compara dos períodos del mismo largo: desde el día en que empezaste a enviar tus resultados a Meta hasta ayer, y los mismos días justo antes."
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/monthly-return

What the ad patients paid, month by month, against ad spend

One point per America/Santiago calendar month of the window: what the people who came or came back through an ad paid that month (a conversation an ad opened in the 12 months before each payment) — their first payment (first_value) and every later payment (later_value, cuotas) — beside the ad spend of the same days. The same computation as /ads/overview’s current.patient_return, over the same window: the measured months add up to it to the peso (total).

Months that ended before measurement started (coverage.measured_from) answer measured: false and null figures — render «—», never 0. The month measurement starts in counts from that day (counted_from). At most 366 days per request. Aggregates only — no patient is identified.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/monthly-return \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-07-01",
      "to": "2026-09-30"
    },
    "months": [
      {
        "month": "2026-07",
        "from": "2026-07-01",
        "to": "2026-07-31",
        "measured": true,
        "counted_from": "2026-07-01",
        "partial": false,
        "spend": 2745532,
        "value": 4234202,
        "first_value": 2772678,
        "later_value": 1461524,
        "patients": 21,
        "payments": 30,
        "roas": 1.542
      },
      {
        "month": "2026-08",
        "from": "2026-08-01",
        "to": "2026-08-31",
        "measured": true,
        "counted_from": "2026-08-01",
        "partial": false,
        "spend": 2710434,
        "value": 6439688,
        "first_value": 3515081,
        "later_value": 2924607,
        "patients": 27,
        "payments": 38,
        "roas": 2.376
      },
      {
        "month": "2026-09",
        "from": "2026-09-01",
        "to": "2026-09-30",
        "measured": true,
        "counted_from": "2026-09-01",
        "partial": true,
        "spend": 1752236,
        "value": 3542033,
        "first_value": 1076914,
        "later_value": 2465119,
        "patients": 20,
        "payments": 29,
        "roas": 2.021
      }
    ],
    "total": {
      "spend": 7208202,
      "value": 14215923,
      "first_value": 7364673,
      "later_value": 6851250,
      "patients": 52,
      "payments": 97,
      "roas": 1.972
    },
    "coverage": {
      "measured_from": "2026-02-19",
      "touches_from": "2026-02-03",
      "from": "2026-07-01",
      "clipped": false,
      "previous_measured": true
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/overview

Ad spend, attributed revenue, ROAS and outcomes for a window

The attribution engine’s overview: current and previous (the immediately-preceding window of equal length), each carrying spend, clicks, ROAS, the cash/first-payment/recurring/refund/dispute legs and per-conversion-type outcome counts.

spend, roas and clicks are null — never 0 — exactly when spend_available is false (#1809): the engine could not scope spend to the window/model requested. A measured zero still arrives as 0 with spend_available: true. Render null as "—", never as zero, and let every ratio you derive from it inherit the null.

Every other money field (revenue, cash_revenue, first_payment_revenue, …) is parsed from the engine’s exact-decimal wire string (ADR 0018) to a number ONCE, at the provider mapper.

meta_send is the experiment marker (the same as /ads/meta-send): when the workspace started sending its outcomes to Meta.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
grainquerytotal \dayno
comparequeryprevious_periodno
curl https://api.vitrinadev.com/api/v1/ads/overview \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "current": {
      "spend": 845200,
      "spend_available": true,
      "clicks": 3120,
      "revenue": 3120000,
      "organic_direct_revenue": 410000,
      "roas": 3.69,
      "outcomes": 42,
      "attributed_outcomes": 31,
      "attributed_cash_outcomes": 9,
      "coverage_percent": 73.8,
      "cash_revenue": 2700000,
      "cash_payments": 11,
      "first_payment_revenue": 1950000,
      "first_payment_customers": 8,
      "recurring_revenue": 750000,
      "recurring_customers": 3,
      "refunds": 0,
      "refund_amount": 0,
      "disputes": 0,
      "dispute_amount": 0,
      "dispute_reversed_amount": 0,
      "outcome_counts": {
        "lead_created": 30,
        "appointment_booked": 9,
        "closed_won": 3
      }
    },
    "previous": {
      "spend": null,
      "spend_available": false,
      "clicks": null,
      "revenue": 2480000,
      "organic_direct_revenue": 410000,
      "roas": null,
      "outcomes": 42,
      "attributed_outcomes": 31,
      "attributed_cash_outcomes": 9,
      "coverage_percent": 73.8,
      "cash_revenue": 2700000,
      "cash_payments": 11,
      "first_payment_revenue": 1950000,
      "first_payment_customers": 8,
      "recurring_revenue": 750000,
      "recurring_customers": 3,
      "refunds": 0,
      "refund_amount": 0,
      "disputes": 0,
      "dispute_amount": 0,
      "dispute_reversed_amount": 0,
      "outcome_counts": {
        "lead_created": 30,
        "appointment_booked": 9,
        "closed_won": 3
      }
    },
    "days": [
      {
        "date": "2026-08-24",
        "spend": 31800,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-25",
        "spend": 35700,
        "revenue": 130000,
        "outcomes": 3,
        "attributed_outcomes": 2,
        "partial": false
      },
      {
        "date": "2026-08-26",
        "spend": 28700,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-27",
        "spend": 33400,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-28",
        "spend": 30300,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-29",
        "spend": 19700,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-30",
        "spend": 22100,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-31",
        "spend": 28700,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-01",
        "spend": 33400,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-02",
        "spend": 30300,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-03",
        "spend": 31800,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-04",
        "spend": 35700,
        "revenue": 130000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-05",
        "spend": 17800,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-06",
        "spend": 20700,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-07",
        "spend": 30300,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-08",
        "spend": 31800,
        "revenue": 120000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-09",
        "spend": 35700,
        "revenue": 130000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-10",
        "spend": 28700,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-11",
        "spend": 33400,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-12",
        "spend": 18800,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-13",
        "spend": 19700,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-14",
        "spend": 35700,
        "revenue": 130000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-15",
        "spend": 28700,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-16",
        "spend": 33400,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-17",
        "spend": 30200,
        "revenue": 110000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-18",
        "spend": 31800,
        "revenue": 120000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-19",
        "spend": 22100,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-20",
        "spend": 17800,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-21",
        "spend": 33400,
        "revenue": 120000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-09-22",
        "spend": 13600,
        "revenue": 50000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": true
      }
    ],
    "previous_days": [
      {
        "date": "2026-07-25",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-26",
        "spend": null,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-27",
        "spend": null,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-28",
        "spend": null,
        "revenue": 100000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-29",
        "spend": null,
        "revenue": 90000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-30",
        "spend": null,
        "revenue": 90000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-07-31",
        "spend": null,
        "revenue": 110000,
        "outcomes": 3,
        "attributed_outcomes": 2,
        "partial": false
      },
      {
        "date": "2026-08-01",
        "spend": null,
        "revenue": 50000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-02",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-03",
        "spend": null,
        "revenue": 90000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-04",
        "spend": null,
        "revenue": 90000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-05",
        "spend": null,
        "revenue": 110000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-06",
        "spend": null,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-07",
        "spend": null,
        "revenue": 100000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-08",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-09",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-10",
        "spend": null,
        "revenue": 110000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-11",
        "spend": null,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-12",
        "spend": null,
        "revenue": 100000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-13",
        "spend": null,
        "revenue": 90000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-14",
        "spend": null,
        "revenue": 90000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-15",
        "spend": null,
        "revenue": 70000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-16",
        "spend": null,
        "revenue": 50000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-17",
        "spend": null,
        "revenue": 100000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-18",
        "spend": null,
        "revenue": 90000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-19",
        "spend": null,
        "revenue": 90000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-20",
        "spend": null,
        "revenue": 110000,
        "outcomes": 2,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-21",
        "spend": null,
        "revenue": 80000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-22",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      },
      {
        "date": "2026-08-23",
        "spend": null,
        "revenue": 60000,
        "outcomes": 1,
        "attributed_outcomes": 1,
        "partial": false
      }
    ],
    "gaps": [
      {
        "from": "2026-09-12",
        "to": "2026-09-15",
        "cause": "meta_disconnected",
        "metrics": [
          "spend"
        ],
        "incident_id": "b1b1b1b1-0000-4000-8000-000000000002"
      }
    ],
    "meta_send": {
      "first_started_at": "2026-10-01T13:00:00.000Z",
      "sending": true,
      "intervals": [
        {
          "started_at": "2026-10-01T13:00:00.000Z",
          "stopped_at": "2026-10-20T15:30:00.000Z"
        },
        {
          "started_at": "2026-11-01T12:00:00.000Z",
          "stopped_at": null
        }
      ]
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/paid-funnel

Paid funnel: spend, leads, qualified, booked, held and paid per arm, campaign, ad set and ad

The scoreboard that judges a paid test on cost per demo HELD. Per row: people at each step (lead → ICP-qualified → demo booked → demo held → first paid invoice), the cost of each step, contacted within 24 h, lead-to-booked, show rate and qualified share, for one destination arm (whatsapp, instant_form, website, other; an ad with spend and nobody is unassigned). Steps are read from the outcome ledger the Meta events leave from, so a column equals the events sent for the same period (basis=event; reconciliation.events prints them). People with no id at the level sit on the arm’s unattributed row, so rows sum to totals; reconciliation.spend_matches checks Σ spend against /ads/overview. A failed read is null, never 0.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
levelqueryarm \campaign \ad_set \
basisquerycohort \eventno
modelquerystringno
curl https://api.vitrinadev.com/api/v1/ads/paid-funnel \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "level": "arm",
    "basis": "cohort",
    "window": {
      "from": "2026-11-16",
      "to": "2026-11-29"
    },
    "rows": [
      {
        "arm": "whatsapp",
        "external_id": null,
        "name": null,
        "parent": null,
        "unattributed": false,
        "leads": 100,
        "qualified": 50,
        "booked": 25,
        "held": 12,
        "paid": 0,
        "contacted_24h": 90,
        "spend": 1400000,
        "costs": {
          "per_lead": 14000,
          "per_qualified": 28000,
          "per_booked": 56000,
          "per_held": 112000,
          "per_paid": null
        },
        "rates": {
          "qualified_share": 0.5,
          "contacted_24h_rate": 0.9,
          "lead_to_booked": 0.25,
          "show_rate": 0.5
        }
      }
    ],
    "arms": [],
    "totals": {
      "leads": 100,
      "qualified": 50,
      "booked": 25,
      "held": 12,
      "paid": 0,
      "contacted_24h": 90,
      "spend": 1400000,
      "costs": {
        "per_lead": 14000,
        "per_qualified": 28000,
        "per_booked": 56000,
        "per_held": 112000,
        "per_paid": null
      },
      "rates": {
        "qualified_share": 0.5,
        "contacted_24h_rate": 0.9,
        "lead_to_booked": 0.25,
        "show_rate": 0.5
      }
    },
    "reconciliation": {
      "overview_spend": 1400000,
      "spend_matches": true,
      "events": null,
      "counts_match": null,
      "leads_without_ad": 0
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/people

Who is behind an Ads number (name, RUT, phone)

The people behind ONE count of the Ads screens, read from the very read that count is folded from — so count equals the number shown and people has exactly count rows (unless truncated). metric names the count: the patient return (paid_patients, arrived_patients, returned_patients — overall, per campaign, per ad, or «sin campaña conocida» with unplaced=1), the people funnel (funnel_&lt;step&gt; = /ads/stages M1 / M2 / M3) or the conversations ads opened (conversations[_&lt;fact&gt;], one row per conversation).

Each row carries the contact and its roster patient with exactly the shape and identity gate of /ads/attributed-sales (patient_link, patient.\{id,name,rut,phone\}: name / rut / phone need contacts:read + clinic:read, plus clinic_patients:read for an API credential), its ad / campaign of origin, the stage reached, its dates and — for the patient return — every counted payment. This is a DIFFERENT population from /ads/attributed-sales (the engine’s last-click credit of one campaign): open the list of the number you show.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
metricquerypaid_patients \arrived_patients \returned_patients \
campaign_idquerystringnomín. 1
ad_idquerystringnopatrón ^[0-9]{1,32}$
unplacedquery1 \trueno
limitqueryintegerno> 0, ≤ 5000
curl https://api.vitrinadev.com/api/v1/ads/people \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "metric": "returned_patients",
    "window": {
      "from": "2026-09-08",
      "to": "2026-10-07"
    },
    "scope": {
      "campaign_external_id": null,
      "ad_external_id": null,
      "unplaced": false
    },
    "count": 1,
    "people": [
      {
        "key": "7d1c2a5e-0000-4000-8000-000000000001",
        "patient_link": "linked",
        "patient": {
          "id": "7d1c2a5e-0000-4000-8000-000000000001",
          "name": "María Ejemplo Soto",
          "rut": "11.111.111-1",
          "phone": "+56911111111"
        },
        "contact": {
          "id": "3f9b6c10-0000-4000-8000-000000000002",
          "display_name": "María Ejemplo",
          "key": "0002"
        },
        "origin": {
          "ad_external_id": "120210000000000002",
          "campaign_external_id": "120210000000000010"
        },
        "stage": null,
        "kind": "returned",
        "first_at": "2026-09-10",
        "paid": {
          "total": 500000,
          "count": 1,
          "first_value": 0,
          "payments": [
            {
              "day": "2026-09-10",
              "amount": 500000,
              "is_first_payment": false,
              "kind": "returned",
              "ad_external_id": "120210000000000002",
              "campaign_external_id": "120210000000000010"
            }
          ]
        },
        "conversation": null
      }
    ],
    "truncated": false,
    "state": "ready",
    "people_state": "ready"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/proyectos

Ads by proyecto: cost per lead, visita and reserva

The per-ad scorecard (/ads/scorecard) summed by the proyecto each ad sells, for an inmobiliaria. An ad belongs to a proyecto when its id is in the proyecto’s meta_ad_ids, else when the proyecto’s name appears in its campaign, ad set or ad name (whole words; the longest name wins; a tie places nobody); an ad no rule places is «Sin proyecto» (proyecto_id: null). Each row has spend, leads, visitas agendadas, visitas realizadas, reservas and the cost of each. Same covered window, same numbers: the proyectos plus «Sin proyecto» add up to totals, the scorecard’s own sum. Every live proyecto is listed, with zeros when no ad of the window belongs to it. Empty proyectos for a workspace with none.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/proyectos \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-09-21",
      "to": "2026-10-20",
      "days": 30
    },
    "coverage": null,
    "proyectos": [
      {
        "proyecto_id": "0b1c2d3e-0000-4000-8000-0000000000c1",
        "nombre": "Condominio Altos de Lircay",
        "region": "Región del Maule",
        "comuna": "Talca",
        "match": "name",
        "campaign_names": [
          "Altos de Lircay · Formulario Meta"
        ],
        "ads": 4,
        "campaigns": 2,
        "spend": 1925530,
        "leads": 321,
        "booked": 39,
        "attended": 25,
        "reservas": 5,
        "reservas_value": 3300000,
        "costs": {
          "per_lead": 5999,
          "per_booked": 49373,
          "per_attended": 77021,
          "per_reserva": 385106
        },
        "rates": {
          "lead_to_booked": 0.121,
          "booked_to_attended": 0.641,
          "attended_to_reserva": 0.2
        }
      },
      {
        "proyecto_id": null,
        "nombre": null,
        "region": null,
        "comuna": null,
        "match": null,
        "campaign_names": [
          "Marca · Alcance · Video"
        ],
        "ads": 3,
        "campaigns": 3,
        "spend": 3683150,
        "leads": 441,
        "booked": 36,
        "attended": 22,
        "reservas": 1,
        "reservas_value": 650000,
        "costs": {
          "per_lead": 8352,
          "per_booked": 102310,
          "per_attended": 167416,
          "per_reserva": 3683150
        },
        "rates": {
          "lead_to_booked": 0.082,
          "booked_to_attended": 0.611,
          "attended_to_reserva": 0.045
        }
      }
    ],
    "totals": {
      "ads": 7,
      "campaigns": 5,
      "spend": 5608680,
      "leads": 762,
      "booked": 75,
      "attended": 47,
      "reservas": 6,
      "reservas_value": 3950000,
      "costs": {
        "per_lead": 7360,
        "per_booked": 74782,
        "per_attended": 119333,
        "per_reserva": 934780
      },
      "rates": {
        "lead_to_booked": 0.098,
        "booked_to_attended": 0.627,
        "attended_to_reserva": 0.128
      }
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/report

Reporte: campaigns, ad sets or ads with their funnel, costs and cash return

One table at level (campaign, ad set or ad) for the window. Per row: the spend and the cash credited by the attribution engine, Vitrina’s own conversational funnel (conversations each ad opened → answered → citas → attended → closes) with the cost of each step, «Retorno» = cash ÷ spend (null when no payment was recorded in the period), and Meta’s own figures apart. tail holds the period’s conversations with no ad behind them, so from-ads + tail = the workspace total; reconciliation states Σ spend against /ads/overview. A failed read is null, never 0.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
levelquerycampaign \ad_set \ad
curl https://api.vitrinadev.com/api/v1/ads/report \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "level": "campaign",
    "window": {
      "from": "2026-08-30",
      "to": "2026-09-28"
    },
    "rows": [
      {
        "external_id": "120211000000000001",
        "level": "campaign",
        "name": "Implantes · Septiembre",
        "parent": null,
        "thumbnail_url": null,
        "spark": [
          13200,
          14100,
          13900
        ],
        "spend": 412300,
        "funnel": {
          "conversations": 120,
          "replied": 110,
          "appointments": 20,
          "attended": 12,
          "closes": 2
        },
        "cost_per": {
          "conversation": 3436,
          "appointment": 20615,
          "attended": 34358,
          "close": 206150
        },
        "cash": 0,
        "return": null,
        "meta": {
          "conversations_started": 131,
          "cost_per_conversation": 3147
        }
      }
    ],
    "tail": [
      {
        "key": "direct",
        "conversations": 210
      },
      {
        "key": "website",
        "conversations": 40
      }
    ],
    "totals": {
      "spend": 412300,
      "funnel": {
        "conversations": 120,
        "replied": 110,
        "appointments": 20,
        "attended": 12,
        "closes": 2
      },
      "cost_per": {
        "conversation": 3436,
        "appointment": 20615,
        "attended": 34358,
        "close": 206150
      },
      "cash": 0,
      "return": null,
      "meta": {
        "conversations_started": 131,
        "cost_per_conversation": 3147
      }
    },
    "pipeline": {
      "closed_count": 3,
      "closed_value": 93082
    },
    "cash_recorded": false,
    "reconciliation": {
      "overview_spend": 412300,
      "spend_matches": true,
      "conversations_total": 370,
      "conversations_from_ads": 120,
      "conversations_without_ad": 250
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/report/export

Export the Reporte (aggregates only)

The same table as GET /ads/report, as a file. CSV: ;, Spanish headers, dd-mm-aaaa dates, a preamble with the period, the model and «Retorno = caja». XLSX: the table plus a «Notas» sheet. No row names a person. Audited as ads.report.export.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
levelquerycampaign \ad_set \ad
formatquerycsv \xlsxno
curl https://api.vitrinadev.com/api/v1/ads/report/export \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/scorecard

The one per-ad scorecard (ranking, verdicts, the four returns)

Every ad over the COVERED window (max(from, the first measured day)): spend and cash (first + recurring) from the attribution engine, people, bookings, attended visits and accepted presupuestos from Vitrina’s own rows (the conversations each ad opened), the four ratios, a rank and ONE verdict. Every Ads surface that speaks about one ad reads this — the best-ad card, Creativos, the actions and «¿Por qué?» never compute a return of their own.

Ranking: accepted-quote return, then cost per person, then bookings. Cash return sorts first only once the window holds cash_evidence.min_payments payments credited to ads. An ad with too little spend or too few people is not ranked (thin). The top-ranked ad and any ad returning ≥ 1× are never proposed for a pause. Nothing here is a probability.

ParameterInTypeRequiredConstraints
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
windowquery7d \14d \28d \
modelquerystringno
linea_idqueryanyno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/scorecard \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-09-20",
      "to": "2026-09-30",
      "days": 11
    },
    "coverage": null,
    "basis": "accepted",
    "cash_evidence": {
      "payments": 3,
      "min_payments": 5,
      "used_for_ranking": false
    },
    "evidence": {
      "thin": true,
      "days": 11,
      "min_days": 14
    },
    "rows": [
      {
        "ad_external_id": "120212000000000101",
        "ad_name": "Implantes 2",
        "display_name": "Implantes › INTERESES › Implantes 2",
        "ad_set_name": "INTERESES",
        "campaign_name": "Implantes",
        "campaign_external_id": "120210000000000001",
        "thumbnail_url": null,
        "video_external_id": null,
        "ads_manager_url": "https://www.facebook.com/adsmanager/manage/ads?act=<account-id>&selected_ad_ids=120212000000000101",
        "spend": 57861,
        "people": 52,
        "booked": 0,
        "attended": 0,
        "accepted": {
          "count": 1,
          "value": 1343355
        },
        "cash": {
          "first": 4990,
          "recurring": 0,
          "total": 4990,
          "payments": 1
        },
        "fatigue": "active",
        "ratios": {
          "accepted_return": 23.22,
          "cash_return": 0.09,
          "first_payment_return": 0.09,
          "cost_per_person": 1113
        },
        "lead": {
          "metric": "accepted",
          "value": 23.22
        },
        "rank": 1,
        "verdict": "best"
      }
    ],
    "best_ad_external_id": "120212000000000101"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/share/resumen

One calendar month as the Resumen share card (data)

The month the monthly Ads email reports, as data: the card’s dominant figure (the return; the outcomes when there is no value yet; the spend when nothing came back), its caption and lines pre-formatted es-CL in the workspace’s nouns, up to three fact-built sentences (no model) and — when the engine’s recent measurement verdict is broken — the reason, printed after «Falta parte del seguimiento:». period = YYYY-MM (Santiago), default the previous month. No sample mode.

ParameterInTypeRequiredConstraints
periodquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])$
themequerylight \darkno
curl https://api.vitrinadev.com/api/v1/ads/share/resumen \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "period": {
      "month": "2026-08",
      "from": "2026-08-01",
      "to": "2026-08-31",
      "closed": true,
      "label": "Agosto · 1 – 31 ago 2026"
    },
    "vocabulary": "generic",
    "has_activity": true,
    "dominant": "roas",
    "headline": "Agosto: 3,7× de retorno",
    "tile": {
      "number": "3,7×",
      "unit": "retorno",
      "caption": "$845.200 invertidos · $3.120.000 en cierres",
      "lines": [
        "31 cierres desde anuncios",
        "Mejor campaña: «Temporada de primavera», 4,2× de retorno",
        "En julio: 2,9× de retorno"
      ]
    },
    "sentences": [
      "Tus anuncios trajeron **$3.120.000** en cierres, con 31 cierres (el 73,8% del total).",
      "La mejor campaña fue «Temporada de primavera», con 4,2× de retorno.",
      "Son 7 cierres más que en julio."
    ],
    "tracking_issue": null,
    "image_alt": "3,7× retorno en agosto. $845.200 invertidos · $3.120.000 en cierres. 31 cierres desde anuncios. Mejor campaña: «Temporada de primavera», 4,2× de retorno. En julio: 2,9× de retorno",
    "figures": {
      "spend": 845200,
      "revenue": 3120000,
      "roas": 3.69,
      "attributed_outcomes": 31,
      "best_campaign": "Temporada de primavera"
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/share/resumen.png

One calendar month as the Resumen share card (PNG)

The same card as GET /ads/share/resumen, drawn as the Resumen «Compartir» image: a 1200 × 630 PNG (the monthly Ads email carries its own shorter crop of the same facts instead). Aggregates and the workspace’s own campaign names only. Carries a strong ETag (a repeat with If-None-Match answers 304) and Cache-Control: private — a day for a closed month, five minutes for the running one.

ParameterInTypeRequiredConstraints
periodquerystringnopatrón ^\d{4}-(0[1-9]|1[0-2])$
themequerylight \darkno
curl https://api.vitrinadev.com/api/v1/ads/share/resumen.png \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 200 · 304 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 503

GET /ads/stages

Where the funnel leaks: M1 ads · M2 agenda · M3 clinic sale

The period’s people from ads in three stages, each with its owner: M1 who wrote (and the cost per conversation), M2 who were answered, how fast, who booked and who attended, M3 quotes presented, accepted and paid — the same people as /ads/funnel. budget_cohorts breaks M3’s quotes down by week of presentation (accepted vs presented, and how many of the accepted were presented in an earlier week). reference carries the program’s healthy writing → booking band (10–15 %) and weakest names the stage that leaks by a plain rule. Counts only — no person is identified, never a probability.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
modelquerystringno
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/stages \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "window": {
      "from": "2026-09-01",
      "to": "2026-09-30"
    },
    "m1": {
      "conversations": 215,
      "spend": 1752236,
      "cost_per_conversation": 8150
    },
    "m2": {
      "ad_conversations": 226,
      "answered": 219,
      "answered_rate": 0.969,
      "median_first_response_minutes": 4,
      "booked": 24,
      "attended": 15,
      "booked_rate": 0.112,
      "attended_rate": 0.625,
      "new_people": {
        "window": {
          "from": "2026-09-21",
          "to": "2026-09-30"
        },
        "booking_start": "2026-09-21",
        "mature_days": 3,
        "unmeasured": null,
        "people": 162,
        "booked": 9,
        "booked_rate": 0.056,
        "paid": 2,
        "paid_rate": 0.222,
        "replied": 118,
        "replied_booked": 9,
        "replied_rate": 0.076,
        "returned": {
          "people": 31,
          "booked": 3
        }
      }
    },
    "m3": {
      "quote_presented": 11,
      "accepted": 5,
      "paid": 3,
      "paid_rate": 0.273
    },
    "reference": {
      "m2_booked_rate": {
        "low": 0.1,
        "high": 0.15,
        "label": "program"
      }
    },
    "weakest": null,
    "credit_known": true
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/state

The tenant's Vitrina Ads entitlement and delegated-key state

Distinguishes "not entitled" (entitlement: off) from "needs re-mint" (needs_remint: true — the live key is the wrong kind for the declared entitlement) and from an in-flight change (pending_on / pending_off). grant_scopes are the scopes the attribution engine returned on the last provision or entitlement call. Requires ads:read.

curl https://api.vitrinadev.com/api/v1/ads/state \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "entitlement": "on",
    "needs_remint": false,
    "key_kind": "ads",
    "grant_scopes": [
      "connections",
      "attribution"
    ],
    "last_error": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /ads/tracking/gtm

Google Tag Manager install: connection, container, install state

Where the programmatic Google Tag Manager install of the Vitrina Ads tag stands: whether Google is connected (and with which Tag Manager permissions — can_edit, can_publish), the chosen web container, and the install state (none → draft → published, or removed). A database read — never calls Google.

available: false means this deployment cannot connect Google: use the manual installer (GET /ads/tracking/installers/gtm). Verify the tag is live with GET /ads/tracking/status. Not gated by the Ads add-on, so a workspace that turned Ads off can still see and remove its tag. Admin-scoped (ads:read).

curl https://api.vitrinadev.com/api/v1/ads/tracking/gtm \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "draft",
      "workspace_path": "accounts/1234567890/containers/987654321/workspaces/7",
      "workspace_url": "https://tagmanager.google.com/#/container/accounts/1234567890/containers/987654321/workspaces/7",
      "tag_path": "accounts/1234567890/containers/987654321/workspaces/7/tags/12",
      "trigger_path": "accounts/1234567890/containers/987654321/workspaces/7/triggers/11",
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": null,
      "published_at": null
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /ads/tracking/gtm/connect

Start the Google consent for Tag Manager

Returns the Google consent URL to send the browser to. access: edit asks the minimal Tag Manager permissions (read containers, write the tag, preview); access: publish adds creating and publishing container versions — asked incrementally, the first time the admin publishes (a 409 GTM_CONSENT_REQUIRED with details.access: publish says when). After the consent the browser lands on return_to with ?gtm_code=…&gtm_state=… — finish with POST /ads/tracking/gtm/connect/complete — or with ?gtm_error= consent_denied | oauth_failed.

return_to must be on the app's own origin. Tokens are stored encrypted and never returned. Admin-scoped (ads:write).

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/connect \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "return_to": "https://app.example.com/w/example/ads",
    "access": "edit"
  }'

Example response (200)

{
  "data": {
    "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=<client-id>&scope=<scopes>&state=<state>"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429 · 503

POST /ads/tracking/gtm/connect/complete

Finish the Google consent for Tag Manager

Exchanges the code the consent returned (gtm_code / gtm_state on the return URL) and stores the Google tokens encrypted. The signed state must belong to the CALLER — the same workspace, and the same person when it recorded one — otherwise 403: a consent link started by someone else can never land a Google grant in their workspace. Single use. Answers the new state (connection.can_edit / can_publish read the scopes Google actually granted). Admin-scoped (ads:write).

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/connect/complete \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "<code>",
    "state": "<state>"
  }'

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": null,
    "install": {
      "state": "none",
      "workspace_path": null,
      "workspace_url": null,
      "tag_path": null,
      "trigger_path": null,
      "site_key": null,
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": null,
      "published_at": null
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429 · 502 · 503

DELETE /ads/tracking/gtm/connection

Disconnect Google Tag Manager

Revokes the Google grant and forgets the tokens. The container choice and install facts stay, so reconnecting later can still uninstall the tag. Does not touch the container. Admin-scoped (ads:write).

curl -X DELETE https://api.vitrinadev.com/api/v1/ads/tracking/gtm/connection \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "not_connected",
      "account_email": null,
      "can_edit": false,
      "can_publish": false
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "draft",
      "workspace_path": "accounts/1234567890/containers/987654321/workspaces/7",
      "workspace_url": "https://tagmanager.google.com/#/container/accounts/1234567890/containers/987654321/workspaces/7",
      "tag_path": "accounts/1234567890/containers/987654321/workspaces/7/tags/12",
      "trigger_path": "accounts/1234567890/containers/987654321/workspaces/7/triggers/11",
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": null,
      "published_at": null
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /ads/tracking/gtm/container

Choose the Tag Manager container to install into

Stores the container (a path from GET /ads/tracking/gtm/containers). Only web containers are accepted. Choosing a different container resets the install state. Admin-scoped (ads:write), Ads gate.

curl -X PUT https://api.vitrinadev.com/api/v1/ads/tracking/gtm/container \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "container_path": "accounts/1234567890/containers/987654321"
  }'

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "none",
      "workspace_path": null,
      "workspace_url": null,
      "tag_path": null,
      "trigger_path": null,
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": null,
      "published_at": null
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 502

GET /ads/tracking/gtm/containers

The Tag Manager web containers the Google account can reach

Every Tag Manager account of the connected Google account with its web containers. suggested marks the containers whose domains match the site's (the site key's allowed origins and the workspace's declared website); suggested_path is the one to pre-select. Live Tag Manager read. Admin-scoped (ads:read), Ads gate.

curl https://api.vitrinadev.com/api/v1/ads/tracking/gtm/containers \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "accounts": [
      {
        "path": "accounts/1234567890",
        "name": "Example",
        "containers": [
          {
            "path": "accounts/1234567890/containers/987654321",
            "code": "GTM-XXXXXXX",
            "name": "example.com",
            "domains": [
              "example.com"
            ],
            "suggested": true
          }
        ]
      }
    ],
    "suggested_path": "accounts/1234567890/containers/987654321",
    "site_hosts": [
      "example.com"
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 502

POST /ads/tracking/gtm/install

Write the Vitrina Ads tag into the container (unpublished)

Creates (or reuses) a dedicated workspace «Vitrina Ads» and writes ONE Custom HTML tag «Vitrina Ads · tag» (tag type html, the snippet of GET /ads/tracking/installers/gtm verbatim) fired by ONE page-view trigger (type pageview, All Pages). Idempotent: re-running updates the same tag and never duplicates it. Nothing is published.

review lists what else is pending: changes in the workspace that are not Vitrina’s (they would ride along on publish), other workspaces with unpublished changes (never published by Vitrina), and other tags that already load tag.js. Admin-scoped (ads:write), Ads gate.

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/install \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "draft",
      "workspace_path": "accounts/1234567890/containers/987654321/workspaces/7",
      "workspace_url": "https://tagmanager.google.com/#/container/accounts/1234567890/containers/987654321/workspaces/7",
      "tag_path": "accounts/1234567890/containers/987654321/workspaces/7/tags/12",
      "trigger_path": "accounts/1234567890/containers/987654321/workspaces/7/triggers/11",
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": null,
      "published_at": null
    },
    "review": {
      "pending_changes": [],
      "other_workspaces": [
        {
          "name": "Default Workspace",
          "pending_changes": 2
        }
      ],
      "duplicate_loader_tags": []
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 502

POST /ads/tracking/gtm/preview

Preview the tag on the site (Tag Assistant)

Writes the tag (as install) and quick-previews the «Vitrina Ads» workspace. install.preview_url is the Tag Assistant link that opens the site with the workspace in preview — only the person who opens it sees the tag. Nothing is published. Admin-scoped (ads:write), Ads gate.

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/preview \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "draft",
      "workspace_path": "accounts/1234567890/containers/987654321/workspaces/7",
      "workspace_url": "https://tagmanager.google.com/#/container/accounts/1234567890/containers/987654321/workspaces/7",
      "tag_path": "accounts/1234567890/containers/987654321/workspaces/7/tags/12",
      "trigger_path": "accounts/1234567890/containers/987654321/workspaces/7/triggers/11",
      "site_key": "<site-key>",
      "preview_url": "https://tagassistant.google.com/#/?source=TAG_MANAGER&id=GTM-XXXXXXX&url=https%3A%2F%2Fexample.com",
      "previewed_at": "2026-09-29T15:00:00.000Z",
      "published_version_path": null,
      "published_at": null
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 502

POST /ads/tracking/gtm/publish

Publish the tag (new container version)

Writes the tag (as install), syncs the workspace, creates a container version from it and publishes that version. Needs the publish consent (409 GTM_CONSENT_REQUIRED, details.access: publish, otherwise).

Never publishes someone else’s changes silently. Merge conflicts refuse with 409 GTM_WORKSPACE_CONFLICT; changes in the workspace that are not Vitrina’s refuse with 409 GTM_PENDING_CHANGES listing them, until the call is repeated with include_pending_changes: true after a person has seen that list. Changes in other workspaces are never part of the version. Idempotent: an already-published, unchanged tag publishes nothing. Then verify with GET /ads/tracking/status. Admin-scoped (ads:write), Ads gate.

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/publish \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "include_pending_changes": false
  }'

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "published",
      "workspace_path": null,
      "workspace_url": null,
      "tag_path": null,
      "trigger_path": null,
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": "accounts/1234567890/containers/987654321/versions/42",
      "published_at": "2026-09-29T15:04:05.000Z"
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429 · 502

POST /ads/tracking/gtm/uninstall

Remove the tag (new container version)

Deletes the Vitrina tag and its trigger in the «Vitrina Ads» workspace, then creates and publishes a container version — with the same pending-changes guard as publish. When the container holds no Vitrina tag there is nothing to publish. Not gated by the Ads add-on: a workspace that turned Ads off can still remove its tag. Needs the publish consent. Admin-scoped (ads:write).

curl -X POST https://api.vitrinadev.com/api/v1/ads/tracking/gtm/uninstall \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "include_pending_changes": false
  }'

Example response (200)

{
  "data": {
    "available": true,
    "connection": {
      "status": "connected",
      "account_email": "[email protected]",
      "can_edit": true,
      "can_publish": true
    },
    "container": {
      "path": "accounts/1234567890/containers/987654321",
      "code": "GTM-XXXXXXX",
      "name": "example.com",
      "domains": [
        "example.com",
        "www.example.com"
      ]
    },
    "install": {
      "state": "removed",
      "workspace_path": null,
      "workspace_url": null,
      "tag_path": null,
      "trigger_path": null,
      "site_key": "<site-key>",
      "preview_url": null,
      "previewed_at": null,
      "published_version_path": "accounts/1234567890/containers/987654321/versions/42",
      "published_at": "2026-09-29T15:04:05.000Z"
    },
    "review": null
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429 · 502

GET /ads/tracking/installers/gtm

The tag as a ready-to-paste Google Tag Manager installer

The tag name, "All Pages" trigger, snippet and setup checklist for a Google Tag Manager Custom HTML tag. The snippet is the COMBINED loader (&lt;script src=".../tag.js?site=…"&gt;): pasting this ONE tag boots the assistant AND starts the measurement. tag_name and setup_checklist are Vitrina-branded user-facing copy (Spanish) that names only tag.js.

site_id (optional) is the publishable pk_… key of the site the tag is being installed on — embedded into the snippet's ?site= query so the tag survives GTM stripping data-* off the &lt;script&gt; tag it re-creates. Absent → the workspace's own site key (the site_key of GET /ads/tracking/key, minted on first read); 400 when the workspace has none and no origin to mint one for.

Same entitlement gate as GET /ads/tracking/key. Admin-scoped (ads:read).

ParameterInTypeRequiredConstraints
site_idquerystringnomín. 1
curl https://api.vitrinadev.com/api/v1/ads/tracking/installers/gtm \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "key_id": "abababab-0000-4000-8000-000000000001",
    "public_key": "trk_live_abc123",
    "site_key": "pk_live_sitekey000000000000",
    "tag_name": "Vitrina Ads · tag",
    "trigger": "All Pages",
    "snippet": "<script src=\"https://api.vitrinadev.com/tag.js?site=pk_live_sitekey000000000000\" async></script>",
    "setup_checklist": [
      "En Google Tag Manager, crea una etiqueta nueva de tipo «HTML personalizado».",
      "Pega el fragmento de Vitrina (tag.js) tal cual.",
      "Activador: All Pages.",
      "Guarda la etiqueta y publica el contenedor.",
      "El asistente y la medición se cargan desde este mismo tag.js: no agregues otro fragmento de Vitrina aparte."
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/tracking/key

The workspace's tracking key and the tag to paste

Ensures an active tracking key exists for the workspace (minting one on the first call — idempotent, a repeat never mints a second key) and returns the tag to paste. combined_snippet is THE tag: Vitrina's one-line loader (&lt;script src=".../tag.js?site=pk_…"&gt;), which boots the assistant and the measurement from the same tag. Its site key (site_key) is the newest unexpired publishable key of the workspace, or — when it has none — one minted on this read for the workspace's Vitrina site, registered domains and declared website, and listed afterwards among its publishable keys. combined_snippet and site_key are null only when no origin can be derived at all.

snippet is the measurement provider's raw tracker script (the tracking half alone), kept for API consumers that self-host it; Vitrina's own UI renders combined_snippet only. meta_pixel_id is present when the workspace has a Meta pixel configured.

402 ENTITLEMENT_NOT_ACTIVE (feature: vitrina_ads) when Vitrina Ads is not active for this workspace; 409 ADS_KEY_NEEDS_REMINT when the Add-on is on but the delegated key has not finished rotating. Admin-scoped (ads:read).

curl https://api.vitrinadev.com/api/v1/ads/tracking/key \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "key_id": "abababab-0000-4000-8000-000000000001",
    "public_key": "trk_live_abc123",
    "snippet": "<script src=\"https://track.example.com/t.js?k=trk_live_abc123\" async></script>",
    "combined_snippet": "<script src=\"https://api.vitrinadev.com/tag.js?site=pk_live_sitekey000000000000\" async></script>",
    "site_key": "pk_live_sitekey000000000000",
    "meta_pixel_id": null
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/tracking/status

Is the tag installed? Last event seen, events in 24 h, domain

Tag detection for the workspace's tracking key. source: tracker (the normal case) reads the measurement provider's per-key last-event time (last_seen_at), the tracker events of the last 24 hours (events_24h) and the hostname of the latest verified install check (domain). installed is true only when the tag sent an event in the last 30 days — never without a timestamp — and it STAYS true for 30 days after the last event, so it does not mean «live»: reporting does (events in the last 24 hours).

When the per-key read is unavailable the answer falls back to the health read's daily UTM scan: source: health, installed = the refreshed scan counted events, reporting: false (unknown), last_seen_at / events_24h / domain = null. events_24h is null (never 0) whenever the count could not be read.

Same entitlement gate as GET /ads/tracking/key, except that sample=1 (or a sandbox workspace) answers a fixed sample status (installed: true, reporting: true, seen minutes ago, source: tracker) without the entitlement and without reading the real tag — like every other Ads read. Admin-scoped (ads:read).

booking_widget is the clinic booking widget’s own install check (#3709): seen when a page loads the widget with the clinic’s pk_ key; null when the workspace has no clinic widget to install.

ParameterInTypeRequiredConstraints
samplequery1 \trueno
curl https://api.vitrinadev.com/api/v1/ads/tracking/status \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "installed": true,
    "reporting": true,
    "last_seen_at": "2026-09-23T14:02:11.000Z",
    "events_24h": 184,
    "domain": "www.clinica-ejemplo.cl",
    "source": "tracker"
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/venta/chileautos-cost

What Chileautos costs

The dealer’s Chileautos cost as a dated history: each entry is an amount every N months, in force from its first day until the next entry. The comparison prices each past period with the cost in force then.

curl https://api.vitrinadev.com/api/v1/ads/venta/chileautos-cost \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "portal": "chileautos",
    "current": {
      "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
      "amount_clp": 600000,
      "period_months": 2,
      "monthly_clp": 300000,
      "effective_from": "2026-09-01",
      "effective_to": null,
      "note": "Plan bimestral",
      "created_at": "2026-10-27T15:00:00.000Z"
    },
    "entries": [
      {
        "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
        "amount_clp": 600000,
        "period_months": 2,
        "monthly_clp": 300000,
        "effective_from": "2026-09-01",
        "effective_to": null,
        "note": "Plan bimestral",
        "created_at": "2026-10-27T15:00:00.000Z"
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

PUT /ads/venta/chileautos-cost

Record what Chileautos costs

Records the Chileautos cost from a day on. A price change is a new entry with a later effective_from; the same effective_from corrects that entry. amount_clp 0 records that the dealer stopped paying.

Body

FieldTypeRequiredConstraints
amount_clpintegeryes≥ 0, ≤ 10000000000
period_monthsintegeryes≥ 1, ≤ 12
effective_fromstringyespatrón ^\d{4}-\d{2}-\d{2}$
notestring | null—máx. 300
curl -X PUT https://api.vitrinadev.com/api/v1/ads/venta/chileautos-cost \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_clp": 600000,
    "period_months": 2,
    "effective_from": "2026-09-01"
  }'

Example response (200)

{
  "data": {
    "portal": "chileautos",
    "current": {
      "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
      "amount_clp": 600000,
      "period_months": 2,
      "monthly_clp": 300000,
      "effective_from": "2026-09-01",
      "effective_to": null,
      "note": "Plan bimestral",
      "created_at": "2026-10-27T15:00:00.000Z"
    },
    "entries": [
      {
        "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
        "amount_clp": 600000,
        "period_months": 2,
        "monthly_clp": 300000,
        "effective_from": "2026-09-01",
        "effective_to": null,
        "note": "Plan bimestral",
        "created_at": "2026-10-27T15:00:00.000Z"
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

DELETE /ads/venta/chileautos-cost/{id}

Remove a Chileautos cost entry

Removes one mistaken entry; the previous entry then stays in force until the next one.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X DELETE https://api.vitrinadev.com/api/v1/ads/venta/chileautos-cost/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "portal": "chileautos",
    "current": {
      "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
      "amount_clp": 600000,
      "period_months": 2,
      "monthly_clp": 300000,
      "effective_from": "2026-09-01",
      "effective_to": null,
      "note": "Plan bimestral",
      "created_at": "2026-10-27T15:00:00.000Z"
    },
    "entries": [
      {
        "id": "2b1f5c8e-6c1d-4f7a-9d55-0f3e2a9b7c41",
        "amount_clp": 600000,
        "period_months": 2,
        "monthly_clp": 300000,
        "effective_from": "2026-09-01",
        "effective_to": null,
        "note": "Plan bimestral",
        "created_at": "2026-10-27T15:00:00.000Z"
      }
    ]
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/venta/comparison

Meta car ads vs Chileautos

Cost per lead, per visit booked and per sale of Chileautos (the entered cost, prorated per day) against Meta car ads («Venta de un auto» Lanzamientos, ad spend) over the same period, and for the cars that had both. Each step counts what happened in the period. A cost is null when unknown and a cost per step is null when its count is zero; counts are never null. Until a «Venta de un auto» Lanzamiento exists the Meta side reads no_lanzamientos with no funnel.

ParameterInTypeRequiredConstraints
fromquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
toquerystringyespatrón ^\d{4}-\d{2}-\d{2}$
curl https://api.vitrinadev.com/api/v1/ads/venta/comparison \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "period": {
      "from": "2026-10-01",
      "to": "2026-10-31",
      "timezone": "America/Santiago"
    },
    "chileautos": {
      "cost_status": "ok",
      "cost_from": "2026-09-01",
      "funnel": {
        "cost": 300000,
        "leads": 42,
        "visits_booked": 9,
        "sales": 2,
        "cost_per_lead": 7143,
        "cost_per_visit": 33333,
        "cost_per_sale": 150000
      }
    },
    "meta": {
      "status": "no_lanzamientos",
      "spend_status": null,
      "lanzamientos": 0,
      "funnel": null
    },
    "same_cars": {
      "status": "no_lanzamientos",
      "vehicle_ids": [],
      "chileautos_listed_cars": 18,
      "chileautos": null,
      "meta": null
    },
    "sold_cars": 5,
    "sold_cars_unattributed": 3
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/venta/lanzamientos

List car ads

One row per car Lanzamiento: the car (and whether it is still available), its ad (on, or paused and why: the car was sold, reserved or hidden, or a price change replaced it), whether automatic steps skip it because it was edited outside Vitrina, the latest price-change swap, and the cap.

ParameterInTypeRequiredConstraints
statusquerydraft \building \paused_ready \
limitqueryintegerno≥ 1, ≤ 200
curl https://api.vitrinadev.com/api/v1/ads/venta/lanzamientos \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "lanzamientos": [],
    "cap": {
      "running": 2,
      "max": 10,
      "remaining": 8
    }
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/venta/lanzamientos

Advertise cars (one Lanzamiento per car)

Creates one «Venta de un auto» Lanzamiento per car and builds it on Meta in the background: one ABO campaign with ONE click-to-WhatsApp ad set at the car's daily budget (default 10 000 CLP) and one ad showing the car's 4:5 render. EVERYTHING IS CREATED PAUSED; confirm each with POST /ads/lanzamientos/{id}/confirm (a first Lanzamiento waits for Vitrina's review). The chat the ad opens carries the car: the agent sends its photos and price and books the visit. At most 10 cars run at once per workspace. 400 with problems and cars[].problems when any car or the account refuses (all or nothing); a car a concurrent request took first is listed in refused.

Body

FieldTypeRequiredConstraints
carsobject[]yes
messaging_account_iduuidyes
region_codesstring[]—
page_idstring—máx. 40
dataset_idstring—máx. 40
curl -X POST https://api.vitrinadev.com/api/v1/ads/venta/lanzamientos \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cars": [
      {
        "vehicle_id": "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "creative_set_id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
        "daily_budget_clp": 10000
      }
    ],
    "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5"
  }'

Example response (202)

{
  "data": {
    "lanzamientos": [],
    "refused": []
  }
}

Answers: 202 · 400 · 401 · 402 · 403 · 404 · 409 · 429

POST /ads/venta/preflight

Check car ads before creating them

Per car: is it sellable and priced, is it already advertised, is its render set done and at the current price, is the daily budget within bounds and Meta's minimum. For the account: Pages, dataset, WhatsApp number, geo and the spending limit against the total of the cars. Reads only.

Body

FieldTypeRequiredConstraints
carsobject[]yes
messaging_account_iduuidyes
region_codesstring[]—
page_idstring—máx. 40
dataset_idstring—máx. 40
curl -X POST https://api.vitrinadev.com/api/v1/ads/venta/preflight \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cars": [
      {
        "vehicle_id": "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "creative_set_id": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
        "daily_budget_clp": 10000
      }
    ],
    "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5"
  }'

Example response (200)

{
  "data": {
    "cars": [
      {
        "vehicle_id": "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "daily_budget_clp": 10000,
        "problems": []
      }
    ],
    "daily_total_clp": 10000,
    "cap": {
      "running": 2,
      "max": 10,
      "remaining": 8
    },
    "ad_account": {
      "readable": true,
      "ad_account_id": "act_100000000000001",
      "currency": "CLP",
      "daily_spending_limit_clp": null,
      "limit_status": "none",
      "spend_today_clp": 0,
      "exceeds_limit": false,
      "min_daily_budget_clp": 1000,
      "warning": null
    },
    "pages": [
      {
        "page_id": "100000000000002",
        "name": "Tu Automotora"
      }
    ],
    "page_id": "100000000000002",
    "dataset": {
      "dataset_id": "100000000000003",
      "selectable": []
    },
    "whatsapp": {
      "messaging_account_id": "7c2d3e4f-5a6b-4c7d-8e9f-a0b1c2d3e4f5",
      "phone_known": true,
      "page_link": "linked",
      "page_link_warning": null
    },
    "geo": {
      "region_codes": [
        "13"
      ],
      "label": "RM",
      "location_types": [
        "home"
      ],
      "supported": true
    },
    "problems": []
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

GET /ads/venta/suggestions

Suggest stock cars to advertise

Ranks the stock cars that can be advertised (disponible, active, priced, with photos, not already advertised) and says why: aging stock (≥ 45 days), many storefront views but few leads (30 days), a high expected margin (only where the purchase cost is recorded) and niche or premium cars. A signal the tenant has no data for is listed in signals_unavailable, never guessed. Also returns the cap of cars running at once.

ParameterInTypeRequiredConstraints
limitqueryintegerno≥ 1, ≤ 50
curl https://api.vitrinadev.com/api/v1/ads/venta/suggestions \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "suggestions": [
      {
        "vehicle": {
          "id": "6a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
          "make": "Toyota",
          "model": "RAV4",
          "version": "2.0 LE 4x2",
          "year": 2020,
          "price_clp": 17990000,
          "odometer_value": 62000,
          "cover_photo": "https://cdn.example.com/rav4.jpg",
          "photos_count": 14
        },
        "days_in_stock": 71,
        "views_30d": 48,
        "leads_30d": 1,
        "expected_margin_share": null,
        "score": 101.1,
        "reasons": [
          {
            "code": "aging",
            "label": "Lleva 71 días en stock"
          },
          {
            "code": "many_views_few_leads",
            "label": "48 visitas en tu sitio en 30 días y solo 1 consulta"
          }
        ]
      }
    ],
    "signals_unavailable": [
      {
        "signal": "margin",
        "reason": "Ningún auto tiene registrado cuánto se pagó por él (costo de compra), así que no podemos estimar el margen."
      }
    ],
    "eligible_without_signal": 6,
    "cap": {
      "running": 2,
      "max": 10,
      "remaining": 8
    },
    "default_daily_budget_clp": 10000
  }
}

Answers: 200 · 400 · 401 · 402 · 403 · 404 · 409 · 429

On this page

GET /ads/actionsPOST /ads/actions/{key}/dismissPOST /ads/actions/{key}/executePOST /ads/actions/{key}/previewGET /ads/actions/executionsGET /ads/actions/executions/{id}POST /ads/actions/executions/{id}/rollbackGET /ads/ads/{id}GET /ads/ads/{id}/videoGET /ads/attributed-salesDELETE /ads/audiences/customer-listGET /ads/audiences/customer-listPOST /ads/audiences/customer-listGET /ads/booking-rateGET /ads/briefingGET /ads/campaignsGET /ads/cohortsPUT /ads/consignaciones/condicionesGET /ads/consignaciones/configGET /ads/consignaciones/landingPUT /ads/consignaciones/landingGET /ads/consignaciones/metricsPUT /ads/consignaciones/ofertaPUT /ads/consignaciones/responsablesPUT /ads/consignaciones/stock-targetGET /ads/conversion-leadsPUT /ads/conversion-leadsGET /ads/conversion-sourceGET /ads/conversion-sync/catalogGET /ads/conversion-sync/connections/{id}/pixelsGET /ads/conversion-sync/data-sourcesPOST /ads/conversion-sync/data-sources/instagram/ensurePOST /ads/conversion-sync/data-sources/instagram/turn-offPOST /ads/conversion-sync/data-sources/website/create-pixelPOST /ads/conversion-sync/data-sources/website/ensurePOST /ads/conversion-sync/data-sources/whatsapp/ensurePOST /ads/conversion-sync/data-sources/whatsapp/turn-offGET /ads/conversion-sync/destinationsPOST /ads/conversion-sync/destinationsGET /ads/conversion-sync/messaging-event-namesPATCH /ads/conversion-sync/messaging-event-namesGET /ads/conversion-sync/preview-eventPOST /ads/conversion-sync/rules/syncPOST /ads/conversion-sync/setupGET /ads/conversion-sync/statusPOST /ads/conversion-sync/testGET /ads/conversion-sync/wiringPOST /ads/conversion-sync/wiring/recheckGET /ads/creative-assetsDELETE /ads/creative-assets/{id}POST /ads/creative-assets/{id}/completePOST /ads/creative-assets/uploadsGET /ads/creativesGET /ads/experimentsPOST /ads/experimentsGET /ads/experiments/{id}PATCH /ads/experiments/{id}GET /ads/experiments/{id}/readoutGET /ads/experiments/currentGET /ads/feedGET /ads/feed/{id}/journeyGET /ads/funnelDELETE /ads/goalGET /ads/goalPUT /ads/goalGET /ads/healthGET /ads/lanzamiento-proposalsGET /ads/lanzamiento-proposals/{id}POST /ads/lanzamiento-proposals/{id}/acceptPOST /ads/lanzamiento-proposals/{id}/dismissGET /ads/lanzamientosPOST /ads/lanzamientosGET /ads/lanzamientos/{id}POST /ads/lanzamientos/{id}/confirmGET /ads/lanzamientos/{id}/metricsPOST /ads/lanzamientos/{id}/rollbackGET /ads/lanzamientos/car-creative-setsPOST /ads/lanzamientos/car-creative-setsGET /ads/lanzamientos/car-creative-sets/{set_id}GET /ads/lanzamientos/creative-setsPOST /ads/lanzamientos/creative-setsGET /ads/lanzamientos/creative-sets/{set_id}GET /ads/lanzamientos/creative-sets/{set_id}/videosPOST /ads/lanzamientos/creative-sets/{set_id}/videosPOST /ads/lanzamientos/preflightGET /ads/leadgen-formsDELETE /ads/leadgen-forms/{form_id}PUT /ads/leadgen-forms/{form_id}GET /ads/leadgen-submissionsGET /ads/lineasGET /ads/measurementGET /ads/meta-sendGET /ads/meta-send/comparisonGET /ads/monthly-returnGET /ads/overviewGET /ads/paid-funnelGET /ads/peopleGET /ads/proyectosGET /ads/reportGET /ads/report/exportGET /ads/scorecardGET /ads/share/resumenGET /ads/share/resumen.pngGET /ads/stagesGET /ads/stateGET /ads/tracking/gtmPOST /ads/tracking/gtm/connectPOST /ads/tracking/gtm/connect/completeDELETE /ads/tracking/gtm/connectionPUT /ads/tracking/gtm/containerGET /ads/tracking/gtm/containersPOST /ads/tracking/gtm/installPOST /ads/tracking/gtm/previewPOST /ads/tracking/gtm/publishPOST /ads/tracking/gtm/uninstallGET /ads/tracking/installers/gtmGET /ads/tracking/keyGET /ads/tracking/statusGET /ads/venta/chileautos-costPUT /ads/venta/chileautos-costDELETE /ads/venta/chileautos-cost/{id}GET /ads/venta/comparisonGET /ads/venta/lanzamientosPOST /ads/venta/lanzamientosPOST /ads/venta/preflightGET /ads/venta/suggestions