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:
- Ative a sua conta no painel (app.broski.pt) — verificação de dados e documento.
- Gere a sua chave secreta em Configurações → API.
- Crie pedidos com
POST /v1/orders— MB WAY confirma em segundos; Multibanco emite um voucher pago em até alguns dias. - 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.
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"}
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 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étodo | Transições |
|---|---|
| MB WAY | pending → paid · failed · expired |
| Multibanco | awaiting_payment → paid · failed · expired (voucher venceu) |
| Qualquer | paid → refunded (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:
- Mesma chave + mesmo corpo → devolve a resposta original (header
Idempotent-Replay: true), sem criar nada. - Mesma chave + corpo diferente →
409 idempotency_key_reused. - Só o sucesso fica gravado: se a criação falhou, pode reusar a mesma chave no retry. Retenção garantida: ≥ 24 h.
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
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:
- Crie um pedido MB WAY de
50(€0,50 — o mínimo real) para o seu próprio telemóvel, confirme, e verifique o webhookorder.paid; depois estorne via API. - Valide a sua verificação de assinatura contra o vetor de teste antes de ir ao ar.
- Acompanhe entregas de webhook (e reenvie manualmente) no painel.
O modo de teste com chaves sk_test_ está em desenvolvimento e será anunciado no changelog.
Checklist de produção
- Conta verificada e ativa no painel.
- Chave
sk_live_guardada só no servidor (variável de ambiente/secret manager). Idempotency-Keyderivada do seu pedido em toda criação.- Voucher Multibanco exibido com entidade, referência, valor e prazo.
- Webhook HTTPS cadastrado; assinatura verificada (vetor de teste passa); dedupe por
id; resposta 2xx ≤ 10 s. - Liberação do produto somente no
order.paid(nunca na criação). - Tratamento de
429com backoff e de402/expired/failedcom 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
- 2026-07-16 — Publicação da documentação e do openapi.yaml. Superfície: pedidos (criar/consultar/listar), estornos, 5 eventos de webhook.