Toki · API

Cobra desde tu sistema. Que paguen con Toki.

Tu caja, tu tienda online o tu ERP crean un cobro con una llamada. Toki devuelve un enlace de pago; tu cliente lo abre, paga desde su billetera y vuelve a tu sitio. El dinero llega a tu cuenta de Toki al instante.

https://api.toki.lat/v1Crear una credencial

Cómo funciona

  • 1. Tu sistema llama a POST /v1/cobros con el monto.
  • 2. Toki responde con un cobro y una url_checkout.
  • 3. Mandas ahí a tu cliente, o la embebes en tu propia página.
  • 4. Él paga con la app de Toki y lo devolvemos a tu url_retorno.
  • 5. Te avisamos por webhook (o consultas el estado del cobro).

Tu credencial pide plata; nunca la cobra. Ninguna llamada a esta API mueve dinero: el cargo lo confirma la persona en su teléfono, desde su propia sesión y su propia billetera. Si tu llave se filtrara, quien la tenga podría generar cobros a tu nombre — molesto, y revocable en un clic — pero no sacarle un peso a nadie.

Autenticación

Cada llamada lleva tu credencial en el header Authorization. El token tiene la forma <key_id>.<secreto>; lo recibes entero al crear la credencial en tu panel, y sólo esa vez.

Authorization: Bearer tk_9f2c8ab1de4057.9d31...c0

En tu panel puedes además restringir desde qué IPs se acepta la credencial. Si la configuras, una llamada desde otra dirección se rechaza aunque el secreto sea correcto. Es la diferencia entre una llave filtrada que sirve desde cualquier parte y una que no sirve fuera de tu servidor.

Límite: 120 llamadas por minuto por credencial.

Moneda

La moneda de todo lo que cobres es la de tu billetera de Toki. No se elige por cobro: un comercio cobra en una moneda, la suya.

Los montos van siempre en la unidad mínima de esa moneda, que es lo estándar en cualquier pasarela. En pesos chilenos el peso no se subdivide, así que 15990 son quince mil novecientos noventa pesos. En una moneda con centavos, 1599 son 15,99.

GET/v1/comercio

Te dice en qué moneda cobras, cómo se llama tu comercio y si la credencial es de prueba o de producción. Úsalo para validar tu configuración sin tener que crear un cobro de mentira.

{ "comercio": { "nombre": "Mi Tienda", "moneda": "CLP", "modo": "prueba" } }

Si tu tienda cobra en otra moneda que tu billetera, no integres todavía. Toki no convierte: cobraría el número que le mandes tratándolo como si fuera de tu moneda. Escríbenos antes.

Crear un cobro

POST/v1/cobros
CampoTipoQué es
montoentero, requeridoEn la unidad mínima de tu moneda. En CLP el peso no se divide: 15990 son quince mil novecientos noventa pesos. En una moneda con centavos, 1599 son 15,99.
conceptotextoLo que ve tu cliente. "Boleta 4471", "Mesa 12".
referencia_externatextoTu identificador. Además hace el cobro idempotente: reintentar con la misma referencia devuelve el cobro que ya existe, en vez de crear otro.
url_retornohttpsA dónde vuelve tu cliente después de pagar.
url_cancelacionhttpsA dónde vuelve si se arrepiente.
expira_minutosenteroEntre 1 y 1440. Por defecto 15.
metadataobjetoLo que quieras guardar. Te lo devolvemos tal cual en el webhook.
curl -X POST https://api.toki.lat/v1/cobros \
  -H "Authorization: Bearer $TOKI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "monto": 15990,
    "concepto": "Boleta 4471",
    "referencia_externa": "b-4471",
    "url_retorno": "https://mitienda.cl/gracias",
    "url_cancelacion": "https://mitienda.cl/carro",
    "metadata": { "caja": "3" }
  }'
{
  "cobro": {
    "id": "9cd827ca-d10d-4768-8627-265d875f1cd2",
    "monto": 15990,
    "moneda": "CLP",
    "concepto": "Boleta 4471",
    "estado": "pending",
    "referencia_externa": "b-4471",
    "metadata": { "caja": "3" },
    "expira_en": "2026-08-25T15:44:57Z",
    "creado_en": "2026-08-25T15:29:57Z",
    "pagado_en": null,
    "url_checkout": "https://toki.lat/pagar/9cd827ca...",
    "qr_svg": "https://api.toki.lat/v1/cobros/9cd827ca.../qr",
    "deeplink": "toki://pay/9cd827ca..."
  }
}

