Toki · API

Cobre pelo seu sistema. Que paguem com Toki.

Seu caixa, sua loja online ou seu ERP criam uma cobrança com uma chamada. A Toki devolve um link de pagamento; seu cliente abre, paga pela carteira dele e volta para o seu site. O dinheiro chega à sua conta Toki na hora.

https://api.toki.lat/v1Criar uma credencial

Como funciona

  • 1. Seu sistema chama POST /v1/cobros com o valor.
  • 2. A Toki responde com uma cobrança e uma url_checkout.
  • 3. Você manda seu cliente para lá, ou incorpora na sua própria página.
  • 4. Ele paga com o app da Toki e o devolvemos para a sua url_retorno.
  • 5. Avisamos por webhook (ou você consulta o status da cobrança).

Sua credencial pede dinheiro; nunca o cobra. Nenhuma chamada a esta API movimenta dinheiro: a cobrança é confirmada pela pessoa no celular dela, da própria sessão e da própria carteira. Se sua chave vazasse, quem a tivesse poderia gerar cobranças em seu nome — chato, e revogável em um clique — mas não tiraria um centavo de ninguém.

Autenticação

Cada chamada leva sua credencial no header Authorization. O token tem a forma <key_id>.<segredo>; você o recebe inteiro ao criar a credencial no seu painel, e só nessa vez.

Authorization: Bearer tk_9f2c8ab1de4057.9d31...c0

No painel você também pode restringir de quais IPs a credencial é aceita. Se configurar, uma chamada de outro endereço é recusada mesmo com o segredo certo. É a diferença entre uma chave vazada que funciona de qualquer lugar e uma que não serve fora do seu servidor.

Limite: 120 chamadas por minuto por credencial.

Moeda

A moeda de tudo o que você cobra é a da sua carteira Toki. Não se escolhe por cobrança: um comércio cobra em uma moeda, a dele.

Os valores vão sempre na menor unidade dessa moeda, o padrão de qualquer gateway. O peso chileno não se subdivide, então 15990 são quinze mil novecentos e noventa pesos. Numa moeda com centavos, 1599 são 15,99.

GET/v1/comercio

Diz em que moeda você cobra, o nome do seu comércio e se a credencial é de teste ou de produção. Use para validar sua configuração sem criar uma cobrança de mentira.

{ "comercio": { "nombre": "Minha Loja", "moneda": "CLP", "modo": "prueba" } }

Se sua loja cobra em moeda diferente da sua carteira, não integre ainda. A Toki não converte: cobraria o número enviado como se fosse na sua moeda. Fale conosco antes.

Criar uma cobrança

POST/v1/cobros
CampoTipoO que é
montointeiro, obrigatórioNa menor unidade da sua moeda. Em CLP o peso não se divide: 15990 são quinze mil novecentos e noventa pesos. Numa moeda com centavos, 1599 são 15,99.
conceptotextoO que seu cliente vê. "Nota 4471", "Mesa 12".
referencia_externatextoSeu identificador. Também torna a cobrança idempotente: repetir com a mesma referência devolve a cobrança que já existe, em vez de criar outra.
url_retornohttpsPara onde seu cliente volta depois de pagar.
url_cancelacionhttpsPara onde volta se desistir.
expira_minutosinteiroEntre 1 e 1440. Padrão 15.
metadataobjetoO que você quiser guardar. Devolvemos igual no 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": "Nota 4471",
    "referencia_externa": "b-4471",
    "url_retorno": "https://minhaloja.com/obrigado",
    "url_cancelacion": "https://minhaloja.com/carrinho",
    "metadata": { "caixa": "3" }
  }'
{
  "cobro": {
    "id": "9cd827ca-d10d-4768-8627-265d875f1cd2",
    "monto": 15990,
    "moneda": "CLP",
    "concepto": "Nota 4471",
    "estado": "pending",
    "referencia_externa": "b-4471",
    "metadata": { "caixa": "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..."
  }
}

Use `url_checkout`: é a página de pagamento que hospedamos. qr_svg e deeplink ficam para quem quiser montar a própria tela — leia a seção seguinte antes de decidir isso.

A página de pagamento

Mandar seu cliente para a url_checkout é a forma recomendada de cobrar, e não só por comodidade.

Um QR não pode ser escaneado pelo mesmo celular que o mostra. Se seu cliente está comprando na sua loja pelo celular — o caso mais comum — uma imagem de QR não serve para nada. Nossa página detecta isso: no celular oferece abrir o app direto, e no desktop mostra o QR.

Ela também acompanha o status sozinha, mostra quanto falta para a cobrança vencer, e devolve o cliente ao seu site quando termina. E como a página é nossa, podemos melhorar o fluxo ou acrescentar meios de pagamento sem que você publique nada de novo.

Incorporar no seu site

Se preferir que seu cliente não saia da sua página, carregue-a em um iframe. Avisamos o resultado com postMessage, então você não precisa consultar nada pelo 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;      // verifique SEMPRE a origem
    if (e.data?.fuente !== 'toki') return;
    if (e.data.evento === 'cobro.paid') {
      // Confirme contra o SEU servidor antes de entregar: uma mensagem do
      // navegador qualquer um pode forjar. Isto serve para reagir na tela.
      mostrarObrigado();
    }
  });
