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/v1Empezar
Cómo funciona
- 1. Tu sistema llama a
POST /v1/cobroscon 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...c0En 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, y 600 por minuto desde una misma IP (ésas se cuentan antes de verificar la llave, para frenar a quien prueba secretos). Pasarte responde 429 con reintentar_en_s.
Alcances
Cada credencial lleva una lista de alcances, y cada ruta exige el suyo. Sirven para que puedas entregarle una llave a un integrador externo sin entregarle toda tu caja: una llave hecha para armar pedidos no debería poder devolverte la plata de un cobro.
| Alcance | Qué habilita |
|---|---|
| cobros | Pedir plata, y deshacerla: POST /v1/cobros, GET /v1/cobros/{id}, /anular, /reembolsar, /simular-pago, todo /v1/servicios y POST /v1/suscripciones. |
| pedidos | El checkout de micro apps: POST /v1/pedidos, GET /v1/pedidos/{id}, /consumir y /v1/apps/usos. |
| equipo | El chat de tu marca: GET /v1/canales, POST /v1/canales/{id}/mensajes y /v1/invocaciones/{id} (leer y responder). No toca dinero. |
Hay dos rutas fuera de la tabla, y es a propósito: GET /v1/comercio no exige alcance —es «quién soy», y no dice nada que la credencial no represente ya— y GET /v1/cobros/{id}/qr no pide credencial, porque se pega en una boleta.
Tres respuestas que no hay que confundir. 403 sin_alcance es «tu llave no tiene ese permiso» — el mensaje te dice cuál falta y cuáles tienes, así que se arregla emitiendo otra credencial, no cambiando el id. 404 no_encontrado es «ese id no existe, o no es de esta credencial»: las dos se responden igual a propósito, porque distinguirlas le diría a cualquiera qué ids existen en Toki. 409 estado_invalido es «existe y es tuyo, pero ya no está pendiente».
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.
/v1/comercioTe 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.
Cobrar
Crear un cobro
/v1/cobros| Campo | Tipo | Qué es |
|---|---|---|
| monto | entero, requerido | En 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. |
| concepto | texto | Lo que ve tu cliente. "Boleta 4471", "Mesa 12". |
| referencia_externa | texto | Tu identificador. Además hace el cobro idempotente: reintentar con la misma referencia devuelve el cobro que ya existe, en vez de crear otro. |
| url_retorno | https | A dónde vuelve tu cliente después de pagar. Con una credencial de prueba se acepta también http (acá y en url_cancelacion), para integrar desde localhost. |
| url_cancelacion | https | A dónde vuelve si se arrepiente. |
| expira_minutos | entero | Entre 1 y 1440. Por defecto 15. |
| metadata | objeto | Lo que quieras guardar. Te lo devolvemos tal cual en el webhook, salvo las claves reservadas origen, items, dte y dte_error, que se descartan. |
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",
"reembolsado": 0,
"es_prueba": false,
"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://toki.lat/api/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
/v1/cobros/{id}/qrDevuelve 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
/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
/v1/cobros/{id}/anularSó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.
Después del cobro
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 (también en el header X-Toki-Evento) y el cobro completo.
{
"id": "5b1e0c4a...",
"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 los 5, 25, 125 y 625 minutos (cinco intentos en total, unas diez horas), y luego desistimos: el último error queda visible en tu panel. Los webhooks pueden llegar repetidos, así que usa el `id` del aviso (también en el header `X-Toki-Evento-Id`) para descartar los repetidos: es el mismo en cada reintento. Y haz tu procesamiento idempotente sobre el id del cobro.
Reembolsos
/v1/cobros/{id}/reembolsarDevuelve 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.
| Campo | Tipo | Qué es |
|---|---|---|
| monto | entero | Cuá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.
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.
Lo digital que cobra la tienda se reparte distinto (19-sep-2026). Cuando el cobro pasa por App Store o Google Play, Toki retiene 41,9 % + IVA del precio publicado y de ahí paga la comisión de la tienda: de $10.000 al vendedor le llegan $5.014, lo cobre Apple o lo cobre Google. Fuera de las tiendas —web, tarjeta o billetera— la retención es 10 % + IVA: de $10.000 te llegan $8.810. Tu neto no depende de la tienda; la diferencia entre Apple y Google la absorbe Toki.
Suscripciones
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
/v1/servicios| Campo | Tipo | Qué es |
|---|---|---|
| nombre | texto, requerido | Lo que ve tu cliente. "Plan mensual", "Cuota socio". |
| precio | entero, requerido | En la unidad mínima de tu moneda, igual que monto. |
| descripcion | texto | Qué incluye. |
| recurrente | booleano | Por defecto true. En false queda publicado pero no admite suscripción: para cobrarlo una vez usa /v1/cobros. |
| cada | entero | Por defecto 1. |
| unidad | texto | day, week, month, semester o year. Por defecto month. |
| dia_de_cobro | entero 1–31 | El 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_semana | entero 0–6 | Sólo con unidad week. 0 es domingo. |
| politica_mes_corto | texto | Qué 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_externa | texto | Tu 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
/v1/serviciosLista todo el catálogo del comercio — también lo publicado desde la app, no sólo lo creado por API.
/v1/servicios/{id}| Campo | Tipo | Qué es |
|---|---|---|
| activo | booleano | En false deja de admitir nuevas suscripciones. Las vigentes siguen cobrándose. |
| precio | entero | Rige para quien se suscriba después. |
| nombre | texto | |
| descripcion | texto |
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
/v1/suscripciones| Campo | Tipo | Qué es |
|---|---|---|
| servicio_id | uuid, requerido | El servicio al que se suscribe. |
| referencia_externa | texto | Tu identificador. Hace la operación idempotente. |
| expira_minutos | entero | Entre 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.
Identidad
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 el panel de tu empresa, en Desarrolladores, 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 el documento de identidad, 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.
/autorizarhttps://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=S256La 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.
/api/oauth/tokengrant_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 (dura una hora), refresh_token (30 días), 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.
/api/oauth/userinfoCon 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.
/api/oauth/revokePara 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.
| Permiso | Qué entrega | Revisión |
|---|---|---|
perfil | Nombre, foto, usuario y si Toki verificó su identidad | No |
email | Correo electrónico | No |
rut | Número de identidad y su país. Vale para cualquier país: la clave se llama rut sólo por historia | Sí |
telefono | Teléfono | Sí |
empresa | Empresas en las que participa, con su identificador tributario (RUT en Chile), y su cargo | Sí |
identidad:verificacion | Nivel de verificación de su identidad (chip, prueba de vida, teléfono), firmado por Toki | Sí |
identidad:documento | Copia del chip de su documento (datos y foto) con la firma del emisor, para verificarla por su cuenta | Sí |
transacciones:autorizar | Autorizar operaciones en su nombre | Sí |
Lo que tienes que saber
- — Usa PKCE, y solo con
S256:plainse rechaza. Sin PKCE el canje sólo se acepta conclient_secret. - — El
redirect_urise 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_tokenrotan: cada canje entrega uno nuevo e invalida el anterior. Reusar uno viejo se trata como robo y corta la sesión. - — El
client_secretnunca viaja al navegador. Si tu app no puede guardarlo, el canje del código va igual con PKCE, pero renovar exige el secreto:grant_type=refresh_tokensin él respondeinvalid_client. Puede ir en el cuerpo o enAuthorization: Basic. - — 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.
Micro apps
Qué son
Una micro app es una web tuya que se abre a pantalla completa dentro de Toki, con acceso a un puente que le deja pedirle cosas a la app: quién es la persona, su ubicación, abrir un pago. Tú no instalas ningún SDK —la persona sí instala tu app desde la tienda de Toki, ver *Tu ficha en la tienda*—: Toki inyecta window.toki en tu página antes de que cargue.
Es a propósito que no haya un SDK. Si tuvieras que instalar un paquete, cada cambio del puente te obligaría a actualizar; inyectado, el contrato lo movemos nosotros sin romperte.
Tu app corre en su propio origen y sólo habla con Toki por mensajes. No se le inyecta nada: ni token, ni identificador de la persona, ni sus datos. Todo lo que quiera saber lo pide, y cada pedido pasa por dos filtros — qué declaraste en el manifiesto y qué autorizó la persona.
El manifiesto
Cada app se registra con un manifiesto. Lo importante: la clave (minúsculas y guiones, es tu identificador estable), la url de arranque —https obligatorio— y las capacidades que vas a pedir.
Las capacidades del manifiesto son la lista completa de lo que tu app puede llegar a hacer, y la persona la ve antes de entrar. Una capacidad que no declaraste no se puede pedir: el puente responde sin_capacidad sin siquiera preguntarle a nadie. Declara lo mínimo — cada permiso de más es una razón para no abrir tu app.
| Capacidad | Qué habilita |
|---|---|
| identidad | Id derivado de la persona y, con confirmación, nombre y foto |
| compartir | Abrir el compartidor del sistema con un texto tuyo |
| ubicacion | Una lectura puntual de dónde está |
| pagar | Abrir el pago de un cobro que creó tu backend |
| comprar | Iniciar una compra con entrega: tú declaras qué vendes y Toki pone el checkout |
| nfc | Leer una etiqueta NFC común (NDEF). No alcanza el chip de un documento |
| documento | Restringida. Leer el chip del documento y recibir sólo sus imágenes: retrato y firma |
| abrir_app | Saltar a otra micro app, con confirmación |
⚠️ `documento` es una capacidad restringida. Declararla no basta: un administrador de Toki la otorga a tu app en particular, con un motivo que queda registrado, y hasta entonces el puente responde sin_capacidad y la app no se puede publicar. Entrega sólo las imágenes —retrato y firma—: nunca el nombre, el número de identidad ni las fechas. Y la lectura la hace Toki: tu página no habla con el chip. La capacidad nfc, que sí es autoservicio, lee etiquetas NDEF comunes y no alcanza un documento — no es un bloqueo, es que un chip de documento no publica NDEF.
Una app pasa por borrador → en_revision → publicada. Sólo publicada se ve fuera de tu equipo, y puede volver a rechazada o suspendida.
La app la registras tú, desde tu panel en Mi empresa → Micro apps. Ahí escribes el manifiesto, la guardas como borrador cuantas veces quieras y la mandas a revisión cuando esté. Nosotros revisamos; no redactamos.
Cada capacidad que marques lleva una frase tuya diciendo para qué la necesitas (200 caracteres). No reemplaza nuestra explicación del permiso: la completa. «Ubicación» no se puede contestar informado; «para cotizar el envío a tu dirección» sí. Sin esa frase la app no entra a revisión — una capacidad sin motivo sólo se puede rechazar.
Ese texto se le muestra a la persona en el momento en que tu app usa el permiso, no al abrirla: es cuando puede decidir con contexto. Y se muestra atribuido a ti, no a Toki.
El puente toki.*
Cada método es una función que recibe un objeto y devuelve una promesa. El objeto está congelado: no se puede reemplazar ni extender.
| Método | Capacidad | Devuelve |
|---|---|---|
| toki.info() | — | { v, idioma, tema, plataforma, version, capsula } — capsula: el espacio, en px CSS, que tu página deja libre arriba a la derecha |
| toki.identidad() | identidad | { id, token, expira_en, vigencia_s } — derivado, más un token firmado; ver abajo |
| toki.perfil() | identidad | { nombre, foto }, con confirmación cada vez |
| toki.compartir({ texto, url }) | compartir | { ok: true }. No sabrás si compartió |
| toki.ubicacion() | ubicacion | { lat, lng, precision } |
| toki.pagar({ cobroId }) | pagar | { abierto: true } |
| toki.pedido({ pedidoId }) | comprar | { abierto: true } — abre la hoja de compra de un pedido tuyo |
| toki.abrirApp({ clave }) | abrir_app | { abierto: true } |
| toki.nfc() | nfc | { id, texto, registros } — una etiqueta NDEF común |
| toki.documento() | documento | { foto, firma } — imágenes en base64; restringida |
| toki.cerrar() | — | cierra y vuelve a Toki |
// window.toki ya existe al cargar; no hay que esperar nada.
const { id } = await toki.identidad();
try {
await toki.pagar({ cobroId });
} catch (e) {
if (e.codigo === 'cancelado') return; // la persona dijo que no
throw e;
}Los errores llegan como Error con un codigo estable: sin_capacidad (no la declaraste), sin_permiso (la persona dijo que no), cancelado, params, metodo_desconocido, mal_formado, interno.
Quién es la persona
toki.identidad() no devuelve el identificador real de la persona en Toki, sino uno derivado de la pareja (persona, app). Es estable para ti y distinto en cada app: dos micro apps no pueden cruzar sus bases y descubrir que hablan con la misma persona.
Nombre y foto son otra cosa: van por toki.perfil() y piden confirmación cada vez, porque desde dentro de la app no podemos saber si el pedido nació de un botón que la persona apretó o de un bucle. Un «no» vale para toda la sesión; insistir no abre otro diálogo.
Lo que tu app nunca va a poder leer: chats, contactos, órdenes, saldo, medios de pago, la sesión de la persona ni su dirección. Tampoco puede correr en segundo plano ni mandar notificaciones.
Vender con entrega
Un cobro sirve para cobrar un monto. Un pedido sirve para vender algo que hay que entregar: tu app declara las líneas y Toki pone el checkout — libreta de direcciones de la persona, cotización del courier, total y confirmación. Necesita la capacidad comprar.
Es una entidad distinta de las órdenes de Merqado, a propósito: lo que vende tu micro app es tuyo y no está en nuestro catálogo. Por eso el pedido lo creas tú por la API y no seleccionando productos nuestros.
/v1/pedidosMandas líneas, nunca un total. El total lo calculamos nosotros y sale del subtotal menos el descuento más la entrega; si el total viajara en el cuerpo, quien compra podría confirmar su pedido con envío en cero.
⚠️ Desde el 19-sep-2026 cada línea lleva la clave de un ítem de tu catálogo (Mi empresa → Micro apps → Ítems) y su precio_unitario tiene que ser el del catálogo. Una línea sin clave se rechaza con 400 cuerpo_invalido; con una clave que no está en tu catálogo activo el mensaje empieza con linea_fuera_de_catalogo, y con otro precio, con precio_no_coincide (los dos también son 400 cuerpo_invalido). Si tu precio varía por tamaño o cantidad, declara un ítem por variante: cambiar el catálogo no requiere publicar nada.
El precio del catálogo no siempre está en el ítem. Si el ítem declara su propio monto, ése manda y no se mira nada más. Si lo deja vacío y cuelga de un servicio de tu catálogo de Mercado, el precio del catálogo es el de ese servicio — y sólo mientras el servicio esté activo: uno archivado no presta el suyo. Si no hay ninguno de los dos, el ítem no tiene precio y la línea responde precio_no_coincide sea cual sea el número que mandes; no es «precio a convenir», es un ítem que no se puede vender.
La consecuencia práctica: un ítem que hereda cambia de precio cuando alguien edita el servicio, sin tocar la micro app ni volver a publicarla. Si tu backend guarda el precio en una copia propia, va a empezar a mandar el viejo y a recibir precio_no_coincide sin haber cambiado nada. Lee el precio vigente del panel (Mi empresa → Micro apps → Ítems, que muestra el precio y de dónde sale), o declara el monto en el ítem para que no dependa del servicio.
curl https://api.toki.lat/v1/pedidos \
-H "Authorization: Bearer $TOKI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"app": "foto-trama",
"items": [{ "nombre": "10 copias 10x15", "cantidad": 1, "precio_unitario": 6990, "clave": "copias-10x15" }],
"entrega": { "tipo": "despacho", "origen_comuna": "PROV", "bulto": { "peso_g": 400 } },
"referencia_externa": "orden-1042"
}'Las tres formas de entregar
| `entrega.tipo` | Qué pasa | Qué tienes que mandar |
|---|---|---|
| despacho | La persona elige su dirección y cotizamos el envío contra el courier | origen_comuna y, si puedes, bulto |
| retiro | La persona elige una de tus bodegas con retiro habilitado | nada: las bodegas salen de tu perfil |
| digital | No hay nada que entregar | nada |
⚠️ origen_comuna es el código de comuna de Chilexpress (PROV, STGO), no el nombre. Con el nombre la cotización falla recién al confirmar, lejos de esta llamada.
Para retiro necesitas al menos una bodega con retiro habilitado en Mi empresa → Bodegas. Es también de dónde sale el costo del retiro: no se manda en el cuerpo, igual que el flete.
Si aceptas más de una, no preguntes tú. Manda entrega.tipos: ["despacho", "retiro"] y la hoja de Toki le pregunta a la persona cómo lo quiere recibir, mostrando el envío estimado y el costo del retiro de cada forma. entrega.tipo pasa a ser la que se propone primero. Preguntarlo en tu página es hacer checkout a mano: tendrías que conocer bodegas y costos, y crear un pedido distinto por respuesta. digital no se combina con las otras —sería dejar que quien paga elija el riel de cobro—, y si ofreces despacho, origen_comuna sigue siendo obligatorio.
Y después, en el teléfono
Con el id que te devolvimos, tu página abre el checkout de Toki: await toki.pedido({ pedidoId: id }). Ahí la persona elige dirección o bodega, ve el total ya cotizado y paga. El monto no viaja en esa llamada — el pedido ya tiene precio. Si el pedido venció, ya no está pendiente o es de otra app, responde params con un detalle que lo dice (pedido_vencido, pedido_pagado, pedido_de_otra_app…).
/v1/pedidos/{id}Esta es tu fuente de verdad, no lo que devuelva el puente: eso último es tu propio código contándote cómo le fue, en un teléfono que no controlas. Acá viene el estado, el despacho cotizado, el total y —si es retiro— entrega.retiro con el nombre, la dirección y la comuna del lugar al que va a ir la persona, para que puedas decírselo.
Lo que nunca vas a recibir es la dirección de quien compra. La elige en nuestra hoja y sirve para despachar; si Toki retira en tu bodega y despacha, no la necesitas. Ese es el punto entero.
El aviso llega como pedido.paid con el id del pedido —no el del cobro, que nunca viste—. También existen pedido.cancelled, pedido.expired, pedido.refunded y pedido.partially_refunded. Se firman igual que los de cobros.
- — Un pedido vive 30 minutos por defecto (
expira_minutos). Pasado eso quedaexpiradoy hay que crear otro. - —
referencia_externaes tu idempotencia: la misma referencia con la misma credencial te devuelve el pedido que ya creaste, no uno nuevo. Si ese pedido ya venció, manda una referencia nueva. - — La
clavede cada línea (obligatoria, ver arriba) es también la que descuenta stock y ata el pedido al ítem declarado. Sin stock suficiente responde409 estado_invalido. - — Un pedido es de un solo vendedor. Un carrito con dos micro apps es otro problema y todavía no existe.
Canjear un pedido pagado
Si lo que vendes cuesta plata generarlo —una imagen de un modelo, un informe, un minuto de video— necesitas saber que ese pedido no se canjeó ya. Llevar esa cuenta en tu servidor es más difícil de lo que parece: en memoria no sirve (dos instancias no se ven) y en tu base es un contador que tienes que mantener consistente con lo que cobró otro. Como el que cobró es Toki, la cuenta la lleva Toki.
/v1/pedidos/{id}/consumirResponde ok sólo si el pedido está pagado y le quedaba un uso, y lo descuenta en la misma llamada. La referencia del cuerpo es tuya y hace el canje idempotente: si la respuesta se te pierde por red después de que contamos el uso, reintentar con la misma referencia devuelve lo mismo —con repetido: true— en vez de gastar otro. Sin eso, un timeout le cobra dos veces a quien compró una.
curl https://api.toki.lat/v1/pedidos/$PEDIDO/consumir \
-H "Authorization: Bearer $TOKI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "referencia": "generacion-1" }'- — 404 — el pedido no existe, o no es tuyo.
- — 409 — todavía no está pagado (el mensaje dice en qué estado), o ya usó todos sus canjes.
- — Un pedido otorga un canje por defecto.
Usos por comprador
Lo de arriba cuenta los canjes de UN pedido. Si vendes paquetes —1 foto por $790, 10 por $4.990— lo que la persona necesita es otra cosa: usos SUYOS. Compra dos paquetes de 10, tiene 20, y los gasta cuando quiera sin acordarse de con qué pedido pagó cuál. Eso no se manda por la API: lo declara tu catálogo con usos_por_compra en el ítem, y Toki los acredita solo cuando el pedido queda pagado. Un ítem sin usos_por_compra —un cuadro impreso— no deja usos.
/v1/apps/usos/consumircurl https://api.toki.lat/v1/apps/usos/consumir \
-H "Authorization: Bearer $TOKI_API_KEY" \
-H "Content-Type: application/json" \
-H "x-toki-identidad: $TOKI_IDENTIDAD" \
-d '{ "comprador": "'$COMPRADOR'", "item": "fotos-10", "referencia": "foto-9f3ac1-v1", "entregable": { "url": "https://…" } }'/v1/apps/usos?comprador={comprador}&item={item}comprador es el seudónimo que ya conoces: el mismo que te llega con la capacidad identidad y en pedido.comprador. Nunca vas a recibir quién es la persona. El GET sirve para pintar «te quedan 7» antes de que apriete el botón, pero NO es un control: entre leer y entregar puede gastar desde otro teléfono. El que decide es el POST, que descuenta y te responde en la misma llamada. Descuenta unidades (de 1 a 1000, por defecto 1), y con una credencial de prueba no gasta nada: responde con simulado: true.
⚠️ La referencia tiene que identificar QUÉ entregas, nunca ser una constante. Repetirla responde ok con repetido: true todas las veces que llames — eso es lo que te salva de un timeout, y lo que la vuelve inútil como límite si es siempre la misma. Por eso guardamos tu entregable y en el reintento te devolvemos el guardado: así no le vuelves a pagar a tu proveedor por generar lo mismo. Las constantes obvias («descarga», «uso», «default») se rechazan.
- — 404 — esa persona no tiene usos de ese ítem, o el ítem no es de tu credencial.
- — 409 `sin_usos` — no le quedan suficientes. La respuesta trae
disponibles. - — 400 `referencia_debil` — tu referencia es una constante obvia, tiene menos de 8 caracteres o empieza con
pedido:oreembolso:, que usa Toki. - — Reembolsar el pedido devuelve sus usos. Si la persona ya gastó alguno, el reembolso no procede solo: lo resuelven ustedes dos.
Identidad firmada
El comprador te lo pasa tu propia página, así que por sí solo no prueba nada: quien conozca el seudónimo de otra persona le gastaría los usos. Por eso toki.identidad() te devuelve, junto al seudónimo, un token firmado por Toki. Lo reenvías tal cual en el encabezado x-toki-identidad y nosotros verificamos que ese seudónimo sea de quien dice ser. El seudónimo sigue siendo la clave del saldo: el token prueba de quién es, no lo reemplaza.
const { id, token, expira_en } = await toki.identidad();
// id → el seudónimo (64 hex). Es la clave de su saldo.
// token → credencial de 15 min. Mándala a TU backend, nunca en una URL.
await fetch('/mi-backend/trabajo', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ persona: id, identidad: token }),
});⚠️ El token es una credencial, no un dato. No lo escribas en logs, no lo pongas en una URL ni en un parámetro de consulta —queda en el historial, en el Referer y en los registros de cualquier servidor por el que pase— y no lo guardes más allá de su vencimiento.
- — Está atado a tu app. Se firma con una llave que sólo existe para tu app: un token de otra micro app no puede validar acá, ni el tuyo allá.
- — No lleva el uid de la persona ni nada que permita cruzarla con otra app. Sólo el seudónimo que ya conocías.
- — Vive 15 minutos. Volver a llamar
toki.identidad()te da otro sin preguntarle nada a la persona. Pídelo antes de cada trabajo largo. - — 401 `identidad_vencida` — venció a mitad. Renueva y reintenta con la misma `referencia`: es idempotente, así que no gasta un uso de más ni te hace regenerar nada.
- — 401 `identidad_requerida` — tu app ya cerró el modo viejo y no mandaste token. 401 `identidad_no_coincide` — ese token es de otra persona que la del
comprador. 401 `identidad_invalida` — lo demás. - — Mientras tu app no lo exija, es opcional —pero si lo mandas se verifica igual—. La respuesta trae
consumo.identidad_firmadapara que veas en qué modo estás. Sitokenvienenull, esa persona tiene una versión de Toki anterior a esto.
Tu ficha en la tienda
La gente descubre tu app en una ficha y la instala antes de abrirla. La ficha la armas tú desde Mi empresa → Micro apps, con estos campos además del manifiesto (ninguno es obligatorio para publicar, pero una ficha sin portada ni capturas convence poco):
| Campo | Qué es |
|---|---|
| portada | Imagen horizontal 16:9, de al menos 1280 px de ancho. Es la imagen grande de la ficha y la vista previa al compartir el enlace. |
| capturas | Hasta 8 capturas verticales (9:19,5 o 9:16), de al menos 1080 px de ancho, en el orden que elijas. |
| descripcion_larga | El texto de la ficha, hasta 4000 caracteres. descripcion sigue siendo la línea corta del catálogo. |
| novedades | Qué cambió en esta versión, hasta 1000 caracteres. |
| version_publica | La versión que ve la persona: 1, 1.4 o 1.4.2. La version numérica la sube Toki en cada edición. |
| sitio_soporte | URL https de ayuda. |
| correo_soporte | Correo de contacto. |
| politica_privacidad | URL https. Muy recomendable si pides algo sensible (identidad, ubicación, NFC, documento, pagos). |
Las imágenes viven en Toki, igual que el ícono: las subes desde el panel (o pegas una URL y Toki la copia una vez), se validan por sus bytes —PNG, JPEG o WEBP, nada de SVG, hasta 4 MB— y se recortan al centro a una medida fija. Una URL de tu servidor no se guarda nunca: si tu servidor se cae, tu ficha sigue entera.
| Imagen | Se acepta | Queda en |
|---|---|---|
| Portada | 16:9 (±2 %), ≥ 1280 px de ancho | 1920×1080, o 1280×720 si el original no da |
| Captura | 9:19,5 o 9:16 (±3 %), ≥ 1080 px de ancho | 1080×2340 o 1080×1920 |
La ficha también muestra, sin que escribas nada más: tu empresa y si está verificada, qué permisos pide tu app y cuándo, qué datos usa —derivado de tus capacidades y de la frase que escribiste para cada una— y lo que vendes con su precio.
- — Esenciales, al instalar. Las capacidades que marcas como esenciales (sólo
identidadyubicacion) se aceptan de un toque al instalar, listadas con tu frase. Sin ellas tu app no se abre. - — Lo demás, en contexto. El resto se pide la primera vez que tu app lo usa, y pagos, compras, compartir y abrir otra app se confirman cada vez. Sigue valiendo no pedir permisos al abrir.
- — Si agregas una esencial en una versión nueva, a quien ya instaló se le pide antes de abrir: nunca se concede en silencio. Agrégala sólo si de verdad tu app no funciona sin ella.
/v1/apps/{clave}La misma ficha en JSON, sin credencial: sirve para mostrar en tu sitio cómo te ve la gente en Toki. Trae nombre, portada, capturas, desarrollador (con verificado), permisos (con esencial, sensible, momento y tu declaracion), datos, compras (con precio_clp), version_publica, novedades y soporte. Sólo apps publicadas: lo demás es 404. Se cachea 5 minutos.
Reglas que no se negocian
- — No pidas permisos al abrir. Toki muestra la hoja en el momento en que tu app usa la capacidad, así que llamar a
toki.identidad()en el arranque le pide a la persona decidir sobre algo que todavía no hizo — y la respuesta razonable a eso es «no». Pídelo en la acción que lo necesita. En revisión se mira. - — Sólo `https`, y sólo tu origen. Un enlace a otro sitio se abre en el navegador del sistema, fuera de Toki; un
iframede otro dominio no carga. Si cargara, ese marco tendría los permisos de tu app. - — El monto nunca viaja por el puente. El cobro lo crea tu backend contra la API con tu credencial, y
toki.pagarsólo abre uno que ya existe con su precio. Un precio que llega desde la página es un precio que se edita con la consola abierta. - — Sólo puedes abrir cobros tuyos.
toki.pagarverifica que el cobro pertenezca al perfil comercial dueño de la app. El cobro de otro comercio respondeparams. - — Las cookies de terceros están cortadas dentro del contenedor. Tu página conserva las suyas; lo que no funciona es el pixel de un tercero siguiendo a la persona entre micro apps.
- — Toki no le pasa a tu página sus cookies ni su sesión, y tu página no accede a
file://ni puede abrir ventanas sin un gesto.
Herramientas
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-pagolo 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.
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
/v1/cobros/{id}/simular-pagoMarca 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.
Referencia
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": "..." } }| code | HTTP | Qué pasó |
|---|---|---|
| sin_credencial | 401 | Falta el header Authorization. |
| credencial_invalida | 401 | Llave, secreto, o IP de origen que no calzan. No distinguimos cuál a propósito. |
| sin_alcance | 403 | A tu credencial le falta el alcance que exige esa ruta. El mensaje dice cuál falta y cuáles tienes — ver «Alcances». |
| cuerpo_invalido | 400 | Falta un campo o tiene el tipo equivocado. |
| no_encontrado | 404 | Ese id (cobro, pedido, servicio…) no existe o no es de esta credencial. Las dos cosas se responden igual a propósito. |
| estado_invalido | 409 | Existe y es tuyo, pero no está en condiciones: el cobro ya no está pendiente, no alcanza el saldo para reembolsar, el pedido no está pagado… |
| sin_usos | 409 | A esa persona no le quedan usos de ese ítem. Trae disponibles — ver «Usos por comprador». |
| referencia_debil | 400 | La referencia de un consumo es una constante, tiene menos de 8 caracteres o usa un prefijo reservado. |
| identidad_requerida, identidad_invalida, identidad_vencida, identidad_no_coincide | 401 | Falta o no vale el token de x-toki-identidad — ver «Identidad firmada». |
| demasiadas_solicitudes | 429 | Pasaste un límite: 120 llamadas por minuto por credencial, 600 por IP, o 30 mensajes por minuto en un canal. |
| error_interno | 500 | Falla 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.