Guia para desenvolvedores

Ligue o clube no seu app em 3 passos

Cada passo tem uma explicação em português simples e o código pronto para copiar. Dá para ler mesmo sem ser programador.

1Crie uma chave

No painel. Ela fica guardada só no seu servidor.

2Peça um link e abra

Seu servidor pede o link do clube; o app abre o link numa tela web.

3Avise o que aconteceu

Corrida concluída? Seu servidor avisa e o clube dá as moedas.

0

Glossário rápido

Cinco palavras que aparecem no guia inteiro.

Chave secreta
Uma senha comprida que identifica o seu app para o Clube Coins. Começa com ck_live_.É como a chave do cofre da loja: fica com o gerente, nunca no balcão.
Servidor
O computador da sua empresa que conversa com o app (o “backend”). Só ele pode guardar a chave secreta.É a cozinha do restaurante: o cliente não entra lá.
Webview
Uma tela do app que mostra uma página da web por dentro, sem abrir o navegador.É uma janela do app que dá para o clube.
Evento
Um aviso do seu servidor dizendo que algo aconteceu (ex.: corrida concluída). O clube transforma isso em moedas.É o “carimbo” no cartão fidelidade.
Webhook
O caminho contrário: o clube avisa o seu servidor quando algo acontece lá (ex.: moedas creditadas).É o clube “ligando de volta” para você.
1

Criar a chave

O que é isso, em português

Antes de tudo, o seu app precisa de uma “senha de sistema” para falar com o Clube Coins. Você cria essa chave no painel e entrega para quem cuida do servidor.

No painel clube-console.usecoins.tech, clique em:

  1. Meu clube
  2. Conectar meu app
  3. Criar chave secreta
  • 1 vezA chave inteira aparece só ao ser criada. Copie na hora.
  • Só no servidorGuarde numa variável de ambiente, ex.: CLUBE_SECRET_KEY.
  • Na horaRevogar uma chave vale imediatamente.

Os dois tipos de chave

TipoOnde usarO que pode
ck_live_… secretaSó no servidor do appAbrir sessões, enviar eventos, consultar saldo (conforme as permissões da chave)
pk_live_… públicaPode ir dentro do app (SDK)Só ler a marca (GET /v1/config)

Nunca coloque a chave secreta dentro do aplicativo. Quem tiver a chave abre o clube como qualquer usuário. Nós guardamos só um resumo (hash) dela, por isso ela aparece uma única vez. Se vazar, revogue e crie outra.

2

Testar a chave

O que é isso, em português

Um “alô, está me ouvindo?”. Se a chave estiver certa, o Clube Coins responde com o nome do seu clube, o ambiente e o plano.

Toda chamada vai para o endereço base https://clube-api.usecoins.tech e leva a chave no cabeçalho Authorization:

Authorization: Bearer ck_live_…
GET/v1/ping

Cole no terminal do servidor (troque a variável pela sua chave, se precisar):

curl https://clube-api.usecoins.tech/v1/ping \
  -H "Authorization: Bearer $CLUBE_SECRET_KEY"

Resposta com sucesso: os dados do clube, do ambiente e do plano. Erro 401 com INVALID_KEY? A chave está ausente, errada, vencida ou revogada. Veja Erros e limites.

3

Abrir o clube para um usuário

O que é isso, em português

Quando a pessoa toca em “Clube” no seu app, o app pede ao seu servidor um link de entrada. O servidor pede esse link ao Clube Coins, dizendo quem é a pessoa. O link já leva ela logada, sem senha.

É como um ingresso de uso único: vale por 2 minutos e só para uma entrada.

POST/v1/sessions

Permissão sessions:write. O usuário não tem senha: a identidade é a do seu app. O primeiro acesso cria o cadastro dele no clube.

O que enviar