Usa `url_checkout`: es la página de pago que alojamos nosotros. qr_svg y deeplink quedan para quien quiera armar su propia pantalla — mira la sección siguiente antes de decidirlo.

La página de pago

Mandar a tu cliente a url_checkout es la forma recomendada de cobrar, y no es sólo comodidad.

Un QR no se puede escanear con el mismo teléfono que lo muestra. Si tu cliente está comprando en tu tienda desde su celular —el caso más común— una imagen de QR no le sirve de nada. Nuestra página lo detecta: en el teléfono le ofrece abrir la app directamente, y en el escritorio le muestra el QR.

Además sigue el estado sola, muestra cuánto falta para que el cobro venza, y lo devuelve a tu sitio cuando termina. Y como la página es nuestra, podemos mejorar el flujo o agregar medios de pago sin que tú vuelvas a desplegar nada.

Embeberla en tu sitio

Si prefieres que tu cliente no salga de tu página, cárgala en un iframe. Te avisamos del resultado con postMessage, así no tienes que consultar nada desde el navegador.

<iframe
  src="https://toki.lat/pagar/9cd827ca..."
  style="width:100%;max-width:420px;height:620px;border:0"
  allow="clipboard-write"></iframe>

<script>
  window.addEventListener('message', (e) => {
    if (e.origin !== 'https://toki.lat') return;      // verifica SIEMPRE el origen
    if (e.data?.fuente !== 'toki') return;
    if (e.data.evento === 'cobro.paid') {
      // Confirma contra TU servidor antes de entregar: un mensaje del navegador
      // lo puede falsificar cualquiera. Esto sirve para reaccionar en pantalla.
      mostrarGracias();
    }
  });
</script>

El `postMessage` es para la interfaz, no para decidir. Antes de entregar un producto, confirma con GET /v1/cobros/{id} desde tu servidor o espera el webhook. Cualquiera puede mandarle un mensaje a tu página; nadie puede falsificar nuestra respuesta firmada.

El QR

GET/v1/cobros/{id}/qr

Devuelve el código en SVG, para que se vea nítido tanto en una boleta térmica como en una pantalla de caja. Es la única ruta que no pide credencial: se pega en una boleta, donde no hay dónde poner un header. Lo que expone es el mismo id que ya va dentro del código, y con ese id sólo se puede pagar.

Úsalo cuando el pago ocurre frente a ti —una caja, una boleta impresa— donde tu cliente tiene su propio teléfono para escanear. Para cobrar por internet, usa la página de pago.

<img src="https://api.toki.lat/v1/cobros/{id}/qr" alt="Paga con Toki" />

Consultar un cobro

GET/v1/cobros/{id}

Devuelve el cobro con su estado: pending, paid, cancelled o expired. Es el camino de respaldo si no puedes recibir webhooks — una caja detrás de una red cerrada integra sólo con esto.

curl https://api.toki.lat/v1/cobros/9cd827ca-d10d-4768-8627-265d875f1cd2 \
  -H "Authorization: Bearer $TOKI_API_KEY"

Anular un cobro

POST/v1/cobros/{id}/anular

Sólo mientras esté pending. Un cobro ya pagado no se anula por acá: devolver plata es un reembolso, con su propio flujo. Anular un cobro pagado dejaría la contabilidad diciendo una cosa y el cobro otra.

Servicios y suscripciones

Un servicio es algo a lo que la gente se suscribe: un plan, una membresía, una cuota mensual. Lo publicas por API y generas un enlace para que alguien se suscriba. Desde ahí, el cobro se repite solo.

Nadie queda suscrito sin confirmarlo. El enlace no activa nada: la persona ve cuánto y cada cuánto se le va a cobrar, y lo confirma en su app. Después le aparece en Dinero → Suscripciones, donde puede cancelarla sin pasar por ti.

Publicar un servicio

POST/v1/servicios
CampoTipoQué es
nombretexto, requeridoLo que ve tu cliente. "Plan mensual", "Cuota socio".
precioentero, requeridoEn la unidad mínima de tu moneda, igual que `monto`.
descripciontextoQué incluye.
recurrentebooleanoPor defecto true. En false queda publicado pero no admite suscripción: para cobrarlo una vez usa /v1/cobros.
cadaenteroPor defecto 1.
unidadtextoday, week, month, semester o year. Por defecto month.
dia_de_cobroentero 1–31El día del mes en que se cobra a todos. Sólo con unidad month, semester o year. Si no lo mandas, cada persona se cobra el día que se suscribió.
dia_de_semanaentero 0–6Sólo con unidad week. 0 es domingo.
politica_mes_cortotextoQué hacer cuando el día no existe en el mes: last (último día), first_next (el 1 del siguiente) o skip. Sólo con dia_de_cobro mayor que 28.
referencia_externatextoTu identificador. Hace el alta idempotente.