</script>

O `postMessage` é para a interface, não para decidir. Antes de entregar um produto, confirme com GET /v1/cobros/{id} do seu servidor ou espere o webhook. Qualquer um pode mandar uma mensagem para sua página; ninguém pode forjar nossa resposta assinada.

O QR

GET/v1/cobros/{id}/qr

Devolve o código em SVG, para ficar nítido tanto em uma bobina térmica quanto em uma tela de caixa. É a única rota que não pede credencial: ela vai impressa em notas, onde não há onde colocar um header. O que expõe é o mesmo id que já vai dentro do código, e com esse id só dá para pagar.

Use quando o pagamento acontece na sua frente — um caixa, uma nota impressa — onde seu cliente tem o próprio celular para escanear. Para cobrar pela internet, use a página de pagamento.

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

Consultar uma cobrança

GET/v1/cobros/{id}

Devolve a cobrança com seu estado: pending, paid, cancelled ou expired. É o caminho reserva se você não puder receber webhooks — um caixa atrás de uma rede fechada integra só com isto.

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

Cancelar uma cobrança

POST/v1/cobros/{id}/anular

Só enquanto estiver pending. Uma cobrança já paga não se cancela por aqui: devolver dinheiro é um reembolso, com fluxo próprio. Cancelar uma cobrança paga deixaria a contabilidade dizendo uma coisa e a cobrança outra.

Serviços e assinaturas

Um serviço é algo que as pessoas assinam: um plano, uma associação, uma mensalidade. Você publica pela API e gera um link para alguém assinar. Dali em diante, a cobrança se repete sozinha.

Ninguém fica assinado sem confirmar. O link não ativa nada: a pessoa vê quanto e de quanto em quanto tempo será cobrada, e confirma no app dela. Depois aparece em Dinheiro → Assinaturas, onde ela pode cancelar sem passar por você.

Publicar um serviço

POST/v1/servicios
CampoTipoO que é
nombretexto, obrigatórioO que seu cliente vê. "Plano mensal", "Mensalidade".
preciointeiro, obrigatórioNa menor unidade da sua moeda, igual a `monto`.
descripciontextoO que inclui.
recurrentebooleanoPadrão true. Com false fica publicado mas não aceita assinatura: para cobrar uma vez use /v1/cobros.
cadainteiroPadrão 1.
unidadtextoday, week, month, semester ou year. Padrão month.
dia_de_cobrointeiro 1–31O dia do mês em que todos são cobrados. Só com unidade month, semester ou year. Sem ele, cada pessoa é cobrada no dia em que assinou.
dia_de_semanainteiro 0–6Só com unidade week. 0 é domingo.
politica_mes_cortotextoO que fazer quando o dia não existe no mês: last, first_next ou skip. Só com dia_de_cobro acima de 28.
referencia_externatextoSeu identificador. Torna a criação idempotente.

