VitrinaAPI

Marketplaces

Connected portal accounts: their health, their sync history and «Sync now».

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/marketplacesList the workspace’s marketplace accounts
GET/marketplaces/{id}Fetch one marketplace account
GET/marketplaces/{id}/healthPer-job-kind health for the account
POST/marketplaces/{id}/syncTrigger a stock sync
GET/marketplaces/{id}/sync-runsRecent sync runs
GET/marketplaces/yapo/feed-swapYapo 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"

Example response (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"
    }
  ]
}

Answers: 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.

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

Example response (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"
  }
}

Answers: 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.

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

Example response (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"
    }
  ]
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
forceboolean—
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
  }'

Answers: 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.

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

Example response (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"
    }
  ]
}

Answers: 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"

Example response (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
    }
  }
}

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

On this page