◈ paynext.docs
Referência da API

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.

POST /v1/charges sandbox

        

Dica: um telemóvel terminado em 999999 força uma falha. Precisa de uma chave? Abrir conta de teste.

Introdução

Todos os pedidos são feitos sobre HTTPS para o URL base:

https://api.paynext.co.mz/v1

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_...":
Guarde a chave em segredo. Nunca a exponha no frontend nem em repositórios públicos. Pode revogá-la e criar outra a qualquer momento no painel.

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:

EstadoSignificado
pendingIniciada; à espera de confirmação do cliente (PIN)
succeededPagamento recebido
failedNão foi possível cobrar
refundedReembolsada ao cliente
disputedContestada pelo cliente

Criar uma cobrança

POST /v1/charges

Inicia uma cobrança numa carteira móvel. O cliente recebe um pedido de confirmação (PIN) no telemóvel.

ParâmetroTipoDescrição
amount obrigatóriointeiroValor em centavos (mín. 100)
method obrigatóriostringmpesa, emola ou izi
phone obrigatóriostringTelemóvel do cliente
currencystringSó MZN (predefinido)
descriptionstringReferê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
}
A confirmação final chega por webhook (charge.succeeded). Não confie apenas na resposta imediata, que é pending.

Obter uma cobrança

GET /v1/charges/{id}
curl https://api.paynext.co.mz/v1/charges/ch_9kx2... \
  -u "sk_live_...":

Listar cobranças

GET /v1/charges

Devolve as cobranças mais recentes primeiro. Parâmetros de paginação por cursor:

ParâmetroTipoDescrição
limitinteiroItens por página (1–100, predefinido 20)
starting_afterstringID 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

POST /v1/charges/{id}/refund

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"
}
POST /v1/payment-links

Cria uma página de pagamento reutilizável, com um URL que pode partilhar por WhatsApp, SMS ou email.

ParâmetroTipoDescrição
amount obrigatóriointeiroValor em centavos (mín. 100)
titlestringTítulo mostrado no checkout
descriptionstringDescrição do que se cobra
methodsarrayCarteiras aceites, ex. ["mpesa","emola"]
return_urlstringPara onde o cliente volta após pagar
metadataobjetoDados 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"
}
GET /v1/payment-links
GET /v1/payment-links/{id}

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

POST /v1/checkout-sessions

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âmetroTipoDescrição
amount obrigatóriointeiroValor em centavos
descriptionstringMostrada no checkout
methodslistaCarteiras aceites (predefinido: todas)
return_urlstringPara onde voltar após pagar
metadataobjetoDados 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

POST /v1/plans

Um plano define o valor e a frequência de uma cobrança recorrente (mensalidade, assinatura).

ParâmetroTipoDescrição
name obrigatóriostringNome do plano
amount obrigatóriointeiroValor por ciclo, em centavos
intervalstringweek, 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

POST /v1/subscriptions

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âmetroTipoDescrição
plan obrigatóriostringReferência do plano (plan_...)
phone obrigatóriostringTelemóvel do cliente
namestringNome do cliente
methodstringmpesa (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
}
Cada ciclo gera uma cobrança (charge) ligada à subscrição. Ouça charge.succeeded por webhook para confirmar cada pagamento recorrente.

Cancelar e listar

POST /v1/subscriptions/{id}/cancel
GET /v1/subscriptions
GET /v1/subscriptions/{id}

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)

POST /v1/transfers

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).

Toda a transferência é aprovada por um operador PayNext antes de sair, seja qual for o valor. A sua app deve validar e aprovar o pedido do cliente antes de chamar este endpoint; o PayNext não faz essa parte por si. Enquanto estiver pending_approval pode cancelar. O desfecho chega por webhook (transfer.paid / transfer.failed).
ParâmetroTipoDescrição
amount obrigatóriointeiroValor em centavos (mínimo 1 000 = 10 MT)
phone obrigatóriostringNúmero M-Pesa do cliente (9 dígitos, com ou sem +258)
descriptionstringMostrada ao operador e no extrato
metadataobjetoDados 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

EstadoSignificado
pending_approvalEm análise pelo operador PayNext; o valor já está reservado no seu saldo. Cancelável.
processingAprovada; a ordem está no M-Pesa (ou a ser confirmada). Já não se cancela.
paidO cliente recebeu.
failedNã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.

Idempotência. Use 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.
POST /v1/transfers/{id}/cancel
GET /v1/transfers/{id}
GET /v1/transfers

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:

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:

Use o corpo BRUTO da requisição, exactamente como chegou. Se reconstruir o JSON (por exemplo 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 }
  }
}
Responda 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.
Rejeite eventos antigos. 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ódigoSignificado
200 / 201Sucesso
401Chave em falta ou inválida
402Saldo disponível insuficiente (insufficient_funds)
404Recurso não encontrado
409Estado não permite a operação (ex.: cancelar uma transferência já aprovada)
422Parâmetros inválidos ou limite da carteira atingido

Precisa de ajuda? Equipa em Maputo — contactos.