Agentes IA · Servidor MCP
Integrar Pagamentos de Ponta a Ponta
Esta é a razão de ser do servidor MCP. A API Direta de Pagamentos permite-te construir o teu próprio checkout, sem iframe — e o MCP dá ao assistente tudo o que é preciso para a integrar sozinho: descobrir a app, ler a arquitetura, configurar o webhook, escrever o código e testá-lo.
A sequência
Basta uma instrução em linguagem natural. O que se segue é o que o assistente executa por baixo:
| # | Passo | Ferramenta |
|---|---|---|
| 1 | Deduzir a app a partir do domínio do projeto local | find_app_by_url |
| 2 | Ler o contrato completo: chaves, métodos, assinatura, endpoints | get_payment_api_config |
| 3 | Garantir que existe um segredo de assinatura | rotate_webhook_secret |
| 4 | Definir o webhook de faturação e subscrever invoice.paid | update_payment_config |
| 5 | Escrever o handler e o cliente M2M no teu projeto | — (edição de ficheiros) |
| 6 | Sincronizar o catálogo de planos, se houver assinaturas | sync_plans |
| 7 | Auto-validar contra o código acabado de escrever | trigger_test_payment_webhook |
| 8 | Percorrer o ciclo real ponta a ponta | create_invoice → create_test_charge → simulate_invoice_payment |
| 9 | Diagnosticar o que falhou, se falhou | list_webhook_deliveries |
Passo 1 — Descoberta automática
O assistente lê o APP_URL do .env do teu projeto e resolve-o contra as tuas apps
registadas, comparando o domínio com o redirect_url, a homepage, as origens permitidas e os
webhooks de cada uma. Não te pergunta qual é o App ID porque não precisa.
// find_app_by_url { "url": "https://minhaloja.ao" }
{
"status": "success",
"data": {
"host": "minhaloja.ao",
"match_count": 1,
"apps": [{
"id": 7,
"name": "Minha Loja",
"environment": "test",
"client_id": "9d2f...",
"billing_webhook_url": null
}]
}
}
Se match_count for 0, o assistente lista as tuas apps ou cria uma nova com create_oauth_app.
Passo 2 — Ler o contrato, não adivinhar
O get_payment_api_config é o coração da autonomia. Devolve num único pedido tudo o que uma IA
teria de perguntar: as chaves, se o gateway está em modo simulado, os métodos de pagamento com os
respetivos prazos de validade, o esquema exato de assinatura, e a lista completa de endpoints — tanto os
M2M de produção como os de desenvolvimento.
{
"environment": "test",
"uses_mock_gateway": true,
"webhook_secret_configured": true,
"invoice_paid_subscribed": false,
"payment_methods": {
"REF": { "label": "Referência Multicaixa (AppyPay/Pay4All)", "requires_phone_number": false, "validity": "3 dias a partir da criação", "currency": "AOA" },
"GPO": { "label": "Multicaixa Express", "requires_phone_number": true, "validity": "60 segundos a partir da criação", "currency": "AOA" },
"EKZ": { "label": "E-Kwanza", "requires_phone_number": true, "validity": "5 minutos a partir da criação", "currency": "AOA" },
"RDP": { "label": "RedotPay (criptomoeda/stablecoins)", "requires_phone_number": false, "validity": "1 hora a partir da criação", "currency": "USD" },
"WIP": { "label": "WiPay (hospedado)", "requires_phone_number": false, "validity": "sem expiração documentada", "currency": "AOA" }
},
"default_currency": "AOA",
"supported_currencies": ["AOA", "USD"],
"currency_methods": { "AOA": ["REF", "GPO", "EKZ", "WIP"], "USD": ["RDP"] },
"webhook_signature": {
"header": "X-AcapaDev-Signature",
"algorithm": "HMAC-SHA256",
"formula": "hash_hmac('sha256', , webhook_secret)"
},
"endpoints": { "m2m": { "...": "..." }, "developer": { "...": "..." } }
}
Passo 4 — Configurar o webhook
Se não indicares um URL, o assistente deriva-o do domínio do teu projeto. O
subscribe_invoice_paid é essencial: sem ele o evento nunca é enviado, por muito bem
configurado que esteja o URL.
// update_payment_config
{
"app_id": 7,
"billing_webhook_url": "https://minhaloja.ao/api/webhooks/billing",
"subscribe_invoice_paid": true
}
Se definires um billing_webhook_url, só o invoice.paid passa
a ir para lá; os eventos de identidade (user.logout, user.updated) continuam
no webhook_url geral. O segredo de assinatura é o mesmo para ambos. Em desenvolvimento
local precisas de um túnel público (ngrok, Expose) — o SSO tem de conseguir alcançar o URL.
Passo 5 — O código que a IA escreve
O handler verifica a assinatura sobre o corpo bruto do pedido, antes de fazer parse do JSON, e com comparação em tempo constante:
Route::post('/api/webhooks/billing', function (Request $request) {
$raw = $request->getContent();
$expected = hash_hmac('sha256', $raw, config('services.acapadev.webhook_secret'));
if (!hash_equals($expected, (string) $request->header('X-AcapaDev-Signature'))) {
abort(401);
}
$payload = json_decode($raw, true);
if (($payload['event'] ?? null) !== 'invoice.paid') {
return response()->noContent();
}
// Idempotência: o mesmo invoice.paid pode chegar mais do que uma vez.
$invoiceId = $payload['data']['invoice_id'];
if (ProcessedPayment::where('invoice_id', $invoiceId)->exists()) {
return response()->noContent();
}
ProcessedPayment::create(['invoice_id' => $invoiceId]);
// app_reference é a tua referência interna, devolvida tal como a enviaste.
ActivateOrder::dispatch($payload['data']['app_reference']);
return response()->noContent();
})->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
Idempotência não é opcional. O mesmo invoice.paid pode chegar duas vezes —
por reenvio manual, por retry de rede, ou porque o polling e o webhook do gateway confirmaram em
paralelo. Sem esta guarda, creditas o cliente a dobrar.
Passo 7 — O loop de auto-validação
É aqui que o MCP se distingue de uma API normal. O trigger_test_payment_webhook envia um
invoice.paid assinado para o teu endpoint e devolve, de forma síncrona, o
código HTTP e o corpo com que o teu código respondeu:
{
"status": "success",
"data": {
"target_url": "https://minhaloja.ao/api/webhooks/billing",
"signature_header": "X-AcapaDev-Signature",
"payload_sent": { "event": "invoice.paid", "...": "..." },
"delivery": {
"status": "failed",
"response_status": 500,
"response_body": "Undefined array key \"app_reference\""
}
}
}
O assistente lê aquele response_body, percebe que o próprio código dele rebentou, corrige o
ficheiro e volta a disparar — sem te fazer perguntas e sem te pedir para testares nada. A ferramenta não
cria faturas, por isso pode ser repetida à vontade.
Passo 8 — O ciclo real
Depois do handler responder 2xx, o assistente percorre o fluxo completo tal como acontecerá em produção:
create_invoice → devolve invoice_id (UUID) e pay_url
create_test_charge → REF | GPO | EKZ | RDP | WIP, devolve is_mock: true
simulate_invoice_payment → liquida a fatura e dispara o invoice.paid real
list_webhook_deliveries → confirma que o teu endpoint respondeu 200
O create_test_charge é o que te dá a forma exata dos dados que a tua UI de checkout vai ter de
apresentar — a referência e a entidade para REF, o prompt do Multicaixa Express para
GPO, o ticket para EKZ, e o pay_url mais a lista de carteiras
para RDP. O campo expires_at vem sempre normalizado em
ISO8601, seja qual for o método, para saberes quando deixar de mostrar a referência.
Pagamentos em dólar (RedotPay)
A moeda escolhe-se na criação da fatura (currency) e não se muda depois — é ela que
determina os métodos possíveis. AOA dá acesso a REF, GPO e
EKZ; USD dá acesso a RDP (criptomoeda via RedotPay). Pedir
RDP numa fatura em AOA devolve 422, e não há conversão
automática entre as duas.
create_invoice(currency: "USD", success_url: "https://a-tua-app/pagamento/ok")
create_test_charge(payment_method: "RDP")
→ data.pay_url página de checkout da RedotPay (envia o utilizador para lá)
→ data.paymentMethods carteiras suportadas, cada uma com logo e links
→ expires_at 1 hora
Duas coisas que poupam tempo: o success_url da fatura é o URL para onde a RedotPay
devolve o cliente depois de pagar — sem ele, ele fica na página de pagamento do AcapaPay; e os
campos webQrCode/h5QrCode de cada carteira são links,
não imagens de QR code, por isso quem quiser mostrar o QR dentro da própria página tem de gerar
a imagem a partir do link.
Diagnóstico
Sintoma em list_webhook_deliveries | O que significa |
|---|---|
| Nenhuma entrega registada | O evento não está subscrito, ou não há webhook configurado. Verifica invoice_paid_subscribed. |
response_status: 401 | A verificação de assinatura falhou. Quase sempre por se estar a assinar o JSON reserializado em vez do corpo bruto, ou por o segredo no .env estar desatualizado após uma rotação. |
response_status: 404 | A rota não existe no URL configurado, ou o túnel local caiu. |
response_status: 419 | Proteção CSRF ativa na rota. Exclui-a. |
response_status: 500 | Exceção no handler. O response_body traz a mensagem. |
status: failed sem response_status | O SSO não conseguiu sequer alcançar o URL — DNS, certificado ou túnel em baixo. |