CampoO que é
user.idobrigatórioID do usuário no seu app (ex.: "8812")
user.audiencePASSENGER, DRIVER ou outro criado no console
user.nameopcionalNome da pessoa (ex.: "Paula Souza")
user.phoneopcionalTelefone (ex.: "+5531999990000")
startPathopcionalTela inicial do clube; começa com /app (ex.: /app/cupons)

Código

curl -X POST https://clube-api.usecoins.tech/v1/sessions \
  -H "Authorization: Bearer $CLUBE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user": { "id": "8812", "audience": "PASSENGER", "name": "Paula Souza" },
    "startPath": "/app/cupons"
  }'

Dica: a linguagem escolhida vale para todos os exemplos da página. Use as setas ← → para trocar de aba pelo teclado.

O que volta (201)

{
  "launchUrl": "https://clube.usecoins.tech/clube/entrar#t=…",
  "launchToken": "…",
  "expiresAt": "2026-11-07T12:02:00.000Z",
  "user": { "id": "…", "externalUserId": "8812", "audience": "PASSENGER", "created": true }
}
  • 2 minO link vale 2 minutos e uma vez só: peça um novo a cada abertura.
  • 1 diaDepois de entrar, a sessão no clube dura 1 dia.
  • id + públicoO mesmo id com outro audience é outra pessoa (ex.: passageiro e motorista).
4

Mostrar o clube no app

O que é isso, em português

Agora o app abre aquele link numa tela própria. No Android, o nosso kit (SDK) faz tudo sozinho. Nas outras plataformas, você abre o link numa webview e escuta alguns avisos simples que o clube manda.

Android: SDK pronto

Abre o clube numa tela pronta, já logado e com a sua marca. Java puro, sem dependências, Android 5.0+ (minSdk 21). Baixe o AAR em Downloads e coloque em app/libs/. Depois, no app/build.gradle:

dependencies {
    implementation files('libs/clube-coins-android-0.1.0.aar')
}

O SDK já declara a permissão INTERNET e a tela ClubeCoinsActivity. O launchProvider chama o seu servidor, que pede o link com a chave secreta (passo 3). Quando a sessão termina, o SDK pede um link novo sozinho (uma vez).

val config = ClubeCoinsConfig.Builder()
    .launchProvider { startPath, callback ->
        // Chame o SEU servidor (Retrofit, OkHttp, Ktor…), que chama POST /v1/sessions e devolve o launchUrl.
        meuServidor.linkDoClube(usuarioId, startPath,
            onOk = { url -> callback.onSuccess(url) },
            onErro = { e -> callback.onError(e) })
    }
    .startPath("/app/cupons")          // opcional
    .statusBarColor(0xFF1A1446.toInt()) // opcional: cor do seu app
    .listener { event -> Log.d("Clube", event.toString()) }
    .build()

ClubeCoins.open(this, config)

Já tem o link? ClubeCoins.open(context, launchUrl).

Avisos do SDK (ClubeCoinsListener)

TipoQuando
READYO clube abriu (path)
NAVIGATETroca de tela (path)
SESSION_EXPIREDLink vencido/usado ou sessão terminou: o SDK pede um link novo uma vez sozinho
CLOSE_REQUESTEDA pessoa tocou em “Voltar ao app”: o SDK fecha
ERRORFalha ao pedir o link ou ao carregar (reason)
DISMISSEDA tela do clube fechou

O que o SDK já cuida: webview segura (só o endereço do link abre dentro; sem acesso a arquivos; sem conteúdo misto); links de fora (ajuda, WhatsApp, telefone, e-mail) abrem no app certo ou no navegador; o botão voltar navega dentro do clube e fecha no início; rotação e tema escuro não recarregam; sem internet mostra “Tentar de novo”; a ponte window.ClubeCoinsAndroid aceita mensagens só do endereço do clube.

O endpoint no seu servidor (Node.js)

É este endereço que o meuServidor.linkDoClube chama. Ele confere quem está logado no seu app e pede o link ao Clube Coins.

