Portales
Las cuentas de portales conectadas: su estado, su historial de sincronización y «Sincronizar».
Descarga la proyección completa de la API pública: openapi.json.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /marketplaces | List the workspace’s marketplace accounts |
GET | /marketplaces/{id} | Fetch one marketplace account |
GET | /marketplaces/{id}/health | Per-job-kind health for the account |
POST | /marketplaces/{id}/sync | Trigger a stock sync |
GET | /marketplaces/{id}/sync-runs | Recent sync runs |
GET | /marketplaces/yapo/feed-swap | Yapo feed swap: ads to replace and progress |
GET /marketplaces
List the workspace’s marketplace accounts
Every connection, with secrets_preview and has_credentials in place of the stored secrets, plus the current verification_status. Unpaginated.
ad_quota (Yapo panel connections, provider: "yapo_panel"): how many ads the dealer’s Yapo plan allows and how many are active, as the last panel sync or panel publish read them: \{ used, allowed, plan_code, plan_name, checked_at \}. used and allowed are null when Yapo states no number (a free account has no stated allowance; the import plan has no cap); plan_code / plan_name are Yapo’s own (U_PA_30 / "Plan automotriz 30"; allowed is the plan’s N). The whole field is null for every other portal and before the first read. A publish to a full plan is refused before anything is posted, with the publication in error: "Tu plan de Yapo permite N avisos activos; quita uno o sube de plan". Vitrina never buys a plan, an extra ad or a highlight.
curl https://api.vitrinadev.com/api/v1/marketplaces \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "33333333-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"auth_type": "oauth2",
"name": "Mercado Libre Automotora Ejemplo",
"environment": "production",
"api_base_url": null,
"token_url": null,
"auth_url": null,
"seller_identifier": "123456789",
"secrets_preview": {
"access_token": "APP_••••9z"
},
"has_credentials": true,
"has_user_login": false,
"enabled": true,
"sync_enabled": true,
"publish_enabled": true,
"sync_interval_minutes": 60,
"verification_status": "verified",
"verification_checked_at": "2026-10-09T08:00:00.000Z",
"verification_error": null,
"ad_quota": null,
"created_at": "2026-09-01T12:00:00.000Z",
"updated_at": "2026-10-09T08:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /marketplaces/{id}
Fetch one marketplace account
The account view: configuration, verification_status, and has_credentials / secrets_preview rather than the secrets themselves (has_user_login says whether a Veekls app user is stored). This is what to poll after a 202 from /verify.
ad_quota (Yapo panel connections, provider: "yapo_panel"): how many ads the dealer’s Yapo plan allows and how many are active, as the last panel sync or panel publish read them: \{ used, allowed, plan_code, plan_name, checked_at \}. used and allowed are null when Yapo states no number (a free account has no stated allowance; the import plan has no cap); plan_code / plan_name are Yapo’s own (U_PA_30 / "Plan automotriz 30"; allowed is the plan’s N). The whole field is null for every other portal and before the first read. A publish to a full plan is refused before anything is posted, with the publication in error: "Tu plan de Yapo permite N avisos activos; quita uno o sube de plan". Vitrina never buys a plan, an extra ad or a highlight.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/marketplaces/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "33333333-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"auth_type": "oauth2",
"name": "Mercado Libre Automotora Ejemplo",
"environment": "production",
"api_base_url": null,
"token_url": null,
"auth_url": null,
"seller_identifier": "123456789",
"secrets_preview": {
"access_token": "APP_••••9z"
},
"has_credentials": true,
"has_user_login": false,
"enabled": true,
"sync_enabled": true,
"publish_enabled": true,
"sync_interval_minutes": 60,
"verification_status": "verified",
"verification_checked_at": "2026-10-09T08:00:00.000Z",
"verification_error": null,
"ad_quota": null,
"created_at": "2026-09-01T12:00:00.000Z",
"updated_at": "2026-10-09T08:00:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /marketplaces/{id}/health
Per-job-kind health for the account
One row per job kind, each carrying both last_run_at and last_success_at.
The distinction is the entire point of this endpoint and must survive into whatever renders it. The account’s older last_sync_* columns conflated every scheduled job into one triple and could only say when something last RAN, which is how a week-long outage stayed invisible: the panel lead scrape stamped those columns every 30 minutes while the stock sync sharing them had been dead for days, and the UI showed a fresh green tick over a dead job. Alert on last_success_at; last_run_at only tells you something is still trying.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/marketplaces/<id>/health \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"accountId": "33333333-0000-4000-8000-000000000001",
"tenantId": "a1a1a1a1-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"name": "Mercado Libre Automotora Ejemplo",
"syncIntervalMinutes": 60,
"jobKind": "stock_sync",
"lastStatus": "success",
"lastRunAt": "2026-10-09T08:00:00.000Z",
"lastSuccessAt": "2026-10-09T08:00:00.000Z",
"lastError": null,
"accountCreatedAt": "2026-09-01T12:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /marketplaces/{id}/sync
Trigger a stock sync
Enqueues the import and answers 202 with \{ enqueued: true, force \}.
The job id is STABLE per account with no timestamp, which makes this idempotent while a run is in flight: a second click, or a click that races the scheduled tick, collapses into the ONE running sync and still answers 202. So a 202 means "a sync is running for this account", not "your sync started". Watch /sync-runs or /health for what actually happened. Retries three times with exponential backoff before the job is abandoned.
force: true is the operator release for an account the inbound drop-guard blocked (ADR 0090 §5): a run whose candidate set is >= 8 units or >= 15% of active origin-owned stock aborts before deactivating anything and records status: "blocked". It stays blocked on every scheduled retry until an operator confirms the drop is real (a dealer clearing their lot, not a feed rotation) and forces ONE run through with this flag — it bypasses only that threshold, never the empty-feed guard.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
force | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/marketplaces/<id>/sync \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"force": false
}'Responde: 202 · 400 · 401 · 403 · 404 · 409 · 429
GET /marketplaces/{id}/sync-runs
Recent sync runs
The last 20 runs — the limit is fixed and not a query parameter. Each row is one attempt with its outcome, which is what to read after a 202 from /sync.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/marketplaces/<id>/sync-runs \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-000000000021",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"job_kind": "stock_sync",
"status": "success",
"started_at": "2026-10-09T08:00:00.000Z",
"finished_at": "2026-10-09T08:00:41.000Z",
"vehicles_imported": 2,
"vehicles_updated": 11,
"vehicles_deactivated": 1,
"error_message": null,
"raw_summary": {},
"created_at": "2026-10-09T08:00:00.000Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /marketplaces/yapo/feed-swap
Yapo feed swap: ads to replace and progress
What connecting the Yapo XML feed would do to the workspace’s Yapo panel ads, for the consent step (vitrina-app#5956). ads_to_replace counts the live panel ads matched to cars still in stock that the feed would replace; consent_required is true when it is above 0, and then message is the sentence to show (Spanish). Send yapo_feed_swap_consent: true with the connect (POST /marketplaces with provider: yapo, or PUT /marketplaces/\{id\} re-enabling a feed).
swaps counts this workspace’s swap rows per state: awaiting_feed_ad (the car’s feed ad is not live yet), deleting_old_ad (seen; the old ad’s delete is being retried), timed_out (48 h without it: the old ad is kept, the dealer was alerted, still watched), swapped (done), old_ad_removed (the car left stock during the swap) and cancelled (nothing to do: old ad already gone, or the feed was disconnected). Read-only.
curl https://api.vitrinadev.com/api/v1/marketplaces/yapo/feed-swap \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"ads_to_replace": 12,
"consent_required": true,
"message": "Al conectar el feed de Yapo se reemplazarán 12 avisos publicados desde el panel.",
"swaps": {
"awaiting_feed_ad": 3,
"deleting_old_ad": 0,
"timed_out": 0,
"swapped": 9,
"old_ad_removed": 0,
"cancelled": 0
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Margen y rentabilidad
El Resumen del negocio de una unidad vendida (en JSON, PDF, Excel o CSV) y la exportación de Margen y rentabilidad: una fila por nota de venta del período, con las cifras del negocio sin IVA y las del comprador con IVA.
Mis notificaciones
Lo que ve quien llama: el feed de notificaciones del workspace activo y sus preferencias de aviso, por preset o celda por celda.