Los campos que no corresponden a la unidad elegida se rechazan, no se ignoran: mandar dia_de_semana en un plan mensual devuelve un 400, para que no te quedes creyendo que configuraste algo.

curl -X POST https://api.toki.lat/v1/servicios \
  -H "Authorization: Bearer $TOKI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Plan mensual",
    "precio": 19990,
    "cada": 1,
    "unidad": "month",
    "dia_de_cobro": 1,
    "referencia_externa": "plan-mensual"
  }'
{
  "servicio": {
    "id": "b97c3820-b9ea-4352-aba4-a459aaa02015",
    "nombre": "Plan mensual",
    "descripcion": null,
    "precio": 19990,
    "moneda": "CLP",
    "recurrente": true,
    "cada": 1,
    "unidad": "month",
    "dia_de_cobro": 1,
    "dia_de_semana": null,
    "politica_mes_corto": null,
    "activo": true,
    "referencia_externa": "plan-mensual",
    "creado_en": "2026-08-25T15:50:15Z"
  }
}

Listar y editar

GET/v1/servicios

Lista todo el catálogo del comercio — también lo publicado desde la app, no sólo lo creado por API.

PATCH/v1/servicios/{id}
CampoTipoQué es
activobooleanoEn false deja de admitir nuevas suscripciones. Las vigentes siguen cobrándose.
precioenteroRige para quien se suscriba después.
nombretexto
descripciontexto

Cambiar el precio no afecta a quien ya está suscrito. Su monto quedó fijado cuando aceptó; subírselo desde acá sería cobrarle algo que nunca autorizó.

Generar el enlace de suscripción

POST/v1/suscripciones
CampoTipoQué es
servicio_iduuid, requeridoEl servicio al que se suscribe.
referencia_externatextoTu identificador. Hace la operación idempotente.
expira_minutosenteroEntre 1 y 1440. Por defecto 60: suscribirse se piensa más que pagar una boleta.
curl -X POST https://api.toki.lat/v1/suscripciones \
  -H "Authorization: Bearer $TOKI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "servicio_id": "…", "referencia_externa": "socio-118" }'

La respuesta tiene la misma forma que un cobro —misma url_checkout, mismos estados— más el servicio_id. Consultas su estado con GET /v1/cobros/{id} y recibes el webhook cobro.paid cuando la persona confirma.

Modo de prueba

Crea una credencial en modo prueba desde tu panel y desarrolla contra ella sin mover un peso. La llave se ve distinta —empieza con tk_test_— para que no se confunda con la de producción ni en un log ni en un archivo de configuración.

Un cobro de prueba no puede pagarse con dinero real, y uno real no puede darse por pagado simulando. Los dos candados están en la base, no en esta documentación: no dependen de que nadie se equivoque.

Dar por pagado un cobro de prueba

POST/v1/cobros/{id}/simular-pago

Marca el cobro como pagado y dispara tu webhook, sin escribir un solo asiento contable. Es como pruebas tu pantalla de "gracias" y tu manejo del evento antes de cobrarle a nadie.

curl -X POST https://api.toki.lat/v1/cobros/{id}/simular-pago \
  -H "Authorization: Bearer $TOKI_TEST_KEY"

Los cobros de prueba traen es_prueba: true en la respuesta. Si tu integración los ve en producción, es que subiste la credencial equivocada.

Reembolsos

POST/v1/cobros/{id}/reembolsar

Devuelve el dinero de un cobro pagado. Sin monto, devuelve todo lo que quede; con monto, devuelve esa parte y puedes volver a llamar hasta completar.

CampoTipoQué es
montoenteroCuánto devolver, en la unidad mínima de tu moneda. Si lo omites, se devuelve todo lo pendiente.

Tu cliente recupera el 100% de lo que pagó, y ese monto sale completo de tu billetera. La comisión no vuelve: el cobro ya se procesó, así que ya estaba ganada. En la práctica significa que devuelves un poco más de lo que recibiste — la diferencia es la comisión de esa venta.

Ojo con esto al operar: para devolver una venta necesitas tener el monto completo disponible, no sólo lo que recibiste por ella. Si ya retiraste la plata y no alcanza, el reembolso falla con un error que dice cuánto falta — preferimos eso a dejarte un saldo negativo que después haya que perseguir.

