AcapaPay · Laravel SDK
API de Pagamento Direta (sem iFrame)
Além do checkout hospedado (redirecionamento ou <x-acapapay::iframe>), o AcapaPay expõe uma API de pagamento direta: um conjunto de endpoints M2M em bruto que permitem à tua aplicação construir a própria interface de checkout, sem nunca redirecionar o utilizador para o SSO nem embutir nenhum iFrame.
1. Autenticação
Todos os endpoints abaixo são M2M (machine-to-machine), protegidos pelo mesmo middleware client do Passport usado pelo resto do AcapaPay. Obtém um token OAuth2 client_credentials com as mesmas credenciais (ACAPAPAY_CLIENT_ID / ACAPAPAY_CLIENT_SECRET) já usadas para checkoutSession():
POST https://api.{dominio}/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=...&client_secret=...&scope=*
Usa o access_token devolvido como Authorization: Bearer <token> em todos os pedidos seguintes.
AcapaPay::direct() — createInvoice(), charge(), status(), simulate() — que trata do token, dos erros e da forma dos payloads por ti. Os endpoints em bruto abaixo continuam a documentar o contrato, e são o caminho para quem não usa Laravel. O SDK CodeIgniter tem o seu próprio método de conveniência (criarCobranca()) — ver a referência do CodeIgniter.
2. Fluxo completo
POST /v1/billing/invoices— cria a fatura (cliente, itens, referência, metadata).POST /v1/billing/invoices/{id}/charges— gera a cobrança (REF,GPO,EKZ,RDPouWIP) para essa fatura.- A tua app mostra os dados devolvidos (referência/entidade, prompt Multicaixa Express, ticket/QR code E-Kwanza, ou o link de pagamento em cripto) na tua própria UI.
GET /v1/billing/invoices/{id}/status— usado em polling (a cada poucos segundos) até a fatura ficarpaid. Internamente há um rate-limit de 15s por fatura antes de voltar a consultar o gateway externo — pedidos mais frequentes devolvem o último estado conhecido sem novo pedido externo.- Assim que a fatura é confirmada como paga (via polling ou via webhook do gateway), o SSO dispara o webhook
invoice.paidpara owebhook_urlconfigurado na tua App, assinado emX-AcapaDev-Signature(HMAC-SHA256) — ver Webhooks.
Todas as cobranças e reconciliações geradas por esta API ficam visíveis no admin do SSO (Faturação → detalhe da fatura), exatamente como as geradas pela página de checkout hospedada.
Webhook URL dedicado para faturação (opcional)
Por omissão, o invoice.paid é enviado para o mesmo webhook_url usado pelos eventos de identidade (user.logout, user.updated, user.role_updated). Se o teu sistema de faturação for um serviço separado do que trata a sessão/autenticação, podes configurar um "Webhook URL de Faturação" dedicado no painel Developer → Webhooks & Eventos da tua app.
- Quando preenchido, só o evento
invoice.paidpassa a ir para esse URL — os restantes eventos continuam a ir para owebhook_urlgeral. - Quando vazio, o comportamento é o de sempre:
invoice.paidcontinua a ir para owebhook_urlgeral. Nenhuma app já integrada precisa de mudar nada. - Usa o mesmo
webhook_secrete a mesma assinatura HMAC dowebhook_urlgeral — não há segredo separado por endpoint.
3. Como Apresentar o Pagamento em Cripto ao Utilizador — 2 Fluxos
Guia completo do dólar, com exemplos do SDK: Pagamentos em USD (Cripto).
Isto aplica-se especificamente ao método RDP (RedotPay/criptomoeda), que é o único onde a diferença entre os dois fluxos é significativa. Para REF/GPO/EKZ a API direta já te dá a referência ou o ticket em texto, prontos a mostrar — não há QR code nem redirecionamento externo envolvido nesses métodos.
Quando geras uma cobrança RDP, tens de escolher uma de duas formas de levar o utilizador a pagar. Nenhuma é objetivamente "melhor" — a escolha depende de quanto controlo visual precisas e de quanto trabalho de implementação estás disposto a fazer.
Fluxo 1 — Redirecionar (recomendado por omissão)
É o mais simples, e é o que a nossa própria página de checkout hospedado usa internamente. Chamas o endpoint de cobrança, recebes um data.pay_url, e envias o utilizador para lá — ponto final.
$response = Http::withToken($token)->post("{$apiHost}/v1/billing/invoices/{$invoiceId}/charges", [
'payment_method' => 'RDP',
]);
$payUrl = $response->json('data.pay_url');
return redirect($payUrl);
// ou, se estiveres a responder a um pedido AJAX/fetch feito pelo teu frontend:
// return response()->json(['pay_url' => $payUrl]);
// e no frontend: window.open(data.pay_url, '_blank');
O que o utilizador vê ao chegar a esse pay_url: uma página hospedada pela RedotPay (não é tua, nem nossa) com a lista de carteiras suportadas (MetaMask, Phantom, Trust Wallet, Coinbase, TronLink, etc.), cada uma já com o seu próprio QR code desenhado e pronto a digitalizar. A RedotPay trata de tudo: gerar as imagens de QR, detetar se o visitante está em mobile ou desktop, abrir o deep-link correto se tiver a carteira instalada no telemóvel, etc.
| Vantagens | Desvantagens | |
|---|---|---|
| Fluxo 1 | Zero código extra de UI; a RedotPay mantém a página atualizada (novas carteiras, correções de segurança); funciona logo em mobile e desktop. | O utilizador sai da tua aplicação/domínio durante um instante. |
Fluxo 2 — Construir a tua própria interface com o QR code embutido
Se quiseres que o utilizador nunca saia da tua página — por exemplo, mostrar o QR code dentro de um modal da tua app, ao lado do teu logótipo — usa o array data.paymentMethods devolvido pela API de cobrança.
webQrCode e h5QrCode que a RedotPay devolve não são imagens. São links (deep-links de carteira, do tipo https://phantom.app/ul/browse/...). A RedotPay não gera nenhuma imagem de QR code para ti — quem tem de gerar a imagem, a partir desse link de texto, és tu. Isto é o "trabalho extra": sem esse passo de geração da imagem, não tens QR code nenhum para mostrar, só um link em bruto.
Estrutura de cada item em data.paymentMethods (nem todos os campos vêm sempre preenchidos — depende da carteira):
{
"id": "phantom",
"name": "Phantom",
"logo": "https://staticsource1.redotpay.com/aquirer/wallets/phantom.svg",
"webUrl": "https://phantom.app/ul/browse/...", // abrir num browser desktop
"webQrCode": "https://phantom.app/ul/browse/...", // converter em QR (desktop)
"h5Url": "https://phantom.app/ul/browse/...", // abrir num browser mobile
"h5QrCode": "https://phantom.app/ul/browse/...", // converter em QR (mobile/H5)
"appUrl": "https://phantom.app/ul/browse/...", // deep-link direto para a app
"appQrCode": null
}
Uma cobrança RDP típica devolve entre 12 e 13 carteiras neste array (MetaMask, Phantom, Trust Wallet, Binance Wallet, OKX Wallet, Bitget Wallet, TokenPocket, ImToken, Coinbase, Rainbow, TronLink, RedotPay e Coinbase Pay — confirmado com uma chamada real ao ambiente sandbox).
Gerar a imagem do QR code
Há duas formas de o fazer — escolhe consoante onde preferes que o trabalho aconteça:
Opção A — Gerar no servidor (PHP), com simplesoftwareio/simple-qrcode:
composer require simplesoftwareio/simple-qrcode
{{-- resources/views/checkout/cripto.blade.php --}}
@foreach ($wallets as $wallet)
@if ($wallet['webQrCode'])
<div class="carteira">
<img src="{{ $wallet['logo'] }}" alt="{{ $wallet['name'] }}">
<p>{{ $wallet['name'] }}</p>
{{-- SimpleQrCode desenha o SVG do QR diretamente a partir do link --}}
{!! QrCode::size(220)->generate($wallet['webQrCode']) !!}
</div>
@endif
@endforeach
Opção B — Gerar no browser (JavaScript), com uma lib tipo qrcode: útil se o teu checkout for uma SPA (Vue/React) e quiseres evitar uma dependência PHP extra.
<canvas id="qr-phantom"></canvas>
<script src="https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js"></script>
<script>
// 'linkDaCarteira' é o valor de wallet.webQrCode que já devolveste do backend
QRCode.toCanvas(document.getElementById('qr-phantom'), linkDaCarteira);
</script>
Se detetares que o visitante está num telemóvel (user agent, ou uma media query no frontend), o mais correto é usares h5QrCode em vez de webQrCode, ou até nem mostrares QR nenhum e usares antes um botão "Abrir carteira" apontado a appUrl — porque num telemóvel não faz sentido pedir ao utilizador para digitalizar um QR code com o próprio telemóvel.
| Vantagens | Desvantagens ("trabalho extra") | |
|---|---|---|
| Fluxo 2 | Zero saída da tua aplicação; controlo total do design. | Tens de escolher e integrar uma lib de geração de QR; tens de decidir web vs h5/appUrl consoante o dispositivo; a tua UI tem de lidar bem quando a lista de carteiras da RedotPay mudar. |
Qual escolher?
| Fluxo 1 (redirecionar) | Fluxo 2 (QR embutido) | |
|---|---|---|
| Trabalho de implementação | Nenhum além de ler data.pay_url | Escolher/integrar uma lib de QR, decidir web vs h5 por dispositivo |
| Onde o utilizador vê o QR | Página da RedotPay (fora da tua app) | Dentro da tua própria página |
| Manutenção ao longo do tempo | Nenhuma (a RedotPay atualiza a página dela) | Tens de acompanhar mudanças no array paymentMethods |
| Recomendado para | A maioria das apps — arranca em minutos | Apps onde a experiência de marca dentro do checkout é crítica |
ChargeResult devolvido por AcapaPay::direct()->charge() já expõe paymentMethods(), qrCodeUrls() (a versão simplificada do array acima) e appUrl($walletId) — poupa-te de navegar o array manualmente. Ver a secção "Como apresentar o pagamento ao utilizador" no README do pacote (v1.2.0+).4. Exemplo Completo (Laravel)
<?php
use Illuminate\Support\Facades\Http;
class PagamentoDiretoController extends Controller
{
protected function tokenM2M(): string
{
$response = Http::asForm()->post(config('acapapay.host') . '/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => config('acapapay.client_id'),
'client_secret' => config('acapapay.client_secret'),
'scope' => '*',
]);
return $response->json('access_token');
}
public function criarFaturaECobranca(Request $request)
{
$token = $this->tokenM2M();
$apiHost = config('acapapay.api_host');
// 1. Criar a fatura
$invoice = Http::withToken($token)->post("{$apiHost}/v1/billing/invoices", [
'customer_name' => $request->user()->name,
'customer_email' => $request->user()->email,
'items' => [
['description' => 'Plano PRO', 'quantity' => 1, 'unit_price' => 15000],
],
'app_reference' => 'pedido-' . $request->user()->id,
])->json();
// 2. Gerar a cobrança (ex: Referência Multicaixa)
$charge = Http::withToken($token)->post(
"{$apiHost}/v1/billing/invoices/{$invoice['invoice_id']}/charges",
['payment_method' => 'REF']
)->json();
// $charge['data'] contém a referência/entidade a mostrar na tua UI
// $charge['expires_at'] é o prazo de validade da cobrança (ver tabela abaixo)
return response()->json($charge);
}
}
5. Endpoints
POST /v1/billing/invoices
Cria a fatura. Um pagamento nunca existe sem estar associado a uma fatura.
| Campo | Tipo | Obrigatório | Notas |
|---|---|---|---|
customer_name | string | Sim | Máx. 255 caracteres |
customer_email | string | Não | — |
customer_nif | string | Não | Máx. 50 caracteres |
currency | string | Não | 3 letras, padrão AOA |
items | array | Sim | Pelo menos 1 item |
items[].description | string | Sim | — |
items[].quantity | integer | Sim | Mínimo 1 |
items[].unit_price | number | Sim | Mínimo 0 |
app_reference | string | Não | A tua referência interna do pedido |
due_date | date | Não | — |
Resposta 201:
{
"status": "success",
"invoice_id": "9b123a45-...",
"pay_url": "https://pay.acapadev.com/9b123a45-...",
"total": 15000,
"currency": "AOA"
}
pay_url é o URL do checkout hospedado para essa fatura — útil como fallback, mas na API direta normalmente ignoras este campo e usas antes /charges abaixo.POST /v1/billing/invoices/{id}/charges
Cria uma cobrança para a fatura {id}.
| Campo | Tipo | Obrigatório | Notas |
|---|---|---|---|
payment_method | string | Sim | REF, GPO, EKZ, RDP ou WIP |
phone_number | string | Se GPO/EKZ · opcional em WIP | Número Multicaixa Express / E-Kwanza / WiPay |
Resposta 201:
{
"status": "success",
"payment_method": "REF",
"is_mock": false,
"expires_at": "2026-08-12T18:53:06+01:00",
"data": { "...": "payload bruto devolvido pelo gateway" }
}
expires_at (ISO 8601) é o prazo de validade normalizado da cobrança — guarda-o na tua BD/cache para saberes quando deixar de mostrar a referência ao utilizador e/ou pedir uma nova cobrança. Vem sempre neste campo, independentemente do método (o gateway devolve o prazo em formatos diferentes consoante o método — o AcapaPay normaliza por ti). Prazos observados em produção:
| Método | Validade | Fonte |
|---|---|---|
REF (Referência Multicaixa) | 3 dias a partir da criação | Devolvida pela AppyPay |
GPO (Multicaixa Express) | 60 segundos a partir da criação | Janela fixa da gateway (não é devolvida no payload — o AcapaPay calcula) |
EKZ (E-Kwanza) | 5 minutos a partir da criação | Devolvida pela E-Kwanza |
expires_at para exibição — serve sobretudo para uma eventual contagem decrescente na UI enquanto se aguarda a aprovação no telemóvel do utilizador.
GET /v1/billing/invoices/{id}/status também devolve este expires_at (dentro de data.transaction), recalculado a partir dos mesmos dados guardados — útil se a tua app recarregar a página e precisar de saber quanto tempo falta sem ter guardado o valor original.
sandbox (ou a fatura tiver metadata.sandbox_mode: true), a resposta é sempre um mock (is_mock: true, entidade 00000, referência 999 999 999), sem chamar o gateway real — útil para testar a tua interface sem gerar cobranças reais.Erros: 422 se a fatura já estiver paid/processing_verification; 500 com {"error": "..."} em caso de falha de comunicação com o gateway.
GET /v1/billing/invoices/{id}/status
Consulta (e tenta atualizar, respeitando o rate-limit de 15s) o estado atual da fatura.
{
"status": "success",
"data": {
"invoice_status": "paid",
"paid_at": "2026-08-09T12:34:56+00:00",
"transaction": {
"payment_method": "REF",
"status": "completed",
"gateway_reference": "INV1A2B3C4D5E",
"expires_at": "2026-08-12T18:53:06+01:00"
}
}
}
POST /v1/billing/invoices/{id}/simulate
Apenas em ambiente sandbox: marca a fatura como paga imediatamente (cria uma transação sandbox_test e dispara o webhook invoice.paid), sem depender do gateway real. Devolve 403 fora de sandbox.
6. Erros
Todos os erros seguem {"error": "mensagem"}, com o código HTTP a indicar a categoria:
| Código | Categoria |
|---|---|
403 | Autorização / restrição de sandbox |
404 | Fatura não encontrada / não pertence à App |
422 | Estado inválido da fatura (ex: já paga) |
500 | Falha de comunicação com o gateway externo |