VitrinaAPI

Webhooks

Suscripciones a los eventos del workspace, y el registro de cada entrega.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

Descarga la proyección completa de la API pública: openapi.json.

Recibir eventos firmados explica este recurso en prosa, con ejemplos ejecutables.

MétodoRutaQué hace
GET/webhooksList webhook subscriptions
POST/webhooksCreate a webhook subscription (returns the plaintext secret once)
DELETE/webhooks/{id}Delete a webhook subscription
GET/webhooks/{id}Get a webhook subscription
PUT/webhooks/{id}Update a webhook subscription
GET/webhooks/{id}/deliveriesList webhook delivery attempts
POST/webhooks/{id}/deliveries/{deliveryId}/redeliverRedeliver one webhook delivery
POST/webhooks/{id}/resumeResume an auto-paused webhook subscription
GET/webhooks/eventsThe webhook event catalogue and the signing contract

GET /webhooks

List webhook subscriptions

Every subscription in the workspace, newest first, with its secret redacted to the first nine characters. Health is last_status + the two failure streaks; paused_at set means the platform AUTO-PAUSED it (20 failed deliveries in a row, or 24 h failing) and it receives nothing until POST /webhooks/\{id\}/resume.

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

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": "d1d1d1d1-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "url": "https://example.com/hooks/vitrina",
      "secret": "whsec_d1d…",
      "events": [
        "lead.created",
        "sla.breached"
      ],
      "enabled": true,
      "description": "ERP sync",
      "created_at": "2026-09-20T01:41:04.274Z",
      "updated_at": "2026-09-20T01:41:04.274Z",
      "last_delivery_at": "2026-09-20T01:42:12.154Z",
      "last_status": 200,
      "consecutive_failures": 0,
      "owner_kind": "api_key",
      "owner_id": "c1c1c1c1-0000-4000-8000-000000000001",
      "include_data": true,
      "paused_at": null,
      "paused_reason": null,
      "failing_since": null,
      "consecutive_failed_deliveries": 0
    }
  ]
}

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

POST /webhooks

Create a webhook subscription (returns the plaintext secret once)

Subscribes url to events (names from GET /webhooks/events, up to twenty, or "*" for all — including events added later). The caller becomes the subscription's OWNER.

include_data defaults to false: every delivery is the notice (resource, changes, author, time, url) with data_omitted: "not_requested". With true, data is added when, at dispatch time, the owner holds the event's read_scope and sees every record — otherwise data_omitted names the reason — and never for a sensitive event.

The response is the ONLY one carrying the full whsec_… secret. The URL must resolve to a public address (private, loopback and metadata hosts are refused with 400).

curl -X POST https://api.vitrinadev.com/api/v1/webhooks \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/vitrina",
    "events": [
      "lead.created",
      "sla.breached"
    ],
    "description": "ERP sync"
  }'

Ejemplo de respuesta (201)

{
  "data": {
    "id": "d1d1d1d1-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "url": "https://example.com/hooks/vitrina",
    "secret": "whsec_d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
    "events": [
      "lead.created",
      "sla.breached"
    ],
    "enabled": true,
    "description": "ERP sync",
    "created_at": "2026-09-20T01:41:04.274Z",
    "updated_at": "2026-09-20T01:41:04.274Z",
    "last_delivery_at": null,
    "last_status": null,
    "consecutive_failures": 0,
    "owner_kind": "api_key",
    "owner_id": "c1c1c1c1-0000-4000-8000-000000000001",
    "include_data": false,
    "paused_at": null,
    "paused_reason": null,
    "failing_since": null,
    "consecutive_failed_deliveries": 0
  }
}

Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /webhooks/{id}

Delete a webhook subscription

Removes the subscription and its delivery log. The secret goes with it; rotating a secret is delete-and-recreate.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
curl -X DELETE https://api.vitrinadev.com/api/v1/webhooks/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Responde: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /webhooks/{id}

Get a webhook subscription