curl -X POST https://api.toki.lat/v1/cobros/{id}/reembolsar \
  -H "Authorization: Bearer $TOKI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "monto": 5000 }'

La respuesta trae reembolsado, el total devuelto hasta ahora. Y te avisamos por webhook: cobro.partially_refunded mientras quede saldo, cobro.refunded cuando se devolvió todo.

WooCommerce

Si tu tienda es WooCommerce, no necesitas escribir código: hay un plugin que hace todo esto por ti.

Descargar el plugin
  • 1. Instálalo desde Plugins → Añadir nuevo → Subir plugin.
  • 2. Ve a WooCommerce → Ajustes → Pagos → Toki.
  • 3. Pega tu credencial de prueba y déjalo en modo de prueba.
  • 4. Copia la "URL para avisos" que aparece ahí y pégala en tu credencial de Toki, junto con su secreto de firma.
  • 5. Haz un pedido completo. Con POST /v1/cobros/{id}/simular-pago lo das por pagado sin mover dinero.
  • 6. Cuando funcione, pega la credencial de producción y desactiva el modo de prueba.

Hace cobros, reembolsos totales y parciales desde el propio pedido, y verifica la firma de cada aviso. El pedido se marca pagado con el webhook, nunca porque el cliente haya vuelto a la tienda — volver no prueba que se pagó.

Al guardar la configuración, el plugin le pregunta a Toki en qué moneda cobras y te avisa si no coincide con la de tu tienda. Toki no convierte monedas, así que en ese caso el método no se muestra en el checkout en vez de cobrar un número en la moneda equivocada.

Webhooks

Si configuras una URL https en tu panel, te avisamos ahí cuando el cobro cambia de estado: cobro.paid, cobro.cancelled o cobro.expired. El cuerpo trae el evento y el cobro completo.

{
  "evento": "cobro.paid",
  "cobro": { "id": "9cd827ca...", "estado": "paid", "monto": 15990, ... }
}

