Vitrina API

Autenticación y API keys

Qué es una key sk_, qué decide un scope, cómo se emite y se revoca una credencial, y cuántas llamadas por minuto aguanta.

Una sola credencial para toda la API: una key sk_ en el header Authorization.

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

La key no lleva el workspace en la URL ni en un header aparte. El workspace es la key: cada credencial nace pegada a uno y no puede leer ni escribir en otro. Por eso no existe un parámetro de tenant en ninguna ruta, y por eso una key filtrada expone exactamente un workspace.

La forma de una key

sk_ y 43 caracteres. El secreto completo se devuelve una sola vez, al emitirla; después la API solo te muestra su prefix, que son sus primeros ocho caracteres:

{
  "id": "784cecc0-681c-49a1-9188-db9c8326949c",
  "name": "sitio web — lectura de stock",
  "prefix": "sk_t2_O9",
  "scopes": ["stock:read"],
  "created_at": "2026-09-15T19:31:40.643089+00:00",
  "last_used_at": "2026-09-15T19:31:41.846+00:00",
  "revoked_at": null
}

El prefix existe para que puedas reconocer una key en una lista sin guardarla en ninguna parte. Es lo que se muestra en la aplicación y lo que conviene registrar en tu propio sistema.

Guarda el secreto donde guardas los otros secretos. No lo pongas en el front-end: cualquiera que abra el inspector lo tiene, y con él tiene todo el workspace.

Los scopes

Un scope es un permiso con la forma recurso:acción. Una key lleva los suyos en una lista y no tiene ninguno más.

Los cuatro que necesitas para lo que documenta este sitio:

ScopeHabilita
stock:readEl lote público: GET /stock, /stock/count, /stock/{id}.
tenant:readLeer sucursales.
tenant:writeCrear, corregir y desactivar sucursales.
api_keys:writeEmitir y revocar otras keys.

Pedí GET /vehicles con una key que solo tenía stock:read:

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Missing required scope: marketplace:read",
    "requestId": "2c5645c2-d902-4e6f-ad1c-8d924b063ba1"
  }
}

Y ahí está la distinción que más confunde al principio: stock:read no es el stock. Es el lote público — lo que la automotora muestra en su sitio, sin patentes ni precios internos. El inventario por dentro vive en /vehicles y pide otros permisos. Un sitio web nunca debería tener los segundos.

Emitir una key acotada

curl -X POST https://api.vitrinadev.com/api/v1/api-keys \
  -H "Authorization: Bearer $VITRINA_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "sitio web — lectura de stock", "scopes": ["stock:read"] }'

name y scopes son todo lo que pide.

expires_at es opcional y sin él la key no caduca. Con él, la fecha se respeta al pie de la letra: emití una con expires_at de ayer y la primera llamada respondió 401, con un mensaje distinto del de una key revocada:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "API key expired",
    "requestId": "89f197a7-8787-4620-bd8f-981bf2a939e9"
  }
}

Es el único 401 que puedes prevenir mirando tus propias keys antes de que pase.

Trampa

No puedes repartir un permiso que no tienes

Con una key que solo llevaba api_keys:read y api_keys:write, pedir una nueva con stock:read responde 403:

{
  "error": {
    "code": "FORBIDDEN",
    "message": "You cannot grant an API key that includes a permission you do not have (stock:read)",
    "requestId": "1cf632ca-8bf5-438f-b2fa-323277dbff1c"
  }
}

Una credencial no puede emitir otra que la supere. Si vas a tener una key "madre" que emite las demás, tiene que llevar ella misma todos los permisos que va a repartir.

Listar lo que existe

curl https://api.vitrinadev.com/api/v1/api-keys \
  -H "Authorization: Bearer $VITRINA_ROOT_KEY"
{
  "data": [
    {
      "id": "784cecc0-681c-49a1-9188-db9c8326949c",
      "name": "sitio web — lectura de stock",
      "prefix": "sk_t2_O9",
      "scopes": ["stock:read"],
      "last_used_at": "2026-09-15T19:31:41.846+00:00",
      "revoked_at": null
    }
  ],
  "meta": { "total": 6 }
}

last_used_at se actualiza en cada llamada — en mi corrida quedó un segundo después de haber emitido la key y haberla usado. Sirve para lo que parece: encontrar las credenciales que nadie usa antes de revocarlas.

Nunca aparece el secreto, ni acá ni en ningún otro lado.

Revocar

curl -X DELETE https://api.vitrinadev.com/api/v1/api-keys/784cecc0-681c-49a1-9188-db9c8326949c \
  -H "Authorization: Bearer $VITRINA_ROOT_KEY"

204, sin cuerpo. La misma key, usada de nuevo:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid API key",
    "requestId": "d782b2fc-2a45-4bd1-a3b7-1e44a6826ea8"
  }
}

Es inmediato y no se deshace. La fila sigue en la lista con revoked_at puesto, para que quede el registro de qué existió; lo que muere es el secreto.

Repetir el DELETE sobre una key ya revocada también responde 204. Puedes reintentar una revocación sin manejar un caso especial.

Para rotar: emite la nueva, cambia tu configuración, comprueba que la nueva responde, y recién ahí revoca la vieja. No hay un modo "las dos a la vez" porque no hace falta — nada te impide tener dos keys vivas mientras dura el cambio.

El techo de llamadas

Cada respuesta trae el estado de tu cupo:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119

El cupo es por credencial, se repone de forma continua y hoy el techo por defecto es de 120 llamadas por minuto. No lo dejes escrito en tu código: un workspace puede tener un techo distinto, y el valor vigente viaja en ese header en cada respuesta.

Disparé 200 llamadas en paralelo con una key nueva. 130 pasaron y 70 fueron rechazadas así:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
Retry-After: 1
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests",
    "requestId": "c712a422-32d0-4ea2-bba4-e7e60067560a"
  }
}

Retry-After viene en segundos y es el tiempo real hasta que se repone el próximo cupo, calculado por el mismo contador que te rechazó. Espéralo y reintenta; no hagas backoff a ciegas cuando la respuesta ya te dice cuánto.

Lo que sigue

En esta página