Os campos que não correspondem à unidade escolhida são recusados, não ignorados: mandar dia_de_semana em um plano mensal devolve 400, para você não ficar achando que configurou algo.

curl -X POST https://api.toki.lat/v1/servicios \
  -H "Authorization: Bearer $TOKI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Plano mensal",
    "precio": 19990,
    "cada": 1,
    "unidad": "month",
    "dia_de_cobro": 1,
    "referencia_externa": "plano-mensal"
  }'
{
  "servicio": {
    "id": "b97c3820-b9ea-4352-aba4-a459aaa02015",
    "nombre": "Plano mensal",
    "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": "plano-mensal",
    "creado_en": "2026-08-25T15:50:15Z"
  }
}

Listar e editar

GET/v1/servicios

Lista todo o catálogo do comércio — inclusive o publicado pelo app, não só o criado pela API.

PATCH/v1/servicios/{id}
CampoTipoO que é
activobooleanoCom false deixa de aceitar novas assinaturas. As vigentes continuam sendo cobradas.
preciointeiroVale para quem assinar depois.
nombretexto
descripciontexto

Mudar o preço não afeta quem já assinou. O valor dela foi fixado quando aceitou; aumentá-lo daqui seria cobrar algo que ela nunca autorizou.

Gerar o link de assinatura

POST/v1/suscripciones
CampoTipoO que é
servicio_iduuid, obrigatórioO serviço a ser assinado.
referencia_externatextoSeu identificador. Torna a operação idempotente.
expira_minutosinteiroEntre 1 e 1440. Padrão 60: assinar se pensa mais do que pagar uma nota.
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" }'

A resposta tem a mesma forma de uma cobrança —mesma url_checkout, mesmos estados— mais o servicio_id. Você consulta o status com GET /v1/cobros/{id} e recebe o webhook cobro.paid quando a pessoa confirma.

Modo de teste

Crie uma credencial em modo teste no seu painel e desenvolva com ela sem movimentar um centavo. A chave é diferente —começa com tk_test_— para não se confundir com a de produção, nem num log nem num arquivo de configuração.

Uma cobrança de teste não pode ser paga com dinheiro real, e uma real não pode ser dada como paga simulando. Os dois cadeados estão no banco, não nesta documentação: não dependem de ninguém tomar cuidado.

Dar uma cobrança de teste como paga

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

Marca a cobrança como paga e dispara seu webhook, sem escrever um único lançamento contábil. É assim que você testa sua tela de obrigado e seu tratamento do evento antes de cobrar de alguém.

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

Cobranças de teste vêm com es_prueba: true na resposta. Se sua integração as vê em produção, você subiu a credencial errada.

Reembolsos

POST/v1/cobros/{id}/reembolsar

Devolve o dinheiro de uma cobrança paga. Sem monto, devolve tudo o que resta; com monto, devolve essa parte, e você pode chamar de novo até completar.

CampoTipoO que é
montointeiroQuanto devolver, na menor unidade da sua moeda. Omita para devolver tudo o que resta.

Seu cliente recupera 100% do que pagou, e esse valor sai inteiro da sua carteira. A comissão não volta: a cobrança já foi processada, então já estava ganha. Na prática você devolve um pouco mais do que recebeu — a diferença é a comissão daquela venda.

Vale planejar: para devolver uma venda você precisa ter o valor completo disponível, não só o que recebeu por ela. Se já sacou e não cobre, o reembolso falha com um erro dizendo quanto falta — preferimos isso a deixar um saldo negativo que alguém terá de perseguir depois.

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 }'

A resposta traz reembolsado, o total devolvido até agora. E avisamos por webhook: cobro.partially_refunded enquanto sobrar algo, cobro.refunded quando tudo voltou.

WooCommerce

Se sua loja é WooCommerce, você não precisa escrever código: existe um plugin que faz tudo isso por você.