Cada entrega va firmada en el header X-Toki-Firma, con la forma t=<epoch>,v1=<hmac>. El HMAC es SHA-256 sobre ${t}.${cuerpo} con el secreto de webhook de tu credencial. Verifícala siempre: sin eso, cualquiera que conozca tu URL puede decirte que le pagaron.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verificar(cuerpo, cabecera, secreto) {
  const p = Object.fromEntries(cabecera.split(',').map((x) => x.split('=', 2)));
  const t = Number(p.t);
  // El timestamp va DENTRO de lo firmado: si no, se puede reenviar un
  // webhook viejo con un t nuevo y la firma seguiría validando.
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const esperado = createHmac('sha256', secreto).update(`${t}.${cuerpo}`).digest('hex');
  const a = Buffer.from(esperado, 'hex');
  const b = Buffer.from(p.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Respondemos a cualquier 2xx como entregado. Si tu servidor falla, reintentamos a 1, 5, 25 y 125 minutos, y luego desistimos: el último error queda visible en tu panel. Los webhooks pueden llegar repetidos, así que trata el `id` del cobro como llave y haz tu procesamiento idempotente.

Qué recibes tú

De cada cobro, Toki descuenta su comisión y te liquida el resto al instante. La API no la devuelve como un número fijo porque no lo es: depende del tipo de cobro, de tu plan, y de cualquier tarifa pactada contigo, que manda sobre las demás.

Lo que sí conviene que sepas: cobrar por API paga la tarifa de venta presencial, la más baja del catálogo — porque estás trayendo tu propio sistema y Toki sólo pone el medio de pago. Vender por el mercado de Toki, con su catálogo y su despacho, cuesta bastante más. Las suscripciones tienen sus propias tarifas, y cobran distinto la inscripción que las renovaciones.

La tuya, ya con todo aplicado, está en tu panel: Empresa → Dinero, junto al detalle de cada liquidación.

Errores

Todos los errores traen la misma forma. El code es estable y pensado para que lo compares en tu código; el mensaje es para que lo leas tú.

{ "error": { "code": "credencial_invalida", "mensaje": "..." } }
codeHTTPQué pasó
sin_credencial401Falta el header Authorization.
credencial_invalida401Llave, secreto, o IP de origen que no calzan. No distinguimos cuál a propósito.
sin_alcance403La credencial no tiene permiso para esa operación.
cuerpo_invalido400Falta un campo o tiene el tipo equivocado.
no_encontrado404No existe ese cobro para esta credencial.
estado_invalido409El cobro ya no está pendiente.
demasiadas_solicitudes429Pasaste las 120 llamadas por minuto.
error_interno500Falla nuestra. Reintentar es seguro si mandas referencia_externa.

Antes de salir a producción

  • — Guarda el secreto donde guardas los demás secretos, nunca en el código ni en el front.
  • — Configura la lista de IPs. Es el candado que sigue sirviendo si el secreto se filtra.
  • — Manda siempre referencia_externa: es lo que impide cobrar dos veces cuando la red falla a mitad de camino.
  • — Verifica la firma de cada webhook, y no decidas nada con un postMessage.
  • — Ten un plan si el webhook no llega: consulta el estado del cobro antes de entregar el producto.

Iniciar sesión con Toki

Deja que las personas entren a tu sitio con su cuenta de Toki. Aprueban desde su teléfono con su rostro, y tú recibes solo los datos que ellas autorizaron.

Es OAuth 2.0 estándar (authorization code + PKCE), no un flujo inventado. Puedes usar la librería que ya conoces, las propiedades de seguridad están estudiadas, y quien no conoce a Toki puede integrarlo sin confiar en nuestra criptografía.

Registra tu aplicación

En toki.lat/connect registras tu app y recibes un client_id y un client_secret. El secreto se muestra una sola vez: de la base solo guardamos su hash, así que no es que no queramos mostrarlo después — no lo tenemos.

Puedes integrar de inmediato con los permisos no sensibles. Para RUT, teléfono, empresas o autorizar operaciones revisamos la app antes: nombre, logo y responsable declarado. Una app llamada "Toki Pagos" con nuestro logo convertiría la pantalla de consentimiento —que es justo donde la persona confía— en una herramienta de phishing.

El flujo

Mandas a la persona a la pantalla de consentimiento, vuelve con un código, y ese código lo canjeas desde tu servidor por un token.

GET/autorizar
https://toki.lat/autorizar
  ?client_id=tu_client_id
  &redirect_uri=https://tusitio.cl/oauth/callback
  &response_type=code
  &scope=perfil+email
  &state=<valor aleatorio tuyo>
  &code_challenge=<SHA-256 del verifier, base64url>
  &code_challenge_method=S256

La persona ve tu nombre, el dominio real de tu sitio, quién responde por él, y cada dato que le pides en lenguaje humano. Si aprueba, vuelve a tu redirect_uri con code y tu state intacto.

POST/api/oauth/token
grant_type=authorization_code
code=<el codigo>
redirect_uri=https://tusitio.cl/oauth/callback
client_id=tu_client_id
client_secret=tu_secreto
code_verifier=<el verifier original>

Devuelve access_token, refresh_token, expires_in y el scope realmente otorgado — que puede ser menor al que pediste. Léelo: es la única forma de saber qué datos tienes de verdad.

GET/api/oauth/userinfo

Con Authorization: Bearer <access_token>. Cada campo sale solo si su permiso está en el token. sub va siempre: es el identificador estable de la persona.

POST/api/oauth/revoke

Para cerrar sesión de tu lado. Responde 200 siempre, incluso con un token inexistente: distinguirlos convertiría esta ruta en una forma de adivinar tokens válidos.

Qué datos puedes pedir

Pide lo mínimo. Cada permiso extra es una pregunta más que la persona tiene que responderse antes de aprobar, y una razón más para no hacerlo.

PermisoQué entregaRevisión
`perfil`Nombre, foto, usuario y si Toki verificó su identidadNo
`email`Correo electrónicoNo
`rut`Documento de identidad y su país
`telefono`Teléfono
`empresa`Empresas en las que participa y su cargo
`transacciones:autorizar`Autorizar operaciones en su nombre

Lo que tienes que saber

  • PKCE es obligatorio y solo con S256. plain se rechaza.
  • — El redirect_uri se compara exacto contra los que registraste. Sin comodines: aceptarlos permitiría que el código llegue a un destino que no controlas.
  • — El código dura un minuto y es de un solo uso. Si se canja dos veces asumimos que se filtró y revocamos todo lo emitido para esa persona en tu app.
  • — Los refresh_token rotan: cada canje entrega uno nuevo e invalida el anterior. Reusar uno viejo se trata como robo y corta la sesión.
  • — El client_secret nunca viaja al navegador. Si tu app no puede guardarlo, el canje va igual con PKCE.
  • — La persona puede retirar el acceso cuando quiera desde su cuenta, y ahí se cortan también los tokens vivos. Tu integración tiene que sobrevivir a eso sin romperse.