Documentação Broski

Receba por MB WAY e Multibanco no seu negócio em Portugal, com uma API simples em https://api.broski.pt.

Introdução

A integração completa tem 4 passos:

  1. Ative a sua conta no painel (app.broski.pt) — verificação de dados e documento.
  2. Gere a sua chave secreta em Configurações → API.
  3. Crie pedidos com POST /v1/orders — MB WAY confirma em segundos; Multibanco emite um voucher pago em até alguns dias.
  4. Receba webhooks com a confirmação de cada pagamento e libere o produto/serviço.

Convenções: valores sempre em cêntimos (€19,90 = 1990); moeda EUR; respostas JSON com datas RFC 3339 (UTC); a API é versionada no path (/v1) e evolui de forma aditiva — campos novos podem aparecer, os existentes não mudam de significado.

Integre com IA — prompt pronto

Vai integrar usando um assistente de código (Claude, ChatGPT, Copilot…)? Copie o prompt abaixo e cole junto do seu pedido — ele contém tudo que a IA precisa saber sobre a nossa API, sem ela precisar navegar nas docs. Você também pode anexar o openapi.yaml.

Integre pagamentos MB WAY e Multibanco no meu checkout usando a API Broski.
Siga EXATAMENTE esta especificação — não invente campos nem endpoints.

## API Broski (gateway de pagamentos, Portugal, EUR)
Base URL: https://api.broski.pt
Autenticação: header "Authorization: Bearer sk_live_..." em toda chamada.
A chave fica SOMENTE no servidor (variável de ambiente) — nunca em browser,
app móvel ou repositório.
Valores sempre em CÊNTIMOS (inteiros): €19,90 = 1990. Moeda única: EUR.
Envie SOMENTE os campos documentados — campo desconhecido causa 400.

## Criar pedido — POST /v1/orders
Headers: Authorization, Content-Type: application/json e
Idempotency-Key (OBRIGATÓRIO, 1–128 chars — derive do id do pedido da loja,
ex.: "pedido-1042-v1"). Reenvio com a mesma chave e mesmo corpo devolve a
resposta original (header Idempotent-Replay: true); mesma chave com corpo
diferente responde 409 idempotency_key_reused.

Corpo (JSON):
- amount      int    obrigatório. MB WAY: 50–500000 (€0,50–€5.000).
                     Multibanco: 1–100000000.
- method      string obrigatório: "mbway" | "multibanco".
- phone       string obrigatório se mbway. Telemóvel PT: "+3519XXXXXXXX".
- external_reference string obrigatório, ÚNICO por conta (A-Za-z0-9_-. até
                     128). Duplicado responde 409 external_reference_taken.
- description string opcional (≤140) — nome do produto (aparece no painel
                     do lojista; não volta nas respostas da API).
- customer    objeto opcional: {name, email, phone, nif}. Armazenado
                     cifrado; NUNCA é devolvido pela API.

Resposta 201 (MB WAY):
{"id":"ord_...","status":"pending","amount":1990,"currency":"EUR",
 "method":"mbway","external_reference":"...","livemode":true,
 "created_at":"RFC3339"}
O cliente aprova no app MB WAY; a confirmação chega EM SEGUNDOS via webhook.

Resposta 201 (Multibanco):
{... "status":"awaiting_payment",
 "multibanco":{"entity":"11249","reference":"123 456 789",
               "amount":4990,"expires_at":"RFC3339"}}
REGRAS MULTIBANCO: exiba entidade, referência, valor e prazo ao cliente
(ele paga no homebanking/caixa em até alguns dias). "awaiting_payment" é
estado NORMAL e duradouro — não trate como erro nem crie pedido novo.

Erros possíveis: 400 validation_failed | invalid_json |
currency_not_supported | amount_below_fee; 401 unauthorized;
402 payment_failed (pedido desfeito — pode reusar a external_reference);
409 (acima); 413 body_too_large (>64KB); 429 rate_limited (120 req/min por
chave — implemente backoff exponencial). Envelope de erro:
{"error":{"type","code","message","param?"}} (message em PT-BR).

## Consultar — GET /v1/orders/{id}    (404 resource_not_found se não existir)
## Listar — GET /v1/orders?status=&method=&starting_after=  (25/página,
   {"data":[resumo...],"has_more":bool})
## Estornar — POST /v1/orders/{id}/refunds  corpo {"amount"?:int,"reason"?:str}
   amount omitido = estorno total. 201 {"id":"ref_...","amount":n,
   "status":"succeeded"}. Erros: 409 order_not_paid; 400 refund_exceeds_amount.

