Issue a personal token
When to use a personal token, what it can see, when it expires.
Use a personal token for anything that should see exactly what you see. Use an API key for anything that belongs to the workspace.
Both are an sk_ credential in the same header, and both call the same routes. What differs is whose permissions they carry. An API key has its own written on it the day it's minted. A personal token carries none of its own. Every call resolves against the membership of the person it acts for, at that moment.
| API key | Personal token | |
|---|---|---|
| Whose is it? | The workspace's. Nobody's in particular. | A member's. |
| What does it see? | The whole workspace. | Exactly what that person sees. |
| Who mints it? | Somebody with api_keys:write. | Any member, for themselves. |
| How long does it last? | Until somebody revokes it. | Until it's revoked, expires, or that person loses access. |
A personal token is for your own scripts, your AI client, a spreadsheet that refreshes itself. All of that should see what you see and go dark when you leave the workspace.
An API key is for what belongs to the workspace: the public site, an integration with another system, a nightly job. Those keep running after whoever connected them leaves.
Minting one
The app mints it under "Mi perfil › Tokens personales › Nuevo token". It asks for a name and an expiry, and lets you pick the permissions. The secret is shown exactly once.
No other credential can mint a personal token. Only a signed-in session will do:
{
"error": {
"code": "FORBIDDEN",
"message": "A personal token can only be minted by the member themselves, from a signed-in session",
"requestId": "0e95dbb6-9e26-4e02-aeb8-1283670577fa"
}
}A connected app can't mint one on your behalf either.
The 201 returns this:
{
"id": "a03db697-d779-4610-b2c2-a9fc4ba24302",
"tenant_id": "00000000-0000-4000-8000-000000000001",
"user_id": "cba9acd8-ab10-4b07-ada5-c7d436d1403e",
"name": "Reportes semanales",
"prefix": "sk_t6pgh",
"scopes": ["leads:read", "contacts:read", "conversations:read", "messages:read"],
"created_at": "2026-09-22T08:23:19.679476+00:00",
"last_used_at": null,
"expires_at": "2026-12-21T08:23:19.677+00:00",
"revoked_at": null,
"connector": false,
"secret": "sk_t6pgh…"
}user_id is the member the token acts for. It's the one visible difference from an API key: in GET /api-keys an API key carries that field as null.
secret is shown once. After that only the prefix remains, its first characters, which is enough to recognise it in a list without storing it anywhere.
The permission ceiling
Leave scopes out and the token is born with everything your role holds today. You can take permissions away: asking for less is always accepted. Asking for more isn't.
{
"error": {
"code": "FORBIDDEN",
"message": "You cannot grant a personal token that includes a permission you do not have (billing:write)",
"requestId": "ff27c537-3f3d-4da4-be98-4f9088a2cb2e"
}
}The 403 names the permission you don't hold.
The stored list is a ceiling, reapplied on every call against the permissions your role holds at that moment. If contacts:read comes off your role tomorrow, the token stops reading contacts that same day, with nobody re-minting it. It doesn't work the other way round: a new permission on your role never appears in a token that didn't ask for it.
Trap
Your token sees only the records you see
A personal token inherits your visibility. If your role only reaches the records assigned to you, the token reaches those same ones and not one more. The same holds for locations when the role is narrowed to its own.
That's why a member with narrowed visibility can mint a personal token and can't mint an API key: the API key would see the whole workspace.
The expiry
Say nothing and you get 90 days. You can ask for an exact date, or null for a token that never expires.
{ "name": "Reportes semanales", "expires_at": null }A token with no expiry makes sense for something that runs every day, like the AI connector. For a one-off script, keep the expiry.
An expired token still shows up in the list, with its expires_at in the past.
Listing and revoking
curl https://api.vitrinadev.com/api/v1/personal-tokens \
-H "Authorization: Bearer $VITRINA_TOKEN"Yours, with no special permission. Somebody else's with ?user_id=, which does need api_keys:read, the same permission that listing the workspace's API keys needs. Revoked ones stay out unless you ask for them with include_revoked=true.
curl -X DELETE https://api.vitrinadev.com/api/v1/personal-tokens/a03db697-d779-4610-b2c2-a9fc4ba24302 \
-H "Authorization: Bearer $VITRINA_TOKEN"204, no body. Yours always; somebody else's with api_keys:write. It's immediate: the next call with that secret answers 401.
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"requestId": "3fe5839c-7b53-42b3-aa0a-5fbe1dec107b"
}
}The day that person leaves
Nobody has to remember to revoke anything. Once the membership stops being active, the token stops working on the very next call. It makes no difference whether the account was deactivated, suspended, or the person was removed:
{
"error": {
"code": "UNAUTHORIZED",
"message": "The member this token acts as no longer has access to this workspace",
"requestId": "5bf3efb6-d32d-407e-99f5-262b97ad5e88"
}
}The token's row is left as it was, so this isn't a revocation. Restore the access and the token works again. An API key behaves differently: it outlives whoever minted it because it belongs to nobody.
The AI connector
The browser-less client you connect from "Configuración › Conexiones › MCP" uses a personal token. Its permissions are the connector's, trimmed to yours. You can spot one by connector: true in the list.
So a narrowed member's connector answers what that person would answer. The economics checkbox hands nothing to somebody who can't read costs. The permission never lands on the token, and the response says so with economics: false.
The call allowance
All your personal tokens share one allowance, yours, separate from the one your session in the app uses. A new token doesn't bring a new allowance. The current state travels on every response:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119The contract for the three endpoints is in Reference · Personal tokens.