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ê.
Criar a chave
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:
- Meu clube
- Conectar meu app
- 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
| Tipo | Onde usar | O que pode |
|---|---|---|
ck_live_… secreta | Só no servidor do app | Abrir sessões, enviar eventos, consultar saldo (conforme as permissões da chave) |
pk_live_… pública | Pode 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.
Testar a chave
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_…
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.
Abrir o clube para um usuário
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.
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
| Campo | O que é | |
|---|---|---|
user.id | obrigatório | ID do usuário no seu app (ex.: "8812") |
user.audience | PASSENGER, DRIVER ou outro criado no console | |
user.name | opcional | Nome da pessoa (ex.: "Paula Souza") |
user.phone | opcional | Telefone (ex.: "+5531999990000") |
startPath | opcional | Tela 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"
}'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: '8812', audience: 'PASSENGER', name: 'Paula Souza' },
startPath: '/app/cupons',
}),
});
const body = await r.json();
if (!r.ok) throw new Error(`${body.code}: ${body.error}`);
console.log(body.launchUrl); // entregue este link ao app<?php
$ch = curl_init('https://clube-api.usecoins.tech/v1/sessions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('CLUBE_SECRET_KEY'),
],
CURLOPT_POSTFIELDS => json_encode([
'user' => ['id' => '8812', 'audience' => 'PASSENGER', 'name' => 'Paula Souza'],
'startPath' => '/app/cupons',
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new Exception($body['code'] . ': ' . $body['error']);
}
echo $body['launchUrl']; // entregue este link ao appimport os
import requests
r = requests.post(
"https://clube-api.usecoins.tech/v1/sessions",
headers={"Authorization": f"Bearer {os.environ['CLUBE_SECRET_KEY']}"},
json={
"user": {"id": "8812", "audience": "PASSENGER", "name": "Paula Souza"},
"startPath": "/app/cupons",
},
timeout=10,
)
body = r.json()
if not r.ok:
raise RuntimeError(f"{body['code']}: {body['error']}")
print(body["launchUrl"]) # entregue este link ao appDica: 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
idcom outroaudienceé outra pessoa (ex.: passageiro e motorista).
Mostrar o clube no app
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)ClubeCoins.open(activity, new ClubeCoinsConfig.Builder()
.launchProvider((startPath, callback) -> meuServidor.linkDoClube(startPath, callback))
.build());Já tem o link? ClubeCoins.open(context, launchUrl).
Avisos do SDK (ClubeCoinsListener)
| Tipo | Quando |
|---|---|
READY | O clube abriu (path) |
NAVIGATE | Troca de tela (path) |
SESSION_EXPIRED | Link vencido/usado ou sessão terminou: o SDK pede um link novo uma vez sozinho |
CLOSE_REQUESTED | A pessoa tocou em “Voltar ao app”: o SDK fecha |
ERROR | Falha ao pedir o link ou ao carregar (reason) |
DISMISSED | A 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": … }:
| Aviso | Quando | O que o app faz |
|---|---|---|
ready | Clube aberto (path) | Esconder o “carregando” |
navigate | Troca de tela (path) | Opcional (ex.: estatística) |
session_expired | Link usado/vencido ou sessão terminou | Pedir um launchUrl novo e reabrir |
close | Pessoa tocou em “Voltar ao app” | Fechar a webview |
Onde receber os avisos em cada plataforma:
| Plataforma | Canal |
|---|---|
| Android | addJavascriptInterface(obj, "ClubeCoinsAndroid") com @JavascriptInterface postMessage(String json) |
| iOS (WKWebView) | userContentController.add(handler, name: "clubeCoins") |
| React Native | <WebView onMessage={…} /> (ReactNativeWebView.postMessage) |
| Flutter | JavaScriptChannel(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.
Dar moedas: eventos
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.
Permissão events:write. Idempotente: o mesmo type + id nunca credita duas vezes (a repetição responde 200 com "duplicate": true).
| Campo | O que é | |
|---|---|---|
type | obrigatório | O que aconteceu (ex.: ride.completed, order.delivered) |
id | obrigatório | ID único do evento no seu app (ex.: "OS-10293") |
user | obrigatório | Quem ganha: { "id": "8812", "audience": "PASSENGER" } |
amountCents | opcional | Valor em centavos (ex.: 2550 = R$ 25,50) |
distanceMeters | opcional | Distância em metros |
occurredAt | opcional | Quando aconteceu (ex.: 2026-11-07T11:58:00-03:00) |
data | opcional | Informaçõ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" }
}'const r = await fetch('https://clube-api.usecoins.tech/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.CLUBE_SECRET_KEY}`,
},
body: JSON.stringify({
type: 'ride.completed',
id: 'OS-10293', // o mesmo id nunca credita duas vezes
user: { id: '8812', audience: 'PASSENGER' },
amountCents: 2550,
distanceMeters: 7300,
occurredAt: '2026-11-07T11:58:00-03:00',
data: { cidade: 'BH' },
}),
});
const body = await r.json();
if (!r.ok) throw new Error(`${body.code}: ${body.error}`);
console.log(body.status, body.coins); // ex.: CREDITED 25<?php
$ch = curl_init('https://clube-api.usecoins.tech/v1/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('CLUBE_SECRET_KEY'),
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'ride.completed',
'id' => 'OS-10293', // o mesmo id nunca credita duas vezes
'user' => ['id' => '8812', 'audience' => 'PASSENGER'],
'amountCents' => 2550,
'distanceMeters' => 7300,
'occurredAt' => '2026-11-07T11:58:00-03:00',
'data' => ['cidade' => 'BH'],
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status < 200 || $status >= 300) {
throw new Exception($body['code'] . ': ' . $body['error']);
}
echo $body['status'] . ' ' . $body['coins']; // ex.: CREDITED 25import os
import requests
r = requests.post(
"https://clube-api.usecoins.tech/v1/events",
headers={"Authorization": f"Bearer {os.environ['CLUBE_SECRET_KEY']}"},
json={
"type": "ride.completed",
"id": "OS-10293", # o mesmo id nunca credita duas vezes
"user": {"id": "8812", "audience": "PASSENGER"},
"amountCents": 2550,
"distanceMeters": 7300,
"occurredAt": "2026-11-07T11:58:00-03:00",
"data": {"cidade": "BH"},
},
timeout=10,
)
body = r.json()
if not r.ok:
raise RuntimeError(f"{body['code']}: {body['error']}")
print(body["status"], body["coins"]) # ex.: CREDITED 25O que volta
{ "status": "CREDITED" | "NO_REWARD" | "REJECTED", "coins": 25, "reason": null, "duplicate": false, … }status | Em português |
|---|---|
CREDITED | Moedas creditadas (quantas: coins) |
NO_REWARD | Evento aceito, mas nenhuma regra deu moedas |
REJECTED | Evento 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).
Mostrar o saldo fora do clube
Opcional. Serve para mostrar “Você tem 48 Moedas” no menu do seu app, sem abrir o clube.
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
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.
Webhooks: o clube avisa você
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:
| Aviso | Quando |
|---|---|
coins.earned | Moedas creditadas |
webhook.test | Teste 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);
});Erros e limites
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" }| HTTP | code | O que fazer |
|---|---|---|
| 401 | INVALID_KEY | Chave ausente, errada, vencida ou revogada |
| 403 | CLUB_SUSPENDED | Clube suspenso: fale com o Clube Coins |
| 403 | SECRET_KEY_REQUIRED / SCOPE_MISSING | Use a chave secreta com a permissão certa |
| 400 | AUDIENCE, START_PATH… | Corrija o corpo da chamada |
| 403 | USER_BLOCKED | Usuário bloqueado no console |
| 429 | RATE_LIMITED | Espere Retry-After segundos (cabeçalhos RateLimit-*) |
| 429 | QUOTA_EXCEEDED / MAU_LIMIT | Limite do plano no mês |
Permissões de cada chamada
| Chamada | Permissão da chave |
|---|---|
POST /v1/sessions | sessions:write |
POST /v1/events | events:write |
GET /v1/users/{audience}/{id}/wallet | users:read |
GET /v1/config | Chave pública basta |
Pronto para produção
Marque conforme for conferindo. Fica salvo neste navegador.
Travou em algum passo? Escreva para contato@usecoins.tech com o code do erro e o horário da chamada.