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/attributed-salesOne campaign’s attributed conversions (the drill-down)
GET/ads/briefingThe briefing: ≤3 sentences about a screen’s figures
GET/ads/campaignsCampaign performance for a window
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/destinationsThe configured export destinations (pre-fills a re-run)
POST/ads/conversion-sync/destinationsCreate or update the Meta CAPI destination
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/wiringWhether the dealer's 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/creativesTop-performing ads (cohort-normalized creative scoring)
GET/ads/feedThe live feed of recorded outcomes
GET/ads/feed/{id}/journeyOne feed row’s path to its outcome
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/overviewAd spend, attributed revenue, ROAS and outcomes for a window
GET/ads/stateThe tenant's Vitrina Ads entitlement and delegated-key state
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/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 a worn-out ad, raise a winning ad set’s budget, re-check the conversion wiring, re-scan the ad links — or, when Meta is not connected, only a nav card to reconnect it. Nothing here runs by itself: preview → confirm → execute.

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).

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}$
includequeryadset_duplicate_swapno
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"
      ],
      "primary": true,
      "title": "Pausar «Video testimonio · 30 s»",
      "why": "Está desgastado: la gente ya lo vio demasiadas veces y **$184.300** de gasto rinde cada vez menos.",
      "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
      },
      "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
      },
      "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
    }
  ]
}

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.

Duplicate-and-swap needs 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).

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 — the consent lines the owner must accept. 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).

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}$
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

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
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"
      },
      "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"
      },
      "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). error.message is Spanish copy ready for the card.

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",
    "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). 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/attributed-sales

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

Proxies Atribu’s campaigns/\{id\}/conversions operation: 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
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

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) or template (deterministic — first week, Meta disconnected, no spend reading, the model slow or rejected). Both read identically. A briefing is cached per facts hash for 24 h, so two tabs and a reload read the same text; when the model misses its budget the template answers and the model briefing is finished in the background for the next load.

lead.figure names the number the lead is built around (display is its exact substring in lead.text). lines[].ask is a suggested follow-up question. 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 \
modelquerystringno
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 36,4% de tus tratamientos vino de anuncios.",
        "fact_keys": [
          "best_campaign_name",
          "best_campaign_roas",
          "ads_share"
        ],
        "ask": "¿Qué campaña me conviene subir?"
      }
    ],
    "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

Proxies Atribu’s campaigns operation (the ranked top-N shape — no level is ever sent, so this is always the campaign rollup, not 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 Atribu 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.

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,
        "top_ad": {
          "external_id": "120212000000000009",
          "name": "Video testimonio 15s",
          "creative_thumbnail_url": "https://cdn.vitrinadev.com/examples/ads/video-testimonio-15s.jpg"
        },
        "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
          }
        ]
      }
    ]
  }
}

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 Atribu. 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/destinations

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

Relayed verbatim from Atribu, 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

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, closed_won→Purchase (event value), and payment_received with its Meta destination DISABLED (cash ledger only). Idempotent: matched to existing rules by definition. 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/wiring

Whether the dealer's ad sets optimize for the events Vitrina sends

Atribu's per-rule verdicts and remediation, verbatim (served from a 6h cache; force=1 re-verifies live — still a read). privacy reports whether a clinic profile carries platform_safe. Requires ads:read.

ParameterInTypeRequiredConstraints
forcequery0 \1 \true \
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",
        "status": "wired",
        "remediation": null
      }
    ],
    "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",
        "status": "wired",
        "remediation": null
      }
    ],
    "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/creatives

Top-performing ads (cohort-normalized creative scoring)

Proxies Atribu’s top-performers operation — 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, a probability, never ROAS) 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 Atribu profile, so it cannot call Atribu’s cross-profile /workspaces/\{id\}/top-performers (that route is session-bearer-only and answers 403 insufficient_scope to any API key).

ParameterInTypeRequiredConstraints
windowquery7d \14d \28d \
limitqueryintegerno> 0, ≤ 100
seriesquery1 \trueno
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,
        "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/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 \appointment_booked \appointment_attended \
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
    },
    {
      "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
    }
  ],
  "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 path to its outcome

The touches that led to one recorded outcome, oldest first, ending at the outcome: ad views and clicks (ad and campaign names), site visits, conversations. From the attribution engine (source: engine) when it credited the outcome and the delegated key may read people; otherwise Vitrina’s own record of this contact’s outcomes (source: vitrina) — still a path, without the ad touches. Never carries a URL, referrer, email or device. 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
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": [
      {
        "kind": "ad",
        "label": "Reel · Antes y después",
        "detail": "Ortodoncia invisible · Septiembre",
        "at": "2026-09-19T21:10:00.000Z",
        "channel": "meta"
      },
      {
        "kind": "click",
        "label": "Reel · Antes y después",
        "detail": "Ortodoncia invisible · Septiembre",
        "at": "2026-09-20T13:02:00.000Z",
        "channel": "meta"
      },
      {
        "kind": "chat",
        "label": "Conversación",
        "detail": null,
        "at": "2026-09-20T13:03:00.000Z",
        "channel": "whatsapp"
      },
      {
        "kind": "outcome",
        "label": "Presupuesto aceptado",
        "detail": null,
        "at": "2026-09-22T19:40:12.000Z"
      }
    ],
    "touch_count": 3,
    "credited_weight": 1,
    "model": "last_touch",
    "source": "engine"
  }
}

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 Atribu operations, so a dashboard panel needs one call, not three:

  • trust — Atribu’s trust operation: 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 — Atribu’s quality/utm operation: 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 — Atribu’s quality/attribution operation: the conversion side — how many of the window’s conversions carry a real ad id, an fbclid only, or nothing, partitioning total_conversions.

⚠ Named /attribution-coverage in this ticket’s original prose — the live Atribu spec names this operation quality/attribution; there is no separate /attribution-coverage path. Same read, corrected name.

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": []
      },
      "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
    }
  }
}

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

GET /ads/overview

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

Proxies Atribu’s overview operation: 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 Atribu’s exact-decimal wire string (ADR 0018) to a number ONCE, at the provider mapper.

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
      }
    ]
  }
}

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 Atribu 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/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.atribu.app/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.

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, 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, seen minutes ago, source: tracker) without the entitlement and without reading the real tag — like every other Ads read. Admin-scoped (ads:read).

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

Example response (200)

{
  "data": {
    "installed": 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

On this page