app.post('/clube/link', autenticado, async (req, res) => {
  const r = await fetch('https://clube-api.usecoins.tech/v1/sessions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.CLUBE_SECRET_KEY}` },
    body: JSON.stringify({ user: { id: req.user.id, audience: 'PASSENGER', name: req.user.nome }, startPath: req.body.startPath }),
  });
  const body = await r.json();
  if (!r.ok) return res.status(502).json(body);
  res.json({ launchUrl: body.launchUrl });
});

Outras plataformas: webview + avisos

Abra o launchUrl numa webview com JavaScript e cookies ligados. O clube manda avisos para o app como JSON { "source": "clube-coins", "type": … }:

AvisoQuandoO que o app faz
readyClube aberto (path)Esconder o “carregando”
navigateTroca de tela (path)Opcional (ex.: estatística)
session_expiredLink usado/vencido ou sessão terminouPedir um launchUrl novo e reabrir
closePessoa tocou em “Voltar ao app”Fechar a webview

Onde receber os avisos em cada plataforma:

PlataformaCanal
AndroidaddJavascriptInterface(obj, "ClubeCoinsAndroid") com @JavascriptInterface postMessage(String json)
iOS (WKWebView)userContentController.add(handler, name: "clubeCoins")
React Native<WebView onMessage={…} /> (ReactNativeWebView.postMessage)
FlutterJavaScriptChannel(name: 'ClubeCoinsFlutter', …)

O SDK Android faz tudo isso por você. SDKs para iOS, React Native e Flutter vêm em seguida; até lá, use a webview com os canais acima.

5

Dar moedas: eventos

O que é isso, em português

Toda vez que algo bom acontece no seu app (uma corrida concluída, um pedido entregue), o seu servidor avisa o clube. O clube confere as regras que você montou no painel e credita as moedas.

Pode repetir sem medo: cada aviso tem um número único (o id). Se o mesmo aviso chegar duas vezes (ex.: a internet caiu e o servidor tentou de novo), a pessoa ganha só uma vez.

POST/v1/events

Permissão events:write. Idempotente: o mesmo type + id nunca credita duas vezes (a repetição responde 200 com "duplicate": true).

CampoO que é
typeobrigatórioO que aconteceu (ex.: ride.completed, order.delivered)
idobrigatórioID único do evento no seu app (ex.: "OS-10293")
userobrigatórioQuem ganha: { "id": "8812", "audience": "PASSENGER" }
amountCentsopcionalValor em centavos (ex.: 2550 = R$ 25,50)
distanceMetersopcionalDistância em metros
occurredAtopcionalQuando aconteceu (ex.: 2026-11-07T11:58:00-03:00)
dataopcionalInformações extras, guardadas junto
curl -X POST https://clube-api.usecoins.tech/v1/events \
  -H "Authorization: Bearer $CLUBE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ride.completed",
    "id": "OS-10293",
    "user": { "id": "8812", "audience": "PASSENGER" },
    "amountCents": 2550,
    "distanceMeters": 7300,
    "occurredAt": "2026-11-07T11:58:00-03:00",
    "data": { "cidade": "BH" }
  }'

O que volta

{ "status": "CREDITED" | "NO_REWARD" | "REJECTED", "coins": 25, "reason": null, "duplicate": false, … }
statusEm português
CREDITEDMoedas creditadas (quantas: coins)
NO_REWARDEvento aceito, mas nenhuma regra deu moedas
REJECTEDEvento recusado (o motivo vem em reason)

ride.completed usa as Campanhas de Coins do clube; os outros tipos usam as Regras de ganho (fixo e/ou por R$, valor mínimo, teto por dia).

6

Mostrar o saldo fora do clube

O que é isso, em português

Opcional. Serve para mostrar “Você tem 48 Moedas” no menu do seu app, sem abrir o clube.

GET/v1/users/{audience}/{id}/wallet

Permissão users:read.