One subscription, secret redacted. 404 for an id in another workspace.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
curl https://api.vitrinadev.com/api/v1/webhooks/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "d1d1d1d1-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "url": "https://example.com/hooks/vitrina",
    "secret": "whsec_d1d…",
    "events": [
      "lead.created",
      "sla.breached"
    ],
    "enabled": true,
    "description": "ERP sync",
    "created_at": "2026-09-20T01:41:04.274Z",
    "updated_at": "2026-09-20T01:41:04.274Z",
    "last_delivery_at": null,
    "last_status": null,
    "consecutive_failures": 0,
    "owner_kind": "api_key",
    "owner_id": "c1c1c1c1-0000-4000-8000-000000000001",
    "include_data": false,
    "paused_at": null,
    "paused_reason": null,
    "failing_since": null,
    "consecutive_failed_deliveries": 0
  }
}

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

PUT /webhooks/{id}

Update a webhook subscription

Partial: send only what changes (at least one field). enabled: false stops deliveries and discards queued ones.

Changing url, events or include_data makes the caller the OWNER. Otherwise anyone with webhooks:write could point a data-carrying subscription at their own server and read what their role cannot. enabled and description leave the owner as it is.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
urlstring—uri, máx. 2048
events* \ai_agent.publish \ai_agent_graph.publish \
enabledboolean—
descriptionstring | null—máx. 500
include_databoolean—
curl -X PUT https://api.vitrinadev.com/api/v1/webhooks/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }'

Ejemplo de respuesta (200)

{
  "data": {
    "id": "d1d1d1d1-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "url": "https://example.com/hooks/vitrina",
    "secret": "whsec_d1d…",
    "events": [
      "lead.created",
      "sla.breached"
    ],
    "enabled": false,
    "description": "ERP sync",
    "created_at": "2026-09-20T01:41:04.274Z",
    "updated_at": "2026-09-20T01:41:04.274Z",
    "last_delivery_at": null,
    "last_status": null,
    "consecutive_failures": 0,
    "owner_kind": "api_key",
    "owner_id": "c1c1c1c1-0000-4000-8000-000000000001",
    "include_data": false,
    "paused_at": null,
    "paused_reason": null,
    "failing_since": null,
    "consecutive_failed_deliveries": 0
  }
}

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

GET /webhooks/{id}/deliveries

List webhook delivery attempts

One row per ATTEMPT, newest first — the first place to look when "the webhooks don't arrive". error is http_&lt;code&gt;, timeout or the network error; response_excerpt is the first 500 characters of what the endpoint answered; request_payload is the exact body sent. A manual redelivery appears as a new row with redelivery_of set.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 0, por defecto 0
curl https://api.vitrinadev.com/api/v1/webhooks/<id>/deliveries \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": [
    {
      "id": 54,
      "subscription_id": "d1d1d1d1-0000-4000-8000-000000000001",
      "event": "lead.created",
      "event_id": "99999999-0000-4000-8000-000000000001",
      "attempt": 1,
      "status_code": 200,
      "error": null,
      "response_excerpt": "{\"ok\":true}",
      "request_payload": {
        "id": "99999999-0000-4000-8000-000000000001",
        "type": "lead.created",
        "version": 1,
        "created_at": "2026-09-20T01:42:11.194Z",
        "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
        "resource": {
          "type": "lead",
          "id": "99999999-0000-4000-8000-000000000002",
          "url": "https://api.vitrinadev.com/api/v1/leads/99999999-0000-4000-8000-000000000002"
        },
        "author": {
          "kind": "api_key",
          "id": "c1c1c1c1-0000-4000-8000-000000000001",
          "name": "Website integration"
        },
        "data": {
          "lead_id": "99999999-0000-4000-8000-000000000002",
          "contact_id": "22222222-0000-4000-8000-000000000001",
          "pipeline_id": "44444444-0000-4000-8000-000000000001",
          "stage_id": "55555555-0000-4000-8000-000000000001",
          "title": "Lead disparado desde el sitio",
          "intent": "buy",
          "source": "website",
          "value_amount": null,
          "value_currency": "CLP",
          "owner_user_id": null,
          "team_id": null
        }
      },
      "latency_ms": 549,
      "created_at": "2026-09-20T01:42:12.116Z",
      "redelivery_of": null
    }
  ],
  "meta": {
    "pagination": {
      "limit": 50
    },
    "offset": 0
  }
}

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

