API do paynext.
Aceite M-Pesa, e-Mola e IZI a partir da sua aplicação. Uma API REST simples, com respostas em JSON e webhooks assinados.
Experimentar agora
Cole a sua chave de teste sk_test_... (do painel, em Chaves de API) e crie uma cobrança a sério, em sandbox. Não se move dinheiro real — a cobrança liquida sozinha em segundos e vê o estado mudar aqui mesmo.
Introdução
Todos os pedidos são feitos sobre HTTPS para o URL base:
https://api.paynext.co.mz/v1
- Os valores são sempre em centavos de Metical (MZN). Ex.:
1250= 12,50 MT;125000= 1.250 MT. - As respostas são em JSON.
- Os métodos de pagamento suportados são mpesa emola izi.
- Cada chave pertence a uma carteira (um negócio): a cobrança entra nessa carteira e a chave só vê os dados dela. Com vários negócios, use uma chave por negócio.
Autenticação
Autentique cada pedido com a sua chave secreta sk_live_..., obtida no painel em Chaves de API. Cada chave pertence a uma carteira — não precisa de indicar a carteira nas chamadas, a chave já a identifica. Aceitamos dois formatos:
Bearer token
curl https://api.paynext.co.mz/v1/charges \
-H "Authorization: Bearer sk_live_..."
HTTP Basic (a chave como utilizador)
curl https://api.paynext.co.mz/v1/charges \
-u "sk_live_...":
Modo de teste
Use uma chave sk_test_... para experimentar sem mover dinheiro real. As cobranças de teste liquidam sozinhas ao fim de alguns segundos; um telemóvel terminado em 999999 força uma falha. Os dados de teste e de produção são totalmente separados: uma chave sk_test_ só vê cobranças de teste, e uma sk_live_ só vê as de produção. No painel, alterne entre os dois com o interruptor Produção / Teste no topo.
Valores e estados
Uma cobrança passa por estes estados:
| Estado | Significado |
|---|---|
pending | Iniciada; à espera de confirmação do cliente (PIN) |
succeeded | Pagamento recebido |
failed | Não foi possível cobrar |
refunded | Reembolsada ao cliente |
disputed | Contestada pelo cliente |
Criar uma cobrança
Inicia uma cobrança numa carteira móvel. O cliente recebe um pedido de confirmação (PIN) no telemóvel.
| Parâmetro | Tipo | Descrição |
|---|---|---|
amount obrigatório | inteiro | Valor em centavos (mín. 100) |
method obrigatório | string | mpesa, emola ou izi |
phone obrigatório | string | Telemóvel do cliente |
currency | string | Só MZN (predefinido) |
description | string | Referência para o comerciante |
# Pedido curl https://api.paynext.co.mz/v1/charges \ -u "sk_live_...": \ -d amount=125000 \ -d method=mpesa \ -d phone="+258 84 000 0000" \ -d description="Curso de finanças" # Resposta 201 { "id": "ch_9kx2...", "object": "charge", "amount": 125000, "currency": "MZN", "method": "mpesa", "status": "pending", "created": 1785965867 }
charge.succeeded). Não confie apenas na resposta imediata, que é pending.Obter uma cobrança
curl https://api.paynext.co.mz/v1/charges/ch_9kx2... \
-u "sk_live_...":
Listar cobranças
Devolve as cobranças mais recentes primeiro. Parâmetros de paginação por cursor:
| Parâmetro | Tipo | Descrição |
|---|---|---|
limit | inteiro | Itens por página (1–100, predefinido 20) |
starting_after | string | ID da última cobrança da página anterior, para obter a página seguinte |
curl "https://api.paynext.co.mz/v1/charges?limit=10" \ -u "sk_live_...": # Resposta { "object": "list", "url": "/v1/charges", "has_more": true, "data": [ /* cobranças */ ] }
Quando has_more for true, passe o id do último item em starting_after para obter a página seguinte.
Reembolsar uma cobrança
Devolve o dinheiro ao cliente. Só funciona sobre uma cobrança succeeded; a cobrança passa a refunded e o saldo do comerciante é ajustado.
curl -X POST https://api.paynext.co.mz/v1/charges/ch_9kx2.../refund \ -u "sk_live_...": # Resposta 200 { "id": "ch_9kx2...", "object": "charge", "status": "refunded" }
Criar um link de pagamento
Cria uma página de pagamento reutilizável, com um URL que pode partilhar por WhatsApp, SMS ou email.
| Parâmetro | Tipo | Descrição |
|---|---|---|
amount obrigatório | inteiro | Valor em centavos (mín. 100) |
title | string | Título mostrado no checkout |
description | string | Descrição do que se cobra |
methods | array | Carteiras aceites, ex. ["mpesa","emola"] |
return_url | string | Para onde o cliente volta após pagar |
metadata | objeto | Dados livres devolvidos no webhook |
curl https://api.paynext.co.mz/v1/payment-links \ -u "sk_live_...": \ -d amount=125000 \ -d description="Mensalidade de Agosto" # Resposta 201 { "id": "vxp9mhqv...", "object": "payment_link", "url": "https://pay.paynext.co.mz/vxp9mhqv...", "amount": 125000, "status": "active" }
Listar e obter links
A listagem aceita a mesma paginação das cobranças (limit, starting_after, has_more).
curl "https://api.paynext.co.mz/v1/payment-links?limit=10" \ -u "sk_live_...":
Sessões de checkout
Cria uma página de pagamento hospedada e devolve o URL para onde redirecionar o cliente. Ideal para lojas e integrações (é o que o plugin WooCommerce usa). O cliente escolhe a carteira e paga; depois volta para o return_url.
| Parâmetro | Tipo | Descrição |
|---|---|---|
amount obrigatório | inteiro | Valor em centavos |
description | string | Mostrada no checkout |
methods | lista | Carteiras aceites (predefinido: todas) |
return_url | string | Para onde voltar após pagar |
metadata | objeto | Dados seus (ex.: order_id), devolvidos no webhook |
curl https://api.paynext.co.mz/v1/checkout-sessions \ -u "sk_live_...": \ -H "Content-Type: application/json" \ -d '{ "amount": 250000, "description": "Encomenda #1042", "return_url": "https://a-sua-loja.co.mz/obrigado", "metadata": { "order_id": 1042 } }' # Resposta 201 { "id": "cs_b0jw...", "object": "checkout_session", "url": "https://paynext.co.mz/pay/b0jw...", "status": "open" }
Criar um plano
Um plano define o valor e a frequência de uma cobrança recorrente (mensalidade, assinatura).
| Parâmetro | Tipo | Descrição |
|---|---|---|
name obrigatório | string | Nome do plano |
amount obrigatório | inteiro | Valor por ciclo, em centavos |
interval | string | week, month (predefinido) ou year |
curl https://api.paynext.co.mz/v1/plans \ -u "sk_live_...": \ -d name="Mensalidade do Curso" \ -d amount=125000 \ -d interval=month # Resposta 201 { "id": "plan_...", "object": "plan", "interval": "month" }
Subscrever um cliente
Liga um cliente a um plano. Cobramos já o primeiro ciclo (o cliente confirma no telemóvel com o PIN) e agendamos os seguintes automaticamente.
| Parâmetro | Tipo | Descrição |
|---|---|---|
plan obrigatório | string | Referência do plano (plan_...) |
phone obrigatório | string | Telemóvel do cliente |
name | string | Nome do cliente |
method | string | mpesa (predefinido) ou emola |
curl https://api.paynext.co.mz/v1/subscriptions \ -u "sk_live_...": \ -d plan=plan_... \ -d phone="+258 84 000 0000" \ -d name="Ana Cossa" # Resposta 201 { "id": "sub_...", "object": "subscription", "status": "active", "cycles": 1, "next_charge": 1788645600 }
charge) ligada à subscrição. Ouça charge.succeeded por webhook para confirmar cada pagamento recorrente.Cancelar e listar
Cancelar interrompe os ciclos futuros; os pagamentos já feitos mantêm-se.
curl -X POST https://api.paynext.co.mz/v1/subscriptions/sub_.../cancel \ -u "sk_live_...": # Resposta 200 { "id": "sub_...", "status": "canceled" }
Transferências para clientes (B2C)
Paga a um cliente seu a partir do saldo da carteira desta chave — por exemplo, quando um utilizador da sua app pede para levantar o que tem lá dentro. O dinheiro segue por M-Pesa para o número indicado (somente M-Pesa, por agora).
pending_approval pode cancelar. O desfecho chega por webhook (transfer.paid / transfer.failed).| Parâmetro | Tipo | Descrição |
|---|---|---|
amount obrigatório | inteiro | Valor em centavos (mínimo 1 000 = 10 MT) |
phone obrigatório | string | Número M-Pesa do cliente (9 dígitos, com ou sem +258) |
description | string | Mostrada ao operador e no extrato |
metadata | objeto | Dados seus (ex.: o id do levantamento na sua app), devolvidos no webhook |
curl https://api.paynext.co.mz/v1/transfers \ -u "sk_live_...": \ -H "Idempotency-Key: levantamento-881" \ -H "Content-Type: application/json" \ -d '{ "amount": 25000, "phone": "841112222", "description": "Levantamento #881", "metadata": { "withdrawal_id": 881 } }' # Resposta 201 { "id": "tr_8f2k...", "object": "transfer", "wallet": "wal_x9k2m1", "amount": 25000, "phone": "841112222", "status": "pending_approval", "cancelled": false, "livemode": true }
Estados
| Estado | Significado |
|---|---|
pending_approval | Em análise pelo operador PayNext; o valor já está reservado no seu saldo. Cancelável. |
processing | Aprovada; a ordem está no M-Pesa (ou a ser confirmada). Já não se cancela. |
paid | O cliente recebeu. |
failed | Não saiu: rejeitada, cancelada (cancelled: true) ou recusada pelo M-Pesa. O valor voltou ao seu saldo; veja failure_reason. |
Saldo e limites
O valor sai do saldo disponível da carteira — GET /v1/wallet devolve available (o que pode mesmo sair agora, já sem reembolsos em curso) além de balance. Sem saldo a API responde 402 insufficient_funds. A sua carteira pode ter limites definidos pela equipa PayNext (transfer_limits.max_amount por transferência e daily_limit por dia, hora de Maputo); acima deles a resposta é 422 sem chegar à fila.
Idempotency-Key com o id do pedido na sua app: repetir a chamada devolve a mesma transferência em vez de criar outra. A resposta fica colada à chave mesmo quando é um erro (ex.: um 402 por falta de saldo): depois de carregar a carteira, repita com uma chave nova.Cancelar só funciona em pending_approval (senão 409); devolve o valor ao saldo de imediato. A lista aceita limit e starting_after, como as cobranças.
curl -X POST https://api.paynext.co.mz/v1/transfers/tr_8f2k.../cancel \ -u "sk_live_...": # Resposta 200 { "id": "tr_8f2k...", "status": "failed", "cancelled": true }
Webhooks
O webhook configura-se por carteira: no painel, em Carteiras → acção Webhook. Aí define o URL e vê o segredo de assinatura dessa carteira — cada carteira tem o seu, e é ele que assina os eventos dela. Enviamos um POST em JSON sempre que uma cobrança, transferência ou levantamento muda de estado:
charge.succeeded— pagamento recebidocharge.failed— pagamento falhoucharge.refunded— pagamento reembolsadotransfer.paid— transferência ao seu cliente concluída (data.phone,data.metadata)transfer.failed— transferência não saiu; o valor voltou ao saldo (data.cancelled,data.failure_reason)payout.paid/payout.failed— levantamento seu, pedido no painel
Verificar a assinatura
Cada evento traz o cabeçalho Paynext-Signature: t=<timestamp>,v1=<hmac>. Recalcule o HMAC-SHA256 sobre a string "{t}.{corpo}" com o segredo whsec_... da carteira (Carteiras → Webhook) e compare:
json_encode(json_decode($raw))), os bytes mudam em descrições com acentos e a assinatura falha só nesses eventos — uma falha intermitente difícil de diagnosticar.// PHP $payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_PAYNEXT_SIGNATURE']; parse_str(str_replace(',', '&', $header), $p); $esperado = hash_hmac('sha256', $p['t'].'.'.$payload, $whsec); if (hash_equals($esperado, $p['v1'])) { // evento autêntico — processar }
# Corpo do evento { "id": "evt_...", "type": "charge.succeeded", "created": 1785986465, "data": { "id": "ch_ft8u...", "amount": 250000, "status": "succeeded", "metadata": { "order_id": 1042 } } }
200 rapidamente — se responder erro, reentregamos. O metadata que enviou na sessão volta aqui, para casar o pagamento com a sua encomenda. O campo data.wallet diz de que carteira é o evento: se tem várias, é por aí que escolhe o segredo a usar na verificação.t é o instante da assinatura: recuse o que estiver a mais de 5 minutos de distância (protege contra reenvio de um evento capturado). E compare sempre com hash_equals, nunca com ==.Erros
Usamos códigos HTTP convencionais. Os erros vêm em JSON:
{
"error": {
"type": "authentication_error",
"message": "Chave de API inválida ou revogada."
}
}
| Código | Significado |
|---|---|
200 / 201 | Sucesso |
401 | Chave em falta ou inválida |
402 | Saldo disponível insuficiente (insufficient_funds) |
404 | Recurso não encontrado |
409 | Estado não permite a operação (ex.: cancelar uma transferência já aprovada) |
422 | Parâmetros inválidos ou limite da carteira atingido |
Precisa de ajuda? Equipa em Maputo — contactos.