Baixar o plugin
  • 1. Instale em Plugins → Adicionar novo → Enviar plugin.
  • 2. Vá em WooCommerce → Configurações → Pagamentos → Toki.
  • 3. Cole sua credencial de teste e deixe o modo de teste ligado.
  • 4. Copie a "URL para avisos" que aparece ali e cole na sua credencial da Toki, junto com o segredo de assinatura.
  • 5. Faça um pedido completo. POST /v1/cobros/{id}/simular-pago dá como pago sem movimentar dinheiro.
  • 6. Quando funcionar, cole a credencial de produção e desligue o modo de teste.

Faz cobranças, reembolsos totais e parciais pelo próprio pedido, e verifica a assinatura de cada aviso. O pedido é marcado como pago pelo webhook, nunca porque o cliente voltou à loja — voltar não prova que pagou.

Ao salvar, o plugin pergunta à Toki em que moeda você cobra e avisa se não bate com a da sua loja. A Toki não converte moedas, então nesse caso o método não aparece no checkout em vez de cobrar um número na moeda errada.

Webhooks

Se você configurar uma URL https no painel, avisamos ali quando a cobrança muda de estado: cobro.paid, cobro.cancelled ou cobro.expired. O corpo traz o evento e a cobrança completa.

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

Cada entrega vai assinada no header X-Toki-Firma, na forma t=<epoch>,v1=<hmac>. O HMAC é SHA-256 sobre ${t}.${corpo} com o segredo de webhook da sua credencial. Verifique sempre: sem isso, qualquer um que conheça sua URL pode dizer que pagou.

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

