Autenticación
OAuth 2.0 client credentials: tu sistema canjea su credencial por un token de una hora y lo manda en cada pedido.
La credencial
Una credencial es un par client_id + client_secret que crea el dueño de la cuenta en Integraciones › API de Kicket. Tiene un entorno, unos permisos y un nombre. El secreto se muestra una sola vez.
| Parte | Forma | Para qué |
|---|---|---|
client_id | kc_live_ + 20 caracteres (kc_test_ en prueba) | Identifica la credencial. No es secreto, pero tampoco lo publiques. |
client_secret | kk_live_ + 40 caracteres (kk_test_ en prueba) | La clave. Sólo la conocen Kicket (su huella) y tu servidor. |
Pedir el token
POST /oauth/token con grant_type=client_credentials, como formulario (application/x-www-form-urlencoded; se acepta también JSON). La credencial va por Authorization: Basic (recomendado) o en el cuerpo como client_id y client_secret, nunca de las dos formas a la vez.
# Las variables salen del archivo kicket-api.env que bajaste al crear la credencial:
# set -a; source kicket-api.env; set +a
curl -s -X POST "$KICKET_API_URL/oauth/token" \
-u "$KICKET_CLIENT_ID:$KICKET_CLIENT_SECRET" \
-d grant_type=client_credentials{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJrY19saXZlXzhGcTJ6THA0TjdtUjF3WTZ2QjNrIn0.4mQ5…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "branches:read sales:read payments:read catalog:read cash:read"
}- El token dura 3600 segundos (
expires_in). Guardalo en memoria y reusalo; pedí uno nuevo un minuto antes de que venza, o cuando una llamada conteste 401. - Tratalo como un texto opaco: no leas su contenido ni dependas de su forma. Va en cada pedido como
Authorization: Bearer <token>. scopees opcional. Sin él, el token trae todos los permisos de la credencial; con él (separados por espacio), sólo esos. Pedir uno que la credencial no tiene es un errorinvalid_scope: no se recorta en silencio.- Los errores de esta ruta tienen el formato de OAuth (
{ error, error_description }). Una credencial mala o revocada esinvalid_client(401). Ver Errores del token.
Permisos (scopes)
Cada ruta pide un permiso. Si el token no lo tiene, la respuesta es 403 insufficient_scope y el header WWW-Authenticate dice cuál falta. Una credencial nueva trae, si el dueño no elige otra cosa: branches:read, sales:read, payments:read, catalog:read, cash:read.
| Permiso | Qué permite | Rutas |
|---|---|---|
branches:read | Leer las sucursales de la cuenta | GET /branches |
sales:read | Leer ventas con sus ítems y pagos | GET /salesGET /sales/{id}GET /reports/daily-sales |
payments:read | Leer pagos, medios de pago y devoluciones | GET /paymentsGET /payment-methodsGET /refunds |
catalog:read | Leer productos, combos y categorías | GET /productsGET /products/{id}GET /categories |
catalog:write | Cambiar el código externo de un producto o combo | PATCH /products/{id} |
customers:read | Leer clientes con teléfono y mail (datos personales) | GET /customersGET /customers/{id}y include=customer en las ventas |
cash:read | Leer las cajas cerradas con su arqueo | GET /cash-sessions |
GET / no pide ningún permiso: alcanza con un token válido. GET /openapi.json es público.Rotar y revocar
- Rotar secreto (en el panel) le da a la credencial un secreto nuevo, que se muestra una vez. El anterior deja de andar en el acto, y los tokens que ya emitió también (contestan 401 dentro de los 30 segundos). Cargá el secreto nuevo en tu sistema y pedí un token.
- Revocar la apaga para siempre: el canje contesta
invalid_clienty sus tokens, 401. Para seguir, creá otra. - Para cambiar de secreto sin cortar el servicio, creá una segunda credencial, pasá tu sistema a la nueva y después revocá la vieja.
- Si la cuenta deja de tener la API habilitada, los pedidos contestan 403
forbidden.
Un cliente mínimo
Pide el token una vez por hora, lo renueva solo, reintenta una vez con un 401 y espera lo que dice Retry-After con un 429. Lo usan las recetas del resto de la guía.
// kicket.mjs — un cliente mínimo, sin dependencias (Node 18 o más nuevo).
const BASE = process.env.KICKET_API_URL ?? 'https://kicket.club/api/v1'
let token = null // { valor, venceEn }
async function pedirToken(intento = 1) {
const basic = Buffer.from(`${process.env.KICKET_CLIENT_ID}:${process.env.KICKET_CLIENT_SECRET}`).toString('base64')
const res = await fetch(`${BASE}/oauth/token`, {
method: 'POST',
headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ grant_type: 'client_credentials' }),
})
// 429: muchos pedidos de token seguidos (por ejemplo, varios procesos a la vez). Esperá lo que dice Retry-After.
if (res.status === 429 && intento < 5) {
await new Promise((r) => setTimeout(r, (Number(res.headers.get('Retry-After')) || 10) * 1000))
return pedirToken(intento + 1)
}
if (!res.ok) throw new Error(`No se pudo pedir el token: ${res.status} ${await res.text()}`)
const { access_token, expires_in } = await res.json()
// Se renueva un minuto antes de que venza: un token por hora, no uno por pedido.
token = { valor: access_token, venceEn: Date.now() + (expires_in - 60) * 1000 }
return token.valor
}
export async function llamar(ruta, { intentos = 5 } = {}) {
const url = ruta.startsWith('http') ? ruta : `${BASE}${ruta}`
for (let intento = 1; ; intento++) {
const valor = token && Date.now() < token.venceEn ? token.valor : await pedirToken()
const res = await fetch(url, { headers: { Authorization: `Bearer ${valor}` } })
if (res.ok) return res.json()
// 401: el token venció o se rotó el secreto. Uno nuevo y otra vez (una sola).
if (res.status === 401 && intento === 1) {
token = null
continue
}
const reintentable = res.status === 429 || res.status >= 500
if (!reintentable || intento >= intentos) {
const p = await res.json().catch(() => ({}))
throw new Error(`${res.status} ${p.code ?? ''}: ${p.detail ?? res.statusText} (request_id ${p.request_id ?? '?'})`)
}
// 429 trae Retry-After; para un 5xx, espera creciente (2, 4, 8… segundos).
const espera = Number(res.headers.get('Retry-After')) || Math.min(60, 2 ** intento)
await new Promise((r) => setTimeout(r, espera * 1000))
}
}Buenas prácticas
- El secreto nunca en el navegador ni en una app: quien lo tenga lee tus ventas. La API no manda CORS a propósito: un pedido desde una página web falla. Llamala desde tu servidor.
- Guardá el secreto en una variable de entorno o en un gestor de secretos; nunca en el código ni en un repositorio.
- Una credencial por sistema, con sólo los permisos que necesita. Así revocás una sin cortar las otras.
- No escribas el token ni el secreto en tus logs. Lo que sí conviene loguear es el
X-Request-Idde cada respuesta. - Si sospechás que un secreto se filtró, rotalo o revocá la credencial enseguida, desde el panel.
- Podés mandar tu propio
X-Request-Id(de 8 a 128 caracteres: letras, números,.,_,:o-) y vuelve el mismo: sirve para seguir un pedido de punta a punta.