API v1

OFF Card — Referência da API

Base URL: https://offzinnn-api.onrender.com · Content-Type: application/json

Visão geral

ConceitoPapel
OFF Card OFFC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXXCredencial secreta. Faz login e paga. Armazenado apenas como hash.
ID B0000000000000000001Chave pública. Só recebe. Não permite login.
sessionIdToken Bearer (validade de 30 dias), obtido em login/registro.
txIdCódigo do comprovante (e chave de idempotência opcional enviada por você).

Valores são inteiros em OFF. Respostas de sucesso trazem "success": true; erros trazem "success": false e "error" com um código estável.

Autenticação

Authorization: Bearer <sessionId>
Content-Type: application/json

Sessão inválida ou expirada: 403 INVALID_SESSION. As sessões são as mesmas do OFF Bank, então uma conta antiga também pode usar a API.

Endpoints

POST/api/v1/card/registerPúblico

Cria uma conta e já emite o OFF Card e uma sessão. O cardCode só aparece nesta resposta.

Corpo
{
  "username": "alice",
  "password": "uma-senha-forte"
}
Resposta
{
  "success": true,
  "userId": "B0000000000000000001",
  "username": "alice",
  "cardCode": "OFFC-7K2MX-…",
  "sessionId": "<token>"
}

Erros: 400 INVALID_USERNAME / INVALID_PASSWORD · 409 USERNAME_TAKEN.

POST/api/v1/card/loginPúblico

Cria uma sessão Bearer. Use ou o card ou usuário + senha. O ID público nunca faz login.

Corpo
{ "cardCode": "OFFC-7K2MX-…" }
// ou
{ "username": "alice", "password": "…" }
Resposta
{
  "success": true,
  "userId": "B0000000000000000001",
  "username": "alice",
  "sessionId": "<token>"
}

Credenciais inválidas: 401 INVALID_CREDENTIALS. Após 10 falhas em 15 min o IP é bloqueado (429 IP_BLOCKED).

POST/api/v1/card/logoutBearer

Revoga a sessão informada no cabeçalho.

Resposta
{ "success": true }

GET/api/v1/card/meBearer

Dados da conta autenticada e saldo.

Resposta
{
  "success": true,
  "userId": "B0000000000000000001",
  "username": "alice",
  "offKey": "OFF-…",
  "balance": "1250",
  "card": { "exists": true, "last4": "7K2M" }
}

POST/api/v1/card/generateBearer

Gera um OFF Card (ou rotaciona, invalidando o anterior). Útil se o card vazou ou se a conta é antiga e ainda não tem card.

Resposta
{
  "success": true,
  "rotated": true,
  "cardCode": "OFFC-NEW…",
  "warning": "…"
}

POST/api/v1/card/transferBearer

Transfere OFF da conta autenticada para um ID público.

Corpo
{
  "toId": "B0000000000000000002",
  "amount": 50,
  "message": "pedido #1042",
  "txId": "pedido-1042"
}
Resposta
{
  "success": true,
  "txId": "OFFPAY-9F3A…",
  "clientTxId": "pedido-1042",
  "from": "B…01", "to": "B…02",
  "amount": "50",
  "balance": "1200",
  "receiptUrl": "/tx/OFFPAY-9F3A…"
}

toId aceita o ID público B… ou a Chave OFF OFF-…. amount é inteiro (sem casas decimais) e respeita os limites do OFF Pay (diário e noturno). txId é opcional e idempotente: repetir a mesma chamada devolve o resultado original com "duplicate": true, sem transferir de novo. Erros: 402 INSUFFICIENT_FUNDS · 404 RECIPIENT_NOT_FOUND · 409 DAILY_LIMIT (traz available) / NIGHT_LIMIT / RECIPIENT_BLOCKED · 403 ACCOUNT_BLOCKED · 400 INVALID_TO_ID / INVALID_AMOUNT / AMOUNT_TOO_SMALL / SAME_ACCOUNT.

POST/api/v1/card/transfer/cardPúblico (card)