## Webhooks (OBRIGATÓRIO implementar — Multibanco confirma dias depois)
O lojista cadastra a URL HTTPS no painel Broski (Configurações → Webhooks) e
guarda o segredo "whbroski_..." (exibido uma vez). Implemente um endpoint
POST público que:
1. Lê o corpo CRU (bytes, antes de parsear JSON).
2. Verifica o header "Broski-Signature: t=<unix>,v1=<hex>" assim:
   esperado = HMAC-SHA256(secret, t + "." + corpo_cru) em hex;
   compare com v1 em TEMPO CONSTANTE; rejeite se |agora - t| > 300s.
   VETOR DE TESTE (sua implementação deve reproduzir):
   secret: whbroski_vetor_de_teste_nao_usar_em_producao
   t: 1750000000
   corpo: {"id":"evt_teste123","type":"order.paid","created":1750000000,"data":{"object":{"id":"ord_teste123","status":"paid"}}}
   header esperado: t=1750000000,v1=5fb448226598b14dea45ff94ee317c2c1edd464f87cd720b0d8673ea89731d4c
3. Responde 2xx em até 10s (processe assíncrono se precisar).
4. Deduplica pelo "id" do evento (entrega at-least-once, SEM ordem
   garantida) e confia no "status" do objeto, não no tipo do evento.

Envelope do evento:
{"id":"evt_...","type":"order.paid","created":unix,
 "data":{"object":{ ...pedido completo, como no GET... }}}
Eventos: order.awaiting_payment, order.paid, order.failed, order.expired,
order.refunded. Sem 2xx: retries por ~24h, depois dead-letter (reenvio
manual no painel).

## Regras de negócio INEGOCIÁVEIS
1. Libere o produto/serviço SOMENTE ao receber order.paid (nunca na criação
   do pedido, nunca confiando no browser do cliente).
2. O valor cobrado vem do SEU servidor — nunca aceite amount do cliente.
3. Trate order.failed/order.expired com mensagem clara e opção de tentar
   de novo; 402 payment_failed idem.
4. Guarde o "id" (ord_...) retornado e associe ao pedido da sua loja.
5. Não há endpoint de webhook via API — o cadastro é no painel.

## Ícones dos métodos no checkout (use estes, não invente)
Logotipos oficiais, sobre fundo CLARO. MB WAY é horizontal, Multibanco é
vertical — use alturas diferentes para equilíbrio visual:
<img src="https://docs.broski.pt/assets/mbway.svg" alt="MB WAY" height="28">
<img src="https://docs.broski.pt/assets/multibanco.svg" alt="Multibanco" height="40">
Documentação completa: https://docs.broski.pt

Selecione o bloco acima e copie (Ctrl/Cmd+A dentro do bloco → Ctrl/Cmd+C). O prompt é autossuficiente: payloads, respostas, webhooks com vetor de teste da assinatura e as regras de segurança.

Autenticação

Toda chamada leva Authorization: Bearer sk_live_…. A chave é criada no painel (até 10 ativas por conta — use uma por sistema e dê nomes: "Loja", "ERP"), exibida uma única vez, e pode ser revogada a qualquer momento.

Nunca use a chave no navegador, em apps móveis ou em repositórios. Ela pertence ao seu servidor. Chave exposta? Revogue no painel e gere outra — efeito imediato.

Primeiro pagamento

MB WAY — confirmação em segundos

curl https://api.broski.pt/v1/orders \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: pedido-1042-v1" \
  -H "Content-Type: application/json" \
  -d '{"amount":1990,"method":"mbway","phone":"+351912345678",
       "external_reference":"pedido-1042","description":"Curso de tráfego",
       "customer":{"name":"Ana Cliente","email":"ana@exemplo.pt"}}'

Resposta 201 com "status":"pending": o cliente recebe a notificação no app MB WAY e aprova. Em segundos chega o webhook order.paid (ou order.failed/order.expired). Limites MB WAY: €0,50 a €5.000 por pedido; telemóvel PT (+3519…) obrigatório.

Multibanco — voucher pago em dias

Mesma chamada com "method":"multibanco" (sem phone). A resposta 201 já vem "status":"awaiting_payment" com o voucher:

"multibanco":{"entity":"11249","reference":"123 456 789",
              "amount":4990,"expires_at":"2026-07-19T14:05:11Z"}
Seu dever de exibição: mostre entidade, referência e valor ao cliente (e o prazo). Ele paga no homebanking ou numa caixa Multibanco. awaiting_payment é um estado normal que pode durar dias — a confirmação chega pelo webhook order.paid. Não trate como erro nem crie um pedido novo.

Ícones para o seu checkout

Hospedamos os ícones dos métodos — use direto no seu checkout (SVG, cache imutável):

MB WAYMultibanco

<!-- MB WAY é horizontal; Multibanco é vertical (marca + texto embaixo) —
     alturas diferentes para equilíbrio visual. Use sobre fundo CLARO. -->
<img src="https://docs.broski.pt/assets/mbway.svg" alt="MB WAY" height="28">
<img src="https://docs.broski.pt/assets/multibanco.svg" alt="Multibanco" height="40">

São os logotipos vetoriais oficiais (marcas da SIBS), ~2 KB cada com cache de 24 h — pode referenciar direto do seu checkout.