function verificar(corpo, cabecalho, segredo) {
  const p = Object.fromEntries(cabecalho.split(',').map((x) => x.split('=', 2)));
  const t = Number(p.t);
  // O timestamp vai DENTRO do que é assinado: senão, dá para reenviar um
  // webhook antigo com um t novo e a assinatura continuaria válida.
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const esperado = createHmac('sha256', segredo).update(`${t}.${corpo}`).digest('hex');
  const a = Buffer.from(esperado, 'hex');
  const b = Buffer.from(p.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Tratamos qualquer 2xx como entregue. Se seu servidor falhar, tentamos de novo em 1, 5, 25 e 125 minutos, e então desistimos: o último erro fica visível no seu painel. Webhooks podem chegar repetidos, então trate o `id` da cobrança como chave e faça seu processamento idempotente.

O que você recebe

De cada cobrança, a Toki desconta sua comissão e liquida o resto na hora. A API não devolve isso como número fixo porque não é: depende do tipo de cobrança, do seu plano, e de qualquer tarifa acordada com você, que prevalece sobre as demais.

Vale saber: cobrar pela API paga a tarifa de venda presencial, a mais baixa do catálogo — porque você está trazendo seu próprio sistema e a Toki só entra com o meio de pagamento. Vender pelo marketplace da Toki, com catálogo e entrega, custa bem mais. As assinaturas têm tarifas próprias, e cobram a adesão diferente das renovações.

A sua, já com tudo aplicado, está no seu painel: Empresa → Dinheiro, junto ao detalhe de cada liquidação.

Erros

Todos os erros têm a mesma forma. O code é estável e pensado para você comparar no seu código; a mensaje é para você ler.

{ "error": { "code": "credencial_invalida", "mensaje": "..." } }
codeHTTPO que aconteceu
sin_credencial401Falta o header Authorization.
credencial_invalida401Chave, segredo ou IP de origem que não batem. Não dizemos qual, de propósito.
sin_alcance403A credencial não tem permissão para essa operação.
cuerpo_invalido400Falta um campo ou ele tem o tipo errado.
no_encontrado404Não existe essa cobrança para esta credencial.
estado_invalido409A cobrança não está mais pendente.
demasiadas_solicitudes429Você passou das 120 chamadas por minuto.
error_interno500Falha nossa. Repetir é seguro se você mandar referencia_externa.

Antes de ir para produção

  • — Guarde o segredo onde guarda os outros segredos, nunca no código nem no front.
  • — Configure a lista de IPs. É o cadeado que continua servindo se o segredo vazar.
  • — Mande sempre referencia_externa: é o que impede cobrar duas vezes quando a rede falha no meio.
  • — Verifique a assinatura de cada webhook, e não decida nada com um postMessage.
  • — Tenha um plano para quando o webhook não chegar: consulte o status antes de entregar.

Entrar com o Toki

Deixe que as pessoas entrem no seu site com a conta Toki. Elas aprovam pelo telefone com o rosto, e você recebe apenas os dados que autorizaram.

É OAuth 2.0 padrão (authorization code + PKCE), não um fluxo inventado. Use a biblioteca que você já conhece, as propriedades de segurança já foram estudadas, e quem nunca ouviu falar do Toki consegue integrar sem confiar na nossa criptografia.

Registre a sua aplicação

Em toki.lat/connect você registra o app e recebe um client_id e um client_secret. O segredo aparece uma única vez: guardamos só o hash, então não é que não queiramos mostrá-lo depois — nós não o temos.

Você pode integrar já com as permissões não sensíveis. Para documento, telefone, empresas ou autorizar operações revisamos o app antes: nome, logo e responsável declarado. Um app chamado "Toki Pagamentos" com o nosso logo transformaria a tela de consentimento — justamente onde a pessoa confia — numa ferramenta de phishing.

O fluxo

Você manda a pessoa para a tela de consentimento, ela volta com um código, e o seu servidor troca esse código por um token.

GET/autorizar
https://toki.lat/autorizar
  ?client_id=seu_client_id
  &redirect_uri=https://seusite.com/oauth/callback
  &response_type=code
  &scope=perfil+email
  &state=<valor aleatório seu>
  &code_challenge=<SHA-256 do verifier, base64url>
  &code_challenge_method=S256

A pessoa vê o seu nome, o domínio real do seu site, quem responde por ele, e cada dado que você pede em linguagem humana. Se aprovar, volta ao seu redirect_uri com code e o seu state intacto.

POST/api/oauth/token
grant_type=authorization_code
code=<o codigo>
redirect_uri=https://seusite.com/oauth/callback
client_id=seu_client_id
client_secret=seu_segredo
code_verifier=<o verifier original>

Devolve access_token, refresh_token, expires_in e o scope realmente concedido — que pode ser menor do que você pediu. Leia: é a única forma de saber quais dados você tem de verdade.

GET/api/oauth/userinfo

Com Authorization: Bearer <access_token>. Cada campo só é devolvido se a permissão estiver no token. sub vem sempre: é o identificador estável da pessoa.

POST/api/oauth/revoke

Para encerrar a sessão do seu lado. Responde 200 sempre, mesmo para um token que nunca existiu: distingui-los transformaria esta rota num jeito de adivinhar tokens válidos.

Que dados você pode pedir

Peça o mínimo. Cada permissão extra é mais uma pergunta que a pessoa precisa responder antes de aprovar, e mais um motivo para não aprovar.

PermissãoO que devolveRevisão
`perfil`Nome, foto, usuário e se o Toki verificou a identidadeNão
`email`E-mailNão
`rut`Documento de identidade e seu paísSim
`telefono`TelefoneSim
`empresa`Empresas de que participa e seu cargoSim
`transacciones:autorizar`Autorizar operações em nome delaSim

O que você precisa saber

  • PKCE é obrigatório e só com S256. plain é rejeitado.
  • — O redirect_uri é comparado exatamente com os que você registrou. Sem curingas: aceitá-los deixaria o código chegar a um destino que você não controla.
  • — O código dura um minuto e é de uso único. Se for trocado duas vezes, assumimos que vazou e revogamos tudo o que foi emitido para essa pessoa no seu app.
  • — Os refresh_token rotacionam: cada troca devolve um novo e invalida o anterior. Reutilizar um antigo é tratado como roubo e encerra a sessão.
  • — O client_secret nunca vai para o navegador. Se o seu app não puder guardá-lo, a troca funciona mesmo assim com PKCE.
  • — A pessoa pode retirar o acesso quando quiser pela conta dela, e os tokens vivos são cortados junto. A sua integração tem que sobreviver a isso sem quebrar.