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:
| Scope | Habilita |
|---|---|
stock:read | El lote público: GET /stock, /stock/count, /stock/{id}. |
tenant:read | Leer sucursales. |
tenant:write | Crear, corregir y desactivar sucursales. |
api_keys:write | Emitir 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: 119El 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.