Ciclo de vida do pedido

MétodoTransições
MB WAYpendingpaid · failed · expired
Multibancoawaiting_paymentpaid · failed · expired (voucher venceu)
Qualquerpaidrefunded (somente no estorno total)

Os estados finais não regridem. Um evento duplicado nunca re-transiciona um pedido.

Idempotência

O header Idempotency-Key é obrigatório na criação de pedidos e é a sua proteção contra cobrança dupla em timeouts/reenvios:

Use uma chave derivada do seu pedido (ex.: pedido-1042-v1). A external_reference é uma segunda barreira: é única por conta (409 external_reference_taken).

Estornos

curl https://api.broski.pt/v1/orders/ord_9f2c1ab34d/refunds \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"amount":500,"reason":"troca"}'   # omita amount p/ estorno total

Só pedidos paid podem ser estornados (409 order_not_paid); parciais são repetíveis até o total (400 refund_exceeds_amount acima disso). No estorno total o pedido vira refunded e o webhook order.refunded é emitido. O valor devolvido ao cliente segue o caminho do método original.

Webhooks

Essenciais no Multibanco (o pagamento chega dias depois). Cadastre a URL HTTPS no painel (Configurações → Webhooks) e guarde o segredo whbroski_… (mostrado uma vez). Formato do envelope e política de retries na referência.

Verificando a assinatura

Header Broski-Signature: t=<unix>,v1=<hex>, onde v1 = HMAC-SHA256(secret, t + "." + corpo_cru). Use o corpo cru (antes de parsear o JSON) e comparação em tempo constante; rejeite t a mais de 5 min do seu relógio.

Node.js
const crypto = require("crypto");
function verificar(sigHeader, corpoCru, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(sigHeader || "");
  if (!m) return false;
  const [_, t, v1] = m;
  if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false;
  const esperado = crypto.createHmac("sha256", secret)
    .update(t + "." + corpoCru).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
}
PHP
function verificar(string $sig, string $corpoCru, string $secret): bool {
  if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $sig, $m)) return false;
  if (abs(time() - (int)$m[1]) > 300) return false;
  $esperado = hash_hmac('sha256', $m[1] . '.' . $corpoCru, $secret);
  return hash_equals($esperado, $m[2]);
}
Python
import hmac, hashlib, time, re
def verificar(sig: str, corpo_cru: bytes, secret: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", sig or "")
    if not m or abs(time.time() - int(m[1])) > 300: return False
    esperado = hmac.new(secret.encode(), f"{m[1]}.".encode() + corpo_cru,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, m[2])

Vetor de teste

Sua implementação deve reproduzir exatamente este resultado:

secret : whbroski_vetor_de_teste_nao_usar_em_producao
t      : 1750000000
corpo  : {"id":"evt_teste123","type":"order.paid","created":1750000000,"data":{"object":{"id":"ord_teste123","status":"paid"}}}
header : t=1750000000,v1=5fb448226598b14dea45ff94ee317c2c1edd464f87cd720b0d8673ea89731d4c
Boas práticas: responda 2xx em ≤10 s (enfileire o processamento); deduplique pelo id do evento (entrega at-least-once, sem ordem garantida); consulte o log de entregas e o reenvio manual no painel quando algo falhar.

Erros e limites

Envelope único: {"error":{"type","code","message","param?"}} — catálogo completo na referência. Limites: 120 requisições/min por chave (acima → 429; recue com backoff exponencial), corpo até 64 KB, listagem em páginas de 25 com cursor starting_after.

Recebimentos

Cada venda confirmada entra no seu saldo (painel → Saldo) e passa por um breve período de retenção antes de ficar disponível. O saque para o seu IBAN é pedido no painel (Saques) e passa por revisão. Extrato completo, taxas aplicadas e comprovantes: tudo no painel.

Ambientes e testes

A API opera hoje em produção — o campo livemode nas respostas identifica o ambiente e já prepara a sua integração para o modo de teste sem breaking change.

Como testar com segurança hoje:

O modo de teste com chaves sk_test_ está em desenvolvimento e será anunciado no changelog.

Checklist de produção

  1. Conta verificada e ativa no painel.
  2. Chave sk_live_ guardada só no servidor (variável de ambiente/secret manager).
  3. Idempotency-Key derivada do seu pedido em toda criação.
  4. Voucher Multibanco exibido com entidade, referência, valor e prazo.
  5. Webhook HTTPS cadastrado; assinatura verificada (vetor de teste passa); dedupe por id; resposta 2xx ≤ 10 s.
  6. Liberação do produto somente no order.paid (nunca na criação).
  7. Tratamento de 429 com backoff e de 402/expired/failed com mensagem clara ao cliente.

Suporte e changelog

Dúvidas de integração: suporte@broski.pt. Mudanças na API são sempre aditivas dentro de /v1 e anunciadas nesta página antes de entrar em vigor.

Changelog