VitrinaAPI

Usar el SDK de TypeScript

El paquete de TypeScript con el que se llama esta API desde Node.

@vitrina/api escribe en TypeScript las mismas llamadas que Empezar hace con curl. El paquete se genera desde el mismo documento OpenAPI que sirve el backend. El tipo de cada ruta, de cada error y de cada evento lo trae el editor.

Instalación

npm install @vitrina/api

Requiere Node 18 o superior. El paquete lleva el número del release que describe: @vitrina/[email protected] es la API 11.2.0. No todas las versiones de la API tienen paquete en npm. La lista la da el registro: npm view @vitrina/api versions. Cuando falte la que buscas, instala la publicada más cercana y mira Seguir versiones y deprecaciones para saber qué cambió en el medio.

1. Crea el cliente

import { createClient } from '@vitrina/api';

const client = createClient({
  baseUrl: 'https://api.vitrinadev.com/api/v1',
  apiKey: process.env.VITRINA_KEY,
});

apiKey recibe la misma credencial sk_ de Empezar: una API key o un token personal, siempre como Bearer. Si ya administras tus propios tokens, por ejemplo un token de sesión, pasa bearer en su lugar. El cliente acepta como máximo uno de los dos.

2. Lee algo del workspace

client es un cliente openapi-fetch tipado contra el contrato completo. Cada ruta, cada parámetro y cada forma de respuesta vienen del documento que genera la Referencia.

const { data, error } = await client.GET('/locations');

if (error) {
  // `error` ya viene tipado con los códigos del catálogo
  throw error;
}

console.log(data.data[0].name); // "Sucursal Providencia"

La respuesta es el mismo sobre que devuelve el curl:

{
  "data": [
    {
      "id": "6812d9f0-9bed-44b5-91df-4e5b90941f6b",
      "name": "Sucursal Providencia",
      "comuna_code": "13123",
      "region_code": "13",
      "timezone": "America/Santiago",
      "is_active": true
    }
  ]
}

unwrap() hace lo mismo en una línea y lanza un VitrinaApiError en vez de obligarte a revisar error en cada llamada:

import { unwrap } from '@vitrina/api';

const locations = unwrap(await client.GET('/locations'));
console.log(locations.data[0].name);

3. Reconoce una falla

Una sucursal que no existe devuelve el sobre que describe Errores:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Sucursal no encontrada",
    "requestId": "bf44aad4-6554-45ea-95f5-bc215f11d373"
  }
}

El SDK lo convierte en un VitrinaApiError, con code tipado contra el catálogo:

import { unwrap, VitrinaApiError } from '@vitrina/api';

try {
  unwrap(await client.GET('/locations/{id}', { params: { path: { id: missingId } } }));
} catch (err) {
  if (err instanceof VitrinaApiError) {
    console.log(err.status); // 404
    console.log(err.code); // "NOT_FOUND"
    console.log(err.requestId); // para buscar la llamada en nuestros registros
  }
}

err.isNotFound, err.isUnauthorized, err.isForbidden, err.isRateLimited e err.isIdempotencyConflict cubren las fallas más frecuentes. Ramifica por err.code, nunca por err.message. Es el mismo consejo de Errores, ahora con autocompletado.

Un switch sobre err.code necesita default

err.code tiene el tipo ErrorCode | (string & {}). El autocompletado muestra los códigos que existían cuando generaste el paquete, y un release del servidor puede publicar uno nuevo antes de que actualices el SDK.

4. Reintenta sin duplicar

Una escritura que acepta Idempotency-Key la recibe como cualquier otro header tipado:

import { idempotencyKey } from '@vitrina/api';

await client.POST('/pipelines', {
  params: { header: idempotencyKey(`crear-pipeline-${userId}`) },
  body: { name: 'Postventa' },
});

Reenviar la misma llamada con la misma clave devuelve la respuesta original en vez de crear una segunda copia. La misma clave con un cuerpo distinto responde 409 IDEMPOTENCY_KEY_CONFLICT, que err.isIdempotencyConflict reconoce. Cuando no tienes una clave natural a mano, como el id de la operación que la origina, generateIdempotencyKey() genera una.

5. Recorre una lista completa

La API pagina de tres formas distintas, y conviene mirar la ruta antes de escribir el bucle. Dos operaciones llevan cursor: GET /appointments y GET /conversations/{id}/messages. Quince llevan offset y limit, entre ellas /stock, /vehicles y /contacts/search. El resto, /leads incluido, llevan page y page_size.

paginate() recorre las del primer grupo sin que escribas el bucle:

import { paginate } from '@vitrina/api';

for await (const cita of paginate((cursor) =>
  client.GET('/appointments', { params: { query: { cursor, limit: 100 } } }),
)) {
  console.log(cita.id);
}

Mandarle cursor a una ruta que pagina por offset no devuelve la primera página en silencio: no compila.

paginateAll() hace lo mismo y junta todo en un array. Resérvalo para una lista que sepas acotada; una que no lo esté pertenece al for await de arriba.

6. Suscríbete y verifica la firma

La suscripción se crea igual que en Empezar, como cualquier otra escritura:

const subscription = unwrap(
  await client.POST('/webhooks', {
    body: {
      url: 'https://tu-dominio.cl/vitrina',
      events: ['contact.created'],
      include_data: true,
    },
  }),
);

El receptor verifica la firma con client.events.constructEvent(). En un paso hace lo que Verifica la firma hace a mano: valida el HMAC, revisa la ventana de tiempo y devuelve el sobre ya tipado.

app.post('/vitrina', express.raw({ type: 'application/json' }), (req, res) => {
  const event = client.events.constructEvent({
    secret: process.env.VITRINA_WEBHOOK_SECRET!,
    signatureHeader: req.header('X-Webhook-Signature')!,
    rawBody: req.body, // el buffer crudo; ver la Trampa de abajo
  });

  if (event.type === 'contact.created') {
    console.log(event.data?.name);
  }
  res.json({ received: true });
});

constructEvent() lanza WebhookSignatureError en vez de devolver un resultado que se puede olvidar de revisar. Si prefieres el { ok, reason } sin excepción, usa client.events.verifySignature(), sobre la misma firma t=<timestamp>,v1=<hmac>.

Trampa

Con req.body reparseado, toda firma falla

La firma es sobre los bytes que llegaron, y JSON.parse seguido de JSON.stringify produce un string distinto. Monta el receptor con express.raw({ type: 'application/json' }), como arriba, y pasa ese buffer directo a rawBody. Un req.body que express.json() ya reparseó no sirve.

Lee el recurso del evento en una llamada

Cuando la suscripción no pidió include_data, o el evento llega como aviso porque a su dueño le faltó el scope, el sobre trae resource.url y nada en data. event.fetch() lo resuelve con las credenciales de ese mismo cliente:

const event = client.events.constructEvent({ secret, signatureHeader, rawBody });

if (!event.data) {
  const contact = await event.fetch<{ id: string; name: string }>();
  console.log(contact.name);
}

Lanza EventResourceUnavailableError cuando el sobre no trae resource.url: un evento sin un único recurso dueño, o uno cuyo data ya llegó inline.

Los reintentos, la auto-pausa y los dos modos de entrega están en Webhooks.

En esta página