POST /webhooks/{id}/deliveries/{deliveryId}/redeliver

Redeliver one webhook delivery

Sends the delivery's event again — same X-Webhook-Event-Id, a fresh signature, ONE attempt — to the subscription's current url. Returns 202 once queued; the outcome appears in GET /webhooks/\{id\}/deliveries as a new row with redelivery_of = this delivery.

data is decided again, against the owner as it stands NOW: an owner who lost the read scope since gets the notice. Works on an auto-paused subscription (to test a fix before resuming); 409 on a disabled one. Audited as webhooks.redeliver.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
deliveryIdpathintegersí> 0
curl -X POST https://api.vitrinadev.com/api/v1/webhooks/<id>/deliveries/<id>/redeliver \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (202)

{
  "data": {
    "subscription_id": "d1d1d1d1-0000-4000-8000-000000000001",
    "delivery_id": 54,
    "event": "lead.created",
    "event_id": "99999999-0000-4000-8000-000000000001",
    "status": "queued"
  }
}

Responde: 202 · 400 · 401 · 403 · 404 · 409 · 429

POST /webhooks/{id}/resume

Resume an auto-paused webhook subscription

Lifts the platform's AUTO-PAUSE (paused_at), which it sets after 20 failed deliveries in a row — events whose every retry failed — or 24 hours of failures without a single success, and restarts both failure streaks at zero. Events that happened while it was paused were not queued: read the resources to catch up, or redeliver from the delivery log.

Idempotent: resuming a subscription that is not paused returns it unchanged. Audited as webhooks.resume when it actually resumes. Separate from enabled, which is your own switch.

ParámetroEnTipoObligatorioRestricciones
idpathuuidsí
curl -X POST https://api.vitrinadev.com/api/v1/webhooks/<id>/resume \
  -H "Authorization: Bearer $VITRINA_KEY"

Ejemplo de respuesta (200)

{
  "data": {
    "id": "d1d1d1d1-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "url": "https://example.com/hooks/vitrina",
    "secret": "whsec_d1d…",
    "events": [
      "lead.created",
      "sla.breached"
    ],
    "enabled": true,
    "description": "ERP sync",
    "created_at": "2026-09-20T01:41:04.274Z",
    "updated_at": "2026-09-20T01:41:04.274Z",
    "last_delivery_at": null,
    "last_status": null,
    "consecutive_failures": 0,
    "owner_kind": "api_key",
    "owner_id": "c1c1c1c1-0000-4000-8000-000000000001",
    "include_data": false,
    "paused_at": null,
    "paused_reason": null,
    "failing_since": null,
    "consecutive_failed_deliveries": 0
  }
}

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

GET /webhooks/events

The webhook event catalogue and the signing contract

Every event this workspace can dispatch, each with a sentence on what it means, the exact producer condition that fires it, its schema version, the resource_type it is about, the read_scope that unlocks data, whether it is sensitive (always a notice), an informal data_schema and a realistic sample — plus the signing block: the envelope, the two data modes and every data_omitted reason, the four X-Webhook-* headers plus Vitrina-Livemode, the HMAC scheme with verify pseudocode, the replay window, the retry schedule and the auto-pause thresholds.

Read this before hard-coding an event name. POST /webhooks accepts only names that appear here (plus * for everything), so a picker built from this response cannot offer something the create call will reject.

The catalogue is a constant, not tenant state: it is identical for every workspace and changes only when Vitrina ships a new event. Nothing here is secret — it needs webhooks:read because the rest of the family does, not because the content is sensitive.

Idempotency. Every delivery carries X-Webhook-Event-Id, and all retries and redeliveries of one event reuse it — so a bulk first import produces one id per car and a consumer can dedupe on it. Event ids are unique per event, never per attempt.

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

Ejemplo de respuesta (200)