Transfere usando o card do pagador, sem sessão. Pensado para servidores e lojas parceiras: o jogador informa o card e a loja cobra.

Corpo
{
  "cardCode": "OFFC-7K2MX-…",
  "toId": "B0000000000000000002",
  "amount": 50,
  "txId": "pedido-1042"
}
Resposta
Mesma resposta de /transfer.

Card inexistente: 404 CARD_NOT_FOUND. Nunca exponha o card do jogador no front-end público de terceiros; chame esta rota do seu servidor.

POST/api/v1/card/infoPúblico (card)

Saldo e dados da conta a partir do card, sem sessão.

Corpo
{ "cardCode": "OFFC-7K2MX-…" }
Resposta
{
  "success": true,
  "userId": "B…01",
  "username": "alice",
  "balance": "1250",
  "totalTransactions": 14
}

GET/api/v1/card/transactions?page=1&limit=20Bearer

Extrato paginado (limit máx. 50).

Resposta
{
  "success": true,
  "page": 1,
  "transactions": [
    { "id": "OFF…", "type": "transfer", "from": "B…01", "to": "B…02",
      "amount": "50", "message": null, "date": "…", "direction": "out" }
  ]
}

GET/api/v1/card/lookup/:idPúblico

Confirma que um ID público existe e devolve o nome (para mostrar "Pagar para alice?" antes de enviar).

Resposta
{ "success": true, "id": "B…02", "username": "bob" }

Inexistente: 404 ID_NOT_FOUND.

GET/api/v1/card/tx/:txidPúblico

Dados públicos de um comprovante. A página /tx/<txid> usa este endpoint.

Resposta
{
  "success": true,
  "tx": { "id": "OFF…", "type": "transfer", "date": "…",
          "from": "B…01", "to": "B…02", "amount": "50", "message": null }
}

GET/api/v1/card/statsPúblico

Retrato público do sistema (cache de 30 s).

Resposta
{
  "success": true,
  "totalAccounts": 120,
  "totalCards": 98,
  "totalOff": "5230100",
  "totalTransactions": 4821,
  "transactions24h": 312,
  "volume24h": "154320"
}

GET/api/v1/card/healthPúblico

Verificação de saúde.

Resposta
{ "ok": true, "uptime": 12345.67 }

Erros e limites

StatusCódigo
400INVALID_TO_ID · INVALID_AMOUNT · AMOUNT_TOO_SMALL · INVALID_TX_ID · SAME_ACCOUNT · INVALID_USERNAME · INVALID_PASSWORD · MISSING_CREDENTIALS
401INVALID_CREDENTIALS
402INSUFFICIENT_FUNDS (traz o balance)
403INVALID_SESSION · ACCOUNT_BLOCKED · ORIGIN_NOT_ALLOWED
404CARD_NOT_FOUND · RECIPIENT_NOT_FOUND · ID_NOT_FOUND · INVALID_TRANSACTION
409USERNAME_TAKEN · DAILY_LIMIT · NIGHT_LIMIT · RECIPIENT_BLOCKED
429RATE_LIMIT_EXCEEDED (média de 5 req/s por IP, rajada de 20) · IP_BLOCKED (10 falhas de login/card em 15 min)
500INTERNAL_ERROR

Limites

As transferências usam o mesmo motor do OFF Pay (site e bot): limite diário em janela móvel de 24 h e limite noturno por envio (horário de Fortaleza). Os valores são configurados em off_pay_settings; o padrão é 2.000 OFF por dia e 500 OFF por envio à noite.

Exemplo rápido (Node.js)

const API = "https://offzinnn-api.onrender.com/api/v1/card";

// cobrança na sua loja: chame do SERVIDOR, nunca do navegador
const r = await fetch(`${API}/transfer/card`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    cardCode: jogador.card,          // informado pelo jogador
    toId: "B0000000000000000002",    // ID público da sua loja
    amount: 50,
    txId: `pedido-${pedido.id}`      // idempotente
  })
});
const data = await r.json();
if (data.success) console.log("Pago! Comprovante:", data.txId);