Emitir y rotar tus API keys
Qué credenciales existen, qué abre un scope y cómo se revoca una key.
Autentica cada llamada con una key sk_ en el header Authorization.
curl https://api.vitrinadev.com/api/v1/locations \
-H "Authorization: Bearer $VITRINA_KEY"El workspace no viaja en la URL ni en un header aparte. El workspace es la key: cada credencial nace pegada a uno y no alcanza ningún otro. Por eso ninguna ruta lleva un parámetro de tenant, y por eso una key filtrada expone exactamente un workspace.
Una key pertenece al workspace y no a una persona. Ve todo lo que hay y sigue sirviendo cuando quien la emitió se va. Si necesitas una credencial que vea lo mismo que tú y muera contigo, eso es un token personal.
De quién es una key
De nadie en particular, así la integración que la usa no se cae porque alguien cambie de trabajo.
Cuando una persona deja el workspace:
- Sus API keys siguen vivas. Son credenciales del workspace. La factura que se emite sola a las 3 de la mañana sigue emitiéndose.
- Sus tokens personales y las apps conectadas a su nombre se cortan. Esas sí son suyas. Actúan como esa persona, con sus permisos, y sin ella no tienen a quién representar.
- Los administradores reciben un aviso en la campana y por correo. Lista las keys que quedaron y cuándo se usó cada una. No revoca nada: te dice qué quedó sin dueño para que decidas si la rotas, la revocas o la dejas.
Suspender a alguien no es sacarlo del workspace. Mientras está suspendido sus tokens personales responden 401. Cuando lo reactivas vuelven a funcionar con el mismo secreto, sin emitir nada nuevo.
Trampa
Una key sin dueño sigue autenticando con revoked_at: null
Una key que sobrevive a quien la creó no queda marcada de ninguna forma
especial: sigue siendo una fila normal. El aviso a los administradores es el
único lugar donde queda escrito que su creador ya no está. Por eso conviene que
el name de cada key diga para qué sirve y no quién la pidió.
La forma de una key
sk_ y 43 caracteres. El secreto completo se devuelve una sola vez, al emitirla. Después la API solo muestra su prefix, que son sus primeros caracteres:
{
"id": "2e613613-1142-4fea-a979-a9d3f274b07f",
"name": "docs — lectura del workspace",
"prefix": "sk_LV8lX",
"scopes": ["tenant:read"],
"created_at": "2026-09-22T03:16:03.368968+00:00",
"last_used_at": "2026-09-22T03:16:04.211+00:00",
"revoked_at": null,
"rotated_at": null
}El prefix sirve para reconocer una key en una lista sin guardar el secreto en ninguna parte. Es lo que devuelve GET /api-keys, y lo que conviene anotar en tu propio sistema cuando registres para qué era cada credencial.
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
Una key lleva una lista de scopes y ningún permiso fuera de ella. Cada scope se escribe recurso:acción.
Los cuatro con los que parte casi toda integración:
| Scope | Habilita |
|---|---|
tenant:read | Leer el workspace y sus sucursales. |
tenant:write | Crear, corregir y desactivar sucursales. |
webhooks:write | Crear y editar suscripciones a eventos. |
api_keys:write | Emitir y revocar otras keys. |
El catálogo completo se genera desde el backend y está en Scopes. Ahí sale qué operaciones abre cada permiso y qué roles lo llevan.
Pide el scope de la capa que vas a usar
Un permiso que suena a «todo el recurso» abre la capa de afuera. La de adentro pide otro, y ocurre en cada vertical. La capa de afuera es la que un sitio web o un socio puede tener; la de adentro no.
stock:read no es el stock: es el lote público, lo que la automotora muestra
en su sitio, sin patentes ni costos. El inventario por dentro vive en
/vehicles y pide otro permiso. Con una key que solo lleva stock:read:
{
"error": {
"code": "FORBIDDEN",
"message": "Missing required scope: marketplace:read",
"requestId": "79fae21f-cdfc-45ec-bcb4-89295ccce5cf"
}
}Pide siempre el permiso de la capa que vas a usar y nada más. El de adentro no se hereda.
Emitir una key
curl -X POST https://api.vitrinadev.com/api/v1/api-keys \
-H "Authorization: Bearer $VITRINA_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "docs — lectura del workspace", "scopes": ["tenant:read"] }'name y scopes son todo lo que pide.
expires_at es opcional. Sin él la key no caduca; con él, la fecha se respeta al pie de la letra. Una key cuyo expires_at ya pasó responde 401 con un mensaje distinto del de una key revocada:
{
"error": {
"code": "UNAUTHORIZED",
"message": "API key expired",
"requestId": "6b5e929b-99d6-4f35-af6b-dc8ceff9795d"
}
}Es el 401 que puedes prevenir mirando tus propias keys. Los otros dos y qué hacer con cada uno están en Errores.
Trampa
Pedir un scope que tu key no lleva responde 403
Con una key que solo lleva api_keys:read y api_keys:write, emitir una nueva
con tenant:read responde así:
{
"error": {
"code": "FORBIDDEN",
"message": "You cannot grant an API key that includes a permission you do not have (tenant:read)",
"requestId": "cf505046-9b85-424c-b0cf-e76a1ec8ac5c"
}
}Una credencial no puede emitir otra que la supere. La key «madre» que emite las demás tiene que llevar ella misma todos los permisos que va a repartir.
Listar las keys
curl https://api.vitrinadev.com/api/v1/api-keys \
-H "Authorization: Bearer $VITRINA_ROOT_KEY"{
"data": [
{
"id": "2e613613-1142-4fea-a979-a9d3f274b07f",
"name": "docs — lectura del workspace",
"prefix": "sk_LV8lX",
"scopes": ["tenant:read"],
"last_used_at": "2026-09-22T03:16:04.211+00:00",
"revoked_at": null
}
],
"meta": { "total": 6 }
}last_used_at se actualiza en cada llamada. Sirve para encontrar las credenciales que nadie usa antes de revocarlas.
El secreto no aparece aquí ni en ninguna otra respuesta.
Revocar
curl -X DELETE https://api.vitrinadev.com/api/v1/api-keys/2e613613-1142-4fea-a979-a9d3f274b07f \
-H "Authorization: Bearer $VITRINA_ROOT_KEY"204, sin body. La misma key, usada de nuevo:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"requestId": "f1673233-4f65-42ec-b73d-92e85eb1bad6"
}
}Es inmediato y no se deshace. La fila sigue en la lista con revoked_at puesto, para que quede registro de qué existió; lo que muere es el secreto.
Repetir el DELETE sobre una key ya revocada también responde 204, así que puedes reintentar una revocación sin manejar un caso especial.
Rotar
Rotar le cambia el secreto a una key sin cambiar la key. Mismo id, mismo name, mismos scopes; lo único distinto es el secreto y, con él, el prefix.
curl -X POST https://api.vitrinadev.com/api/v1/api-keys/784cecc0-681c-49a1-9188-db9c8326949c/rotate \
-H "Authorization: Bearer $VITRINA_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{ "grace_period_hours": 24 }'{
"data": {
"id": "784cecc0-681c-49a1-9188-db9c8326949c",
"name": "sitio web — lectura del workspace",
"prefix": "sk_9pQ2R",
"scopes": ["tenant:read"],
"secret": "sk_9pQ2R…",
"grace_period_ends_at": "2026-09-23T20:11:31.575Z",
"rotated_at": "2026-09-22T20:11:31.575Z"
}
}El secret nuevo se devuelve una sola vez, igual que al emitirla.
Rotar conserva el id. Todo lo que apuntaba a esa credencial sigue apuntando al mismo lugar: el registro de auditoría, tus propias anotaciones, cualquier permiso concedido contra ese id. Revocar y crear te deja otra credencial con otro id. Cambiar los permisos sí es revocar y crear: los scopes de una key no se editan ni rotando.
grace_period_hours es opcional y va de 0 a 72. Sin él son 24 horas.
Rotar pide api_keys:write y queda auditado. Una key revocada o de otro workspace responde 404.
Trampa
El secreto viejo sigue sirviendo hasta grace_period_ends_at
Rotar no corta el secreto anterior. Sigue autenticando durante el período de
gracia para que puedas redesplegar tu integración sin una ventana en que nada
autentica. Pasada esa fecha responde 401 como cualquier credencial revocada.
Si el secreto se filtró, ese margen juega en tu contra. Rota con
grace_period_hours: 0 y el viejo deja de servir en el mismo instante.
En la aplicación es el botón «Rotar», en Configuración › Desarrolladores › API keys, junto a «Revocar».
El techo de llamadas
Cada respuesta trae el estado de tu cupo:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119El cupo es por credencial y se repone de forma continua. Hoy el techo por defecto es de 120 llamadas por minuto. No lo dejes escrito en tu código: un workspace puede tener otro techo, y el valor vigente viaja en ese header en cada respuesta.
Una llamada que excede el cupo responde 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": "dc856e9a-c913-42ba-b80c-a5a88b74689e"
}
}Retry-After viene en segundos y lo calcula el mismo contador que te rechazó. Espéralo y reintenta; no hagas backoff a ciegas cuando la respuesta ya te dice cuánto.
El contrato de los cuatro endpoints está en Referencia · API keys.