curl https://clube-api.usecoins.tech/v1/users/PASSENGER/8812/wallet \
  -H "Authorization: Bearer $CLUBE_SECRET_KEY"
{ "user": { "externalUserId": "8812", "audience": "PASSENGER" }, "balance": 48,
  "coinName": { "singular": "Moeda 53", "plural": "Moedas 53" },
  "expiring": [{ "expiresAt": "2027-02-04T…", "amount": 48 }] }

expiring mostra quantas moedas vencem e quando: bom para lembrar a pessoa de usar.

Bônus: a marca do clube

GET/v1/config

Funciona com a chave pública (pk_live_…). Devolve nome, cores, logo, nome da moeda e públicos: útil para o app pintar o botão de entrada do clube.

7

Webhooks: o clube avisa você

O que é isso, em português

Opcional. O clube chama um endereço do seu servidor quando algo acontece lá (ex.: a pessoa ganhou moedas). Assim o app pode mandar uma notificação, por exemplo.

Cada aviso vem com uma assinatura, como um lacre: o seu servidor confere o lacre para ter certeza de que o aviso veio mesmo do Clube Coins.

Cadastre o endereço (https) e gere o segredo no console (Meu clube › Conectar meu app › Avisos automáticos). Avisos enviados hoje:

AvisoQuando
coins.earnedMoedas creditadas
webhook.testTeste disparado pelo console

Falhas são reenviadas. Cabeçalhos: Clube-Event (o tipo do aviso) e Clube-Signature: t=<unix>,v1=<hmac>, onde hmac = HMAC-SHA256(segredo, t + "." + corpo) em hexadecimal. Confira a assinatura e recuse avisos com t muito antigo.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SEGREDO = process.env.CLUBE_WEBHOOK_SECRET; // gerado no console

// Use o corpo BRUTO (o texto exato que chegou), não o JSON já convertido.
app.post('/webhooks/clube', express.raw({ type: '*/*' }), (req, res) => {
  const header = req.get('Clube-Signature') || '';
  const partes = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = partes.t;
  const corpoBruto = req.body.toString('utf8');

  const esperado = crypto.createHmac('sha256', SEGREDO).update(`${t}.${corpoBruto}`).digest();
  const recebido = Buffer.from(partes.v1 || '', 'hex');
  const assinaturaOk = recebido.length === esperado.length && crypto.timingSafeEqual(recebido, esperado);
  const recente = Math.abs(Date.now() / 1000 - Number(t)) < 300; // sugestão: até 5 minutos

  if (!assinaturaOk || !recente) return res.status(400).send('assinatura inválida');

  const tipo = req.get('Clube-Event'); // ex.: coins.earned
  const aviso = JSON.parse(corpoBruto);
  // … faça o que precisar (ex.: notificação no app)
  res.sendStatus(200);
});
8

Erros e limites

O que é isso, em português

Quando algo dá errado, a resposta diz o motivo em texto e num código fixo. Use o código para decidir o que fazer.

Erros vêm como:

{ "error": "mensagem", "code": "CODIGO" }
HTTPcodeO que fazer
401INVALID_KEYChave ausente, errada, vencida ou revogada
403CLUB_SUSPENDEDClube suspenso: fale com o Clube Coins
403SECRET_KEY_REQUIRED / SCOPE_MISSINGUse a chave secreta com a permissão certa
400AUDIENCE, START_PATH…Corrija o corpo da chamada
403USER_BLOCKEDUsuário bloqueado no console
429RATE_LIMITEDEspere Retry-After segundos (cabeçalhos RateLimit-*)
429QUOTA_EXCEEDED / MAU_LIMITLimite do plano no mês

Permissões de cada chamada

ChamadaPermissão da chave
POST /v1/sessionssessions:write
POST /v1/eventsevents:write
GET /v1/users/{audience}/{id}/walletusers:read
GET /v1/configChave pública basta
✓

Pronto para produção

Marque conforme for conferindo. Fica salvo neste navegador.

0 de 9

Travou em algum passo? Escreva para contato@usecoins.tech com o code do erro e o horário da chamada.