VitrinaAPI

Use the TypeScript SDK

The TypeScript package that calls this API from Node.

@vitrina/api writes in TypeScript the same calls Get started makes with curl. The package is generated from the same OpenAPI document the backend serves, so your editor knows the type of every route, every error and every event.

Install

npm install @vitrina/api

Requires Node 18 or newer. The package carries the number of the release it describes: @vitrina/[email protected] is API 11.2.0. Not every API version has a package on npm. The registry has the list: npm view @vitrina/api versions. When the one you want is missing, install the nearest published version and check Track versions and deprecations to see what changed in between.

1. Create the client

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

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

apiKey takes the same sk_ credential from Get started: an API key or a personal token, always as a Bearer. If you already manage your own tokens, a session token say, pass bearer instead. The client accepts at most one of the two.

2. Read something from the workspace

client is an openapi-fetch client typed against the whole contract. Every route, every parameter and every response shape come from the document that generates the Reference.

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

if (error) {
  // `error` already carries the catalogue's codes as a type
  throw error;
}

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

What comes back is the envelope the curl returns:

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

unwrap() does the same in one line and throws a VitrinaApiError instead of making you check error at every call site:

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

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

3. Recognise a failure

A location that doesn't exist comes back in the envelope Errors describes:

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

The SDK turns that into a VitrinaApiError, with code typed against the catalogue:

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); // to find this call in our logs
  }
}

err.isNotFound, err.isUnauthorized, err.isForbidden, err.isRateLimited and err.isIdempotencyConflict cover the failures you'll hit most. Branch on err.code, never on err.message. It's the same advice Errors gives, now with autocomplete.

A switch on err.code needs a default

err.code has the type ErrorCode | (string & {}). Autocomplete shows the codes that existed when you generated the package, and a server release can ship a new one before you regenerate the SDK.

4. Retry without duplicating

A write that accepts Idempotency-Key takes it as any other typed header:

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

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

Resending the same call with the same key returns the original response instead of creating a second copy. The same key with a different body answers 409 IDEMPOTENCY_KEY_CONFLICT, which err.isIdempotencyConflict recognises. When you have no natural key at hand, such as the id of the operation that triggers it, generateIdempotencyKey() mints one.

5. Walk a whole list

The API paginates three different ways, so check the route before writing the loop. Two operations take a cursor: GET /appointments and GET /conversations/{id}/messages. Fifteen take offset and limit, among them /stock, /vehicles and /contacts/search. The rest, /leads included, take page and page_size.

paginate() walks the first group, with no loop to write by hand:

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

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

Passing cursor to a route that paginates by offset does not quietly return the first page. It does not compile.

paginateAll() does the same and collects everything into an array. Keep it for a list you know is bounded; one that isn't belongs on the for await above.

6. Subscribe and verify the signature

The subscription is created the way Get started creates it, as any other write:

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

The receiver verifies the signature with client.events.constructEvent(). In one step it does what Verify the signature does by hand: checks the HMAC, checks the time window, and returns the envelope already typed.

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, // the raw buffer; see the Trap below
  });

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

constructEvent() throws WebhookSignatureError instead of returning a result you could forget to check. If you'd rather have the exception-free { ok, reason }, use client.events.verifySignature(), over the same t=<timestamp>,v1=<hmac> signature.

Trap

With a re-parsed req.body, every signature fails

The signature is over the bytes that arrived, and JSON.parse followed by JSON.stringify produces a different string. Mount the receiver with express.raw({ type: 'application/json' }), as above, and pass that buffer straight to rawBody. A req.body that express.json() re-parsed won't do.

Read the event's resource in one call

When the subscription didn't ask for include_data, or the event arrives as a notice because its owner was missing the scope, the envelope carries resource.url and nothing in data. event.fetch() resolves it with that same client's credentials:

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

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

It throws EventResourceUnavailableError when the envelope carries no resource.url: an event with no single owning resource, or one whose data already arrived inline.

Retries, the auto-pause and the two delivery modes are in Webhooks.

On this page