{
  "data": {
    "events": [
      {
        "name": "sending_domain.verified",
        "version": 1,
        "resource_type": "sending_domain",
        "read_scope": "messaging_accounts:read",
        "sensitive": false,
        "description": "A tenant email sending domain finished DKIM verification and can now send email campaigns (ADR 0032).",
        "fires_when": "A sending-domain refresh observes SES flip VerifiedForSendingStatus to true for the first time.",
        "data_schema": {
          "id (string, uuid)": "The verified sending domain",
          "domain (string)": "The domain name"
        },
        "sample": {
          "id": "99999999-0000-4000-8000-000000000005",
          "domain": "mail.cliente.cl"
        }
      }
    ],
    "signing": {
      "envelope": {
        "id (string, uuid)": "Unique event id; use for idempotency",
        "type (string)": "One of the events above",
        "version (integer)": "The schema version of this event type; bumped only on a breaking change to `data`",
        "livemode (boolean)": "`false` when the event came from a sandbox (test-mode) workspace, `true` otherwise. Mirrored on the `Vitrina-Livemode` header.",
        "created_at (string, ISO 8601 timestamp)": "When the event was produced",
        "tenant_id (string, uuid)": "Your workspace id",
        "resource (object)": "{ type, id, url } — what the event is about; GET `url` with your credential to read it",
        "changes (object, optional)": "field → { from, to } for the identifiers and states that changed",
        "author (object)": "{ kind: 'member' | 'api_key' | 'ai_agent' | 'system' | 'contact', id, name, via? } — who did it",
        "data (object, optional)": "The resource payload (see catalog); present only when the subscription includes data and its owner may read it",
        "data_omitted (string, optional)": "Why `data` is not in this delivery; exactly one of `data` / `data_omitted` is present"
      },
      "data_modes": {
        "notice": "The default. Every delivery carries the notice — resource, changes, author, time, url — and `data_omitted: \"not_requested\"`.",
        "data": "«Incluir datos del recurso» (`include_data: true`). `data` is added when, at dispatch time, the subscription owner holds the resource read scope and sees every record; otherwise the delivery is a notice with the reason in `data_omitted`.",
        "not_requested": "The subscription did not ask for data.",
        "sensitive": "The event is about a dato sensible (health data). It is always a notice, whatever the subscription asked for.",
        "missing_scope:<scope>": "The owner does not hold that read scope (e.g. `missing_scope:contacts:read`).",
        "restricted_visibility": "The owner sees only some records (assigned-only, or some sucursales).",
        "owner_unavailable": "The owner is no longer an active member, or its API key was revoked or expired. Recreate the subscription, or edit its URL or events, to make yourself its owner."
      },
      "headers": {
        "X-Webhook-Event": "The event name (e.g. lead.created).",
        "X-Webhook-Event-Id": "Unique event id. Retries reuse this id so you can dedupe.",
        "X-Webhook-Timestamp": "Unix seconds when the signature was generated.",
        "X-Webhook-Signature": "t=<unix>,v1=<hex> — HMAC-SHA256 of `${timestamp}.${rawBody}` with your subscription secret.",
        "Vitrina-Livemode": "`true` or `false`, mirroring the body's `livemode`. Signed as part of the body — verifying the signature verifies this too."
      },
      "signature_scheme": "hmac-sha256",
      "signature_format": "t=<unix>,v1=<hex>",
      "verify_pseudocode": [
        "const [ts, sig] = parseHeader('X-Webhook-Signature');  // 't=…,v1=…'",
        "const expected = hmac_sha256(secret, ts + '.' + rawBody);  // hex",
        "if (!timing_safe_equal(sig.v1, expected)) reject(401);",
        "if (Math.abs(now_unix() - ts) > 300) reject(401);  // 5-min replay window"
      ],
      "timing_window_seconds": 300,
      "retry": {
        "attempts": 5,
        "backoff": "exponential",
        "delays_seconds": [
          5,
          10,
          20,
          40,
          80
        ],
        "note": "All retries reuse the same X-Webhook-Event-Id so your endpoint can be idempotent."
      },
      "auto_pause": {
        "consecutive_failed_deliveries": 20,
        "failing_hours": 24,
        "note": "A subscription is paused when 20 events in a row exhaust their retries, or when it has failed for 24 hours without a single success. The workspace admins are notified once. A paused subscription receives nothing until POST /webhooks/{id}/resume; events in between are not queued — read the resources to catch up, or redeliver from the delivery log."
      }
    }
  }
}

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

En esta página