KicketKicket
Mi cuenta
Guía · Autenticación▾
  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

API de Kicket · v1

  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

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.

ParteFormaPara qué
client_idkc_live_ + 20 caracteres (kc_test_ en prueba)Identifica la credencial. No es secreto, pero tampoco lo publiques.
client_secretkk_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.

Pedir el token con curl
# 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
Respuesta
{
  "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>.
  • scope es 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 error invalid_scope: no se recorta en silencio.
  • Los errores de esta ruta tienen el formato de OAuth ({ error, error_description }). Una credencial mala o revocada es invalid_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.

PermisoQué permiteRutas
branches:readLeer las sucursales de la cuentaGET /branches
sales:readLeer ventas con sus ítems y pagosGET /salesGET /sales/{id}GET /reports/daily-sales
payments:readLeer pagos, medios de pago y devolucionesGET /paymentsGET /payment-methodsGET /refunds
catalog:readLeer productos, combos y categoríasGET /productsGET /products/{id}GET /categories
catalog:writeCambiar el código externo de un producto o comboPATCH /products/{id}
customers:readLeer clientes con teléfono y mail (datos personales)GET /customersGET /customers/{id}y include=customer en las ventas
cash:readLeer las cajas cerradas con su arqueoGET /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_client y 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.

Cliente mínimo con el token en caché
// 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-Id de 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.
AnteriorPrimeros pasosSiguientePaginación y sincronización