# Como integrar o acquiring do Monobank sem programador via vibe coding

> Guia passo a passo para integrar pagamentos online do Monobank ao seu site: conformidade para MEI, criação de terminal, verificação de webhooks ECDSA, UX robusta e testes de segurança.

O vibe coding transformou radicalmente o desenvolvimento de produtos digitais. Hoje, para adicionar ao seu site a aceitação de pagamentos via Apple Pay, Google Pay ou cartões bancários, não é mais necessário contratar um desenvolvedor backend nem mergulhar em protocolos criptográficos por conta própria. Basta saber formular com clareza a lógica de negócio para o seu agente de IA (Codex, Antigravity, Cursor ou Claude Code) e fornecer-lhe o contexto técnico adequado da documentação.

Neste guia, analisamos o ciclo de vida completo da integração do acquiring online Monobank no seu projeto: desde a conformidade bancária e configuração do painel do empreendedor até a geração de um gateway de pagamento seguro com verificação criptográfica de assinaturas ECDSA nos webhooks, tratamento de condições de corrida na interface e um conjunto obrigatório de testes automatizados.

> [!TIP]
> **Versão em vídeo do guia:** Se você prefere um formato visual, assista ao [vídeo prático detalhado no YouTube →](https://youtu.be/GMh_fOCiQ4E), onde todo o processo é demonstrado em tela viva, desde a primeira solicitação até a cobrança real dos fundos.

> [!NOTE]
> **Pacote de skills agênticas:** Para máxima precisão na geração de código, baixe o pacote oficial de conhecimento para o seu assistente de IA:  
> [Baixar arquivo completo monobank-acquiring.zip (38 KB) →](/downloads/monobank-acquiring.zip)

---

## 1. Arquitetura Hosted Checkout e ciclo de vida do pagamento

O Monobank Acquiring opera sob o esquema **Hosted Checkout** (pagamento em página segura do banco). Isso significa que seu site não precisa de certificações de segurança PCI DSS complexas e caras, pois os dados do cartão são inseridos pelo usuário diretamente no domínio protegido do banco.

A documentação oficial do Monobank para ferramentas de IA está disponível em [monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts). Salve este link como fonte canônica dos requisitos técnicos do banco.

### 1.1. Fluxo básico de pagamento

```
[ Cliente no site ]
       │
       ├─ 1. Clica em «Pagar»
       ▼
[ Seu Servidor ] ──────────────► [ API Monobank: /invoice/create ]
       ▲                              │
       │  recebe pageUrl, invoiceId   │
       └──────────────────────────────┘
       │
       ├─ 2. Redireciona o cliente para pageUrl (Hosted Checkout)
       ▼
[ Página de pagamento Monobank ] ───► Pagamento (Apple Pay / Google Pay / Cartão)
       │
       ├─ 3. POST assíncrono do webhook com assinatura x-sign
       ▼
[ Seu Endpoint de Webhook ] ─────────► Verificação ECDSA SHA-256 → Status "success"
       │
       ├─ 4. O cliente retorna para /payment-result
       ▼
[ Página de sucesso ] ───────────────► Exibição do acesso / recibo
```

- **1. Criação da fatura (`invoice/create`):** O cliente escolhe um produto ou plano no seu site. O handler do servidor envia uma requisição `POST` para a API do Monobank com o valor, a descrição do pagamento e o endereço de retorno. O banco retorna um `invoiceId` único e o link `pageUrl`.
- **2. Redirecionamento para a página de pagamento:** O cliente é redirecionado para o `pageUrl`, onde paga o pedido via Apple Pay, Google Pay, aplicativo Monobank ou inserindo os dados do cartão bancário.
- **3. Recebimento do Webhook:** Após a conclusão do pagamento, o Monobank envia automaticamente uma requisição `POST` para o seu `webHookUrl` previamente configurado, contendo o status da transação e uma assinatura digital criptográfica.
- **4. Fixação do status (Terminal States):** Seu servidor deve reagir a dois estados finais: `success` (pagamento bem-sucedido; libera-se o acesso ou envia-se o pedido) e `failure` (erro ou recusa do pagamento pelo banco).

> [!IMPORTANT]
> O status `expired` (expiração do link) **não gera webhook**. Se o cliente fechar a página de pagamento sem concluir a transação, o estado do pedido deve ser verificado via polling de backup na API.

---

## 2. Pré-requisitos legais e checklist de conformidade do site para autônomos (FOP)

Mesmo que o código de pagamento esteja impecável, o departamento de monitoramento financeiro e segurança do Monobank não ativará a aceitação de pagamentos reais se o site não estiver em conformidade com a legislação ucraniana e as regras dos sistemas de pagamento Visa/Mastercard.

### 2.1. Pré-requisitos bancários
- **Conta FOP ou PJ aberta no Monobank:** O acquiring é conectado exclusivamente a contas empresariais. É proibido por lei receber pagamentos comerciais em cartões pessoais (preto ou branco) de pessoa física.
- **Códigos NACE/CAE adequados:** Os dados de registro do FOP devem conter códigos de atividade para comércio eletrônico ou serviços (por exemplo, `47.91` — Varejo pela internet, `62.01`/`62.02` — Programação e consultoria de computadores, `85.59` — Outros tipos de educação).

### 2.2. Checklist obrigatório de páginas antes da submissão para moderação

Antes de solicitar a ativação do terminal de internet, certifique-se de que seu site possui as seguintes seções (geralmente localizadas no rodapé):

- [ ] **Contrato Público de Oferta (Public Offer Agreement):** Descrição do objeto da transação, momento da celebração do contrato, direitos e obrigações das partes.
- [ ] **Política de Privacidade (Privacy Policy):** Formulação clara sobre quais dados dos clientes são coletados e como são protegidos, conforme a Lei da Ucrânia "Sobre a Proteção de Dados Pessoais".
- [ ] **Condições de Reembolso e Entrega:** Procedimento de devolução de mercadorias ou reembolso dentro de 14 dias, conforme a Lei da Ucrânia "Sobre a Proteção dos Direitos do Consumidor", e, para produtos/serviços digitais ou assinaturas, as regras de desistência do serviço.
- [ ] **Dados completos do empreendedor no rodapé do site:** Nome do FOP ou LTDA, CNPJ/IE (equivalente local), endereço jurídico, número de telefone de contato e e-mail funcional do suporte.
- [ ] **Preços transparentes e descrições:** Cada botão de pagamento deve ter um valor fixo em Hryvnias (UAH) e uma descrição clara do que exatamente o cliente está pagando.

> [!WARNING]
> Se o site não tiver oferta pública, dados do FOP ou se, em vez de preços, houver placeholders como "sob consulta", a equipe de segurança do Monobank rejeitará o registro do terminal na etapa de verificação.

---

## 3. Criação do terminal web no painel Monobank Business

Para interagir com a API de pagamentos, você precisa de uma chave de acesso pessoal — o `X-Token`. Ele é gerado gratuitamente dentro do painel pessoal do empreendedor.

### 3.1. Algoritmo passo a passo para abrir a caixa registradora

1. **Autenticação:** Acesse [web.monobank.ua →](https://web.monobank.ua/) e faça login usando QR code através do aplicativo Monobank no smartphone.
2. **Acesso à Caixa:** No menu de navegação esquerdo, selecione a seção **«Caixa»**.
3. **Adicionar ferramenta:** Clique no botão **«+ Adicionar ferramenta»** e escolha a opção **«Pagamento no site (desenvolvimento próprio)»**.
4. **Registro do terminal:** Digite o nome do projeto (por exemplo, `Meu Site` ou `Terminal Principal`) e confirme a operação clicando em **«Conectar»**.
5. **Geração do token de API:** Abra o terminal criado, vá para a aba **«Integrações / Chaves de API»**, clique em **«Criar token»** e copie a chave gerada.

> 📍 **Navegação no painel:** `web.monobank.ua` → `Caixa` → `+ Adicionar ferramenta` → `Pagamento no site (desenvolvimento próprio)`

![Criação de ferramenta de pagamento no painel Monobank Kasa](/api/guides-media/automation/monobank-acquiring-vibecoding/images/01-monobank-kasa-add-terminal.webp)

> 📍 **Obtenção da chave de API:** `Caixa` → `Seu Terminal` → `Integração` → `Criar X-Token`

![Janela modal de criação e cópia do token de acesso X-Token](/api/guides-media/automation/monobank-acquiring-vibecoding/images/02-monobank-create-token.webp)

### 3.2. Regras inegociáveis de segurança de tokens

- **Nenhum hardcode no código cliente:** O `X-Token` dá acesso direto ao gerenciamento das suas finanças e reembolsos. É estritamente proibido armazená-lo em JavaScript/HTML aberto ou publicá-lo em repositórios GitHub públicos.
- **Uso de variáveis de ambiente:** Armazene o token exclusivamente no arquivo `.env` no servidor, sob o nome `MONOBANK_TOKEN`, ou na seção Secrets da sua plataforma de hosting (Vercel, Render, Railway, Replit, Lovable).
- **Token de teste para desenvolvimento:** Para experimentos iniciais, o banco fornece um token de teste separado na página [api.monobank.ua →](https://api.monobank.ua/), que permite simular transações sem cobrar valores reais.

---

## 4. Prompts oficiais de IA do Monobank para vibe coders

A equipe do Monobank desenvolveu um conjunto de prompts de sistema oficiais para agentes de IA. Sua principal vantagem é a correspondência exata com os endpoints atuais do banco.

![Página da documentação oficial do Monobank para ferramentas de IA](/api/guides-media/automation/monobank-acquiring-vibecoding/images/03-monobank-ai-prompts-page.webp)

> [!WARNING]
> **A principal armadilha para iniciantes — valor em centavos:** A API do Monobank aceita todos os valores exclusivamente nas unidades mínimas da moeda (centavos/kopecks). $100\text{ UAH} = 10\,000\text{ kopecks}$. Se você passar `amount: 100`, o cliente pagará apenas 1 hryvnia.

### 4.1. Prompt base para criação de pagamento

Copie este prompt e envie-o no chat do seu assistente de IA:

```markdown
Quero adicionar ao meu site a funcionalidade de aceitar pagamentos via Monobank.

TAREFA:
Escreva um código completo e pronto para uso que crie um pagamento e redirecione o usuário para a página de pagamento.

O QUE DEVE ACONTECER:
1. No meu site, há um botão "Ir para o pagamento".
2. O usuário clica no botão → um pagamento é criado no Monobank.
3. O usuário é redirecionado para a página de pagamento do Monobank.
4. Após efetuar o pagamento, ele retorna ao meu site (https://mysite.com/payment-result).
5. Eu verifico o status do pagamento e exibo o resultado.

DADOS DE TESTE:
- Valor: 100 UAH (10000 kopecks)
- Descrição do pagamento: "Pagamento do pedido"
- URL de retorno do usuário: https://mysite.com/payment-result

MEU TOKEN DO MONOBANK:
Variável de ambiente MONOBANK_TOKEN definida no arquivo .env

DOCUMENTAÇÃO:
- Criação de pagamento: https://monobank.ua/api-docs/acquiring/methods/ia/post--api--merchant--invoice--create
- Verificação de status: https://monobank.ua/api-docs/acquiring/methods/ia/get--api--merchant--invoice--status

Escreva todo o código necessário para esse fluxo.
```

### 4.2. Prompt para configurar o handler de Webhook

Sem o webhook, o servidor não saberá do fato do pagamento bem-sucedido se o cliente fechar o navegador imediatamente após a cobrança:

```markdown
Preciso que meu servidor detecte automaticamente quando um usuário efetuar o pagamento de um pedido.

TAREFA:
Configure a recepção automática de informações de pagamentos do Monobank (webhook).

O QUE DEVE ACONTECER:
1. O usuário realiza o pagamento na página do Monobank.
2. O Monobank envia automaticamente uma requisição POST para o meu servidor.
3. Meu servidor recebe os dados: invoiceId, status (success/failure), valor.
4. O servidor valida a assinatura criptográfica (ECDSA SHA-256 via header x-sign).
5. Atualiza o status do pedido no banco de dados e registra o resultado em log.

IMPORTANTE:
- É obrigatório validar o header x-sign utilizando a chave pública do banco.
- Processe o corpo bruto da requisição (raw body Buffer / raw string), não o JSON já parseado.
- Trate os status: success, failure, processing, hold, expired.
- Garanta idempotência: se o mesmo webhook for recebido duas vezes, não duplique a entrega do produto.

Escreva todo o código para um handler de webhook robusto.
```

---

## 5. Pacote de skills monobank-acquiring: potencializando o agente

Se limitar apenas a um prompt curto, o assistente de IA escreverá um código básico (aproximadamente 6.8 de 10): o botão funcionará, mas o código não conterá verificação de assinaturas criptográficas, proteção contra adulteração de preço nem tratamento de erros raros de rede.

Para obter nível de produção (9.8–10 pontos), adiciona-se à raiz do projeto um pacote especializado de skills **`monobank-acquiring`**.

![Auditoria de integração de pagamento: comparação de segurança antes e depois do uso da skill](/api/guides-media/automation/monobank-acquiring-vibecoding/images/05-codex-skills-audit.webp)

```bash
# Розпакування пакета скілів у проєкт
curl -L -o monobank-acquiring.zip https://gotburnout.io/downloads/monobank-acquiring.zip
unzip monobank-acquiring.zip -d monobank-acquiring/
```

### 5.1. Anatomia e estrutura do pacote de skills

| Arquivo da Skill | Conteúdo e responsabilidades |
| :--- | :--- |
| **`SKILL.md`** | **Manifesto central:** fluxo base, autenticação `X-Token`, tipos de dados e tratamento de erros `400`, `403`, `429`, `500`. |
| **`quickstart.md`** | **Início rápido:** tutorial passo a passo para criar invoice e fallback-polling com comandos `curl` prontos. |
| **`invoice.md`** | **Ciclo de vida das faturas:** endpoints de criação de invoice, verificação de status, cancelamento e invalidação de links. |
| **`webhook.md`** | **Proteção criptográfica:** verificação matematicamente precisa da assinatura ECDSA SHA-256 do header `x-sign`. |
| **`payment.md`** | **Pagamentos diretos:** cobrança com token de cartão salvo, transações síncronas e verificação 3DS. |
| **`wallet.md`** | **Tokenização de cartões (Wallet):** armazenamento seguro do meio de pagamento do cliente no cofre do banco para pagamentos em um clique. |
| **`fiscal.md`** | **Recibos fiscais e pRRo:** descrição da estrutura do carrinho `basketOrder`, cálculo de impostos, descontos e exportação de recibos em PDF. |
| **`statement.md`** | **Extratos e analytics:** obtenção do registro de operações bem-sucedidas no período selecionado, com cálculo de taxas bancárias. |
| **`merchant.md`** | **Dados do merchant:** obtenção da chave pública do banco, gerenciamento de submerchants e caixas. |
| **`examples/`** | **Servidores prontos:** amostras funcionais de servidores em 6 linguagens populares (Node.js, Python, Go, PHP, C#, Java). |

---

## 6. Implementação prática: fluxo dinâmico de pagamento

Em cada projeto há uma estrutura diferente: alguns vendem consultas digitais com planos fixos, outros têm lojas online com carrinhos dinâmicos, e outros ainda possuem landing pages simples com um único botão de doação ou assinatura.

O erro principal dos iniciantes é "codificar rigidamente" o valor (por exemplo, 1000 UAH) diretamente no código cliente do botão ou no corpo da requisição POST vinda do navegador. Essa abordagem cria uma vulnerabilidade crítica de segurança.

### 6.1. Princípio de segurança: preço dinâmico em vez de hardcode

- **Nunca confie no valor vindo do navegador:** Se o JavaScript do cliente enviar `{ price: 1000 }` para o servidor, um atacante pode, via DevTools ou Postman, alterar esse valor para `{ price: 1 }` e adquirir o acesso por 1 hryvnia.
- **Servidor como única fonte da verdade (Single Source of Truth):** O frontend envia para o backend apenas o identificador do produto (`productId`), o plano escolhido (`planId: "pro"`) ou o array de identificadores do carrinho (`items: [{ id: "book_1", qty: 2 }]`).
- **Conversão automática para centavos:** O servidor obtém o valor atual do arquivo de configuração ou banco de dados e multiplica por 100 por conta própria:
  $$\text{amount} = \text{Math.round}(\text{realPrice} \times 100)$$
  Se você alterar o preço no banco ou anunciar um desconto, a fatura do Monobank será gerada com o novo valor correto sem intervenção no gateway de pagamento.

### 6.2. Prompt universal de IA para adaptação a qualquer projeto

Copie este prompt e envie-o no chat do seu agente de IA (Codex, Antigravity, Cursor ou Claude Code). O agente escaneará os arquivos do seu site, encontrará botões e preços existentes e construirá uma integração robusta:

```markdown
No diretório raiz do nosso projeto, foi adicionado o pacote oficial de conhecimento Monobank Acquiring na pasta /monobank-acquiring.

TAREFA:
1. Analise a arquitetura do nosso projeto: identifique onde estão descritos os produtos, serviços, planos tarifários ou botões de checkout/pagamento.
2. Crie ou integre um endpoint seguro no servidor para criação de pagamentos via Monobank:
   - O cliente envia APENAS o identificador do produto/plano tarifário (por exemplo, tariffId ou productId), mas NÃO o valor.
   - O servidor busca o preço real e atualizado da nossa configuração ou banco de dados e converte-o em centavos (preço * 100).
   - O token é obtido com segurança a partir de process.env.MONOBANK_TOKEN.
   - É feita uma requisição POST para https://api.monobank.ua/api/merchant/invoice/create.
   - Retorna-se ao cliente o link pageUrl para redirecionamento ao checkout do banco.
3. Atualize os nossos botões de pagamento existentes no frontend:
   - Adicione um estado de carregamento (indicador Loader e bloqueio do botão contra cliques repetidos).
   - Em caso de resposta bem-sucedida, realize um redirecionamento suave do usuário para o link recebido do Monobank.
   - Adicione tratamento de erros com notificações intuitivas para o usuário.
4. Crie uma página de resultado do pagamento (/payment-result) que verifique o status do pedido e informe o usuário sobre o sucesso.
5. Escreva testes unitários para validar o cálculo dinâmico dos valores e a criação da fatura (invoice).

Todas as exigências técnicas, estruturas de requisições e códigos de moeda devem ser retiradas dos arquivos na pasta /monobank-acquiring (especialmente SKILL.md e invoice.md).
```

![Trabalho do agente de IA na IDE com análise de estrutura e criação de endpoint dinâmico](/api/guides-media/automation/monobank-acquiring-vibecoding/images/04-codex-initial-prompt.webp)

### 6.3. Template arquitetural do backend para criação de invoice

Aqui estão implementações server-side testadas para os dois stacks web mais populares:

:::tabs
=== Next.js App Router
```typescript
// app/api/checkout/create-invoice/route.ts
import { NextResponse } from "next/server";

const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = {
  plan_starter: { title: "Тариф Starter", priceUah: 490 },
  plan_pro:     { title: "Тариф Pro",     priceUah: 990 },
  plan_vip:     { title: "Тариф VIP",     priceUah: 2490 },
};

export async function POST(req: Request) {
  try {
    const { productId } = await req.json();

    // 1. Валідація: ціна формується виключно на бекенді
    const product = PRODUCTS_CATALOG[productId];
    if (!product) {
      return NextResponse.json({ error: "Обраний товар або тариф не знайдено" }, { status: 400 });
    }

    const amountInKopecks = Math.round(product.priceUah * 100);
    const orderReference = `order_${productId}_${Date.now()}`;
    const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mysite.com";

    // 2. Запит до Monobank API
    const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", {
      method: "POST",
      headers: {
        "X-Token": process.env.MONOBANK_TOKEN!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: amountInKopecks,
        ccy: 980, // Гривня (ISO 4217)
        merchantPaymInfo: {
          reference: orderReference,
          destination: `Оплата: ${product.title}`,
          comment: `Замовлення ${orderReference}`,
        },
        redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`,
        webHookUrl: `${siteUrl}/api/payment/webhook`,
        validity: 3600, // Рахунок діє 1 годину
      }),
    });

    const data = await response.json();
    if (!response.ok) {
      return NextResponse.json({ error: data.errText || "Помилка створення рахунку в банку" }, { status: response.status });
    }

    return NextResponse.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId });
  } catch (error) {
    return NextResponse.json({ error: "Внутрішня помилка ініціалізації платежу" }, { status: 500 });
  }
}
```
=== Express / Node.js
```typescript
// server.ts
import express from "express";

const app = express();
app.use(express.json());

const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = {
  plan_starter: { title: "Тариф Starter", priceUah: 490 },
  plan_pro:     { title: "Тариф Pro",     priceUah: 990 },
  plan_vip:     { title: "Тариф VIP",     priceUah: 2490 },
};

app.post("/api/checkout/create-invoice", async (req, res) => {
  try {
    const { productId } = req.body;

    const product = PRODUCTS_CATALOG[productId];
    if (!product) {
      return res.status(400).json({ error: "Обраний товар або тариф не знайдено" });
    }

    const amountInKopecks = Math.round(product.priceUah * 100);
    const orderReference = `order_${productId}_${Date.now()}`;
    const siteUrl = process.env.SITE_URL || "https://mysite.com";

    const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", {
      method: "POST",
      headers: {
        "X-Token": process.env.MONOBANK_TOKEN!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: amountInKopecks,
        ccy: 980,
        merchantPaymInfo: {
          reference: orderReference,
          destination: `Оплата: ${product.title}`,
          comment: `Замовлення ${orderReference}`,
        },
        redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`,
        webHookUrl: `${siteUrl}/api/payment/webhook`,
        validity: 3600,
      }),
    });

    const data = await response.json();
    if (!response.ok) {
      return res.status(response.status).json({ error: data.errText || "Помилка банку" });
    }

    res.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId });
  } catch (error) {
    res.status(500).json({ error: "Внутрішня помилка ініціалізації платежу" });
  }
});
```
:::

---

## 7. Tratamento seguro de webhooks e assinatura criptográfica ECDSA

A parte mais crítica de qualquer integração financeira é a verificação das notificações de pagamento. Um atacante pode enviar uma requisição HTTP forjada para seu endereço `/api/payment/webhook` com uma mensagem falsa de sucesso.

Para prevenir isso, o Monobank assina cada webhook usando o algoritmo assimétrico **ECDSA (curva secp256r1 / SHA-256)** e transmite a assinatura no header HTTP `x-sign`.

### 7.1. Por que `JSON.stringify` quebra a verificação da assinatura

> [!CAUTION]
> **Armadilha crítica do `rawBody`:** Para verificar a assinatura ECDSA, é necessário o **fluxo de bytes original rigorosamente** enviado pelo servidor do Monobank. Se você tentar parsear o JSON e chamar novamente `JSON.stringify(req.body)`, a ordem das chaves, espaços ou quebras de linha mudarão. Isso resultará em um hash SHA-256 diferente, e a verificação da assinatura **retornará erro garantidamente**!

### 7.2. Implementação da verificação do webhook

Abaixo está a implementação correta para obter o corpo bruto e verificar a assinatura para Next.js App Router e Express:

:::tabs
=== Next.js App Router
```typescript
// app/api/payment/webhook/route.ts
import { NextResponse } from "next/server";
import crypto from "crypto";

let cachedPubKey: string | null = null;

async function getMonobankPubKey(token: string): Promise<string> {
  if (cachedPubKey) return cachedPubKey;

  const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", {
    headers: { "X-Token": token },
    next: { revalidate: 86400 }, // Кешуємо публічний ключ на 24 години
  });
  const data = await res.json();
  cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`;
  return cachedPubKey;
}

export async function POST(req: Request) {
  const signature = req.headers.get("x-sign");
  if (!signature) {
    return new NextResponse("Missing x-sign header", { status: 400 });
  }

  // 1. Отримуємо сире незмінене тіло запиту у вигляді тексту
  const rawBody = await req.text();

  try {
    const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!);

    // 2. Верифікуємо підпис ECDSA SHA-256
    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawBody);
    const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64"));

    if (!isValid) {
      console.error("Вебхук відхилено: недійсний підпис x-sign");
      return new NextResponse("Invalid signature", { status: 400 });
    }

    // 3. Парсимо дані тільки після успішної перевірки криптографії
    const payload = JSON.parse(rawBody);
    const { invoiceId, status, amount, reference } = payload;

    if (status === "success") {
      // Тут активуємо замовлення в базі даних з перевіркою на дублікати (ідемпотентність)
      console.log(`Замовлення ${reference} (${invoiceId}) оплачено на суму ${amount / 100} грн`);
    }

    return new NextResponse("OK", { status: 200 });
  } catch (error) {
    console.error("Помилка обробки вебхука:", error);
    return new NextResponse("Internal verification error", { status: 500 });
  }
}
```
=== Express / Node.js
```typescript
// webhook.ts
import express from "express";
import crypto from "crypto";

const app = express();

// Зберігаємо сирі байти для верифікації криптографічного підпису
app.use(express.json({
  verify: (req: any, _res, buf) => {
    req.rawBody = buf.toString("utf-8");
  }
}));

let cachedPubKey: string | null = null;

async function getMonobankPubKey(token: string): Promise<string> {
  if (cachedPubKey) return cachedPubKey;
  const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", {
    headers: { "X-Token": token },
  });
  const data = await res.json();
  cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`;
  return cachedPubKey;
}

app.post("/api/payment/webhook", async (req: any, res) => {
  const signature = req.headers["x-sign"] as string;
  if (!signature) {
    return res.status(400).send("Missing x-sign header");
  }

  try {
    const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!);
    const rawBody = req.rawBody;

    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawBody);
    const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64"));

    if (!isValid) {
      return res.status(400).send("Invalid signature");
    }

    const { invoiceId, status, amount, reference } = req.body;
    if (status === "success") {
      console.log(`Успішна оплата ${reference} (${invoiceId}) на суму ${amount / 100} грн`);
    }

    res.sendStatus(200);
  } catch (err) {
    res.status(500).send("Internal verification error");
  }
});
```
:::

---

## 8. Teste local de webhooks (Localhost & Cloudflare Tunnels)

Problema comum entre vibe coders: ao iniciar o servidor em `http://localhost:3000`, o banco não consegue entregar o webhook, pois a máquina local não possui IP público na internet.

O Monobank envia webhooks exclusivamente para endereços públicos com protocolo `HTTPS` válido. Para testar o ciclo completo no seu computador, configure um túnel seguro.

### 8.1. Início rápido do túnel (sem cadastro e gratuito)

Use o Cloudflare Tunnel (`cloudflared`) ou `untun`:

:::tabs
=== Cloudflare (npx untun)
```bash
# Миттєвий HTTPS-тунель на 3000 порт без встановлення утиліт
npx untun@latest tunnel --port 3000
```
=== Cloudflare CLI (cloudflared)
```bash
# Встановлення через brew на macOS
brew install cloudflared
cloudflared tunnel --url http://localhost:3000
```
=== ngrok
```bash
# Якщо використовуєте ngrok
ngrok http 3000
```
:::

Após a execução, você receberá um endereço HTTPS público temporário, por exemplo:
`https://your-tunnel-name.trycloudflare.com`

### 8.2. Configuração do webhook para teste local
No código de criação da invoice, durante o desenvolvimento, especifique o endereço obtido:
```typescript
webHookUrl: "https://your-tunnel-name.trycloudflare.com/api/payment/webhook"
```
Agora, ao realizar um pagamento de teste, o banco enviará um webhook real diretamente para seu servidor local no terminal, permitindo confirmar que a assinatura `x-sign` passa na verificação.

---

## 9. UX da página de retorno e resolução de Race Condition

Quando o cliente paga a fatura via aplicativo Monobank ou Apple Pay, o banco o redireciona imediatamente para o `redirectUrl` (`/payment-result?ref=...`).

No entanto, o webhook de rede do servidor do banco para o seu servidor pode sofrer atrasos de 1–2 segundos devido às rotas de rede. Se a página de resultado verificar o status no banco de dados imediatamente, correrá o risco de mostrar: *«Pedido não pago»*, causando pânico no cliente (os fundos foram debitados do cartão, mas o site informa ausência de pagamento).

### 9.1. Padrão de engenharia para resolver a condição de corrida
1. **Estado inicial de carregamento:** A página abre com status neutro: `«Confirmando pagamento no banco...»` e indicador animado.
2. **Polling curto (Short Polling):** O frontend faz até 5 requisições rápidas à sua própria API a cada 1.5 segundos (`/api/orders/check-status?ref=...`), aguardando o webhook alterar o status do pedido no banco para `success`.
3. **Fallback:** Se em 8 segundos o webhook não chegar, o cliente vê a mensagem: *«Pagamento recebido para processamento. Assim que o banco confirmar a transação, o acesso será liberado automaticamente»*.

### 9.2. Componente React pronto para a página de resultado

```tsx
// app/payment-result/page.tsx
"use client";

import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";

export default function PaymentResultPage() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const ref = searchParams.get("ref");

  const [status, setStatus] = useState<"checking" | "success" | "pending" | "failed">("checking");

  useEffect(() => {
    if (!ref) {
      setStatus("failed");
      return;
    }

    let attempts = 0;
    const maxAttempts = 5;

    const interval = setInterval(async () => {
      attempts++;
      try {
        const res = await fetch(`/api/orders/status?ref=${encodeURIComponent(ref)}`);
        const data = await res.json();

        if (data.status === "success") {
          clearInterval(interval);
          setStatus("success");
        } else if (attempts >= maxAttempts) {
          clearInterval(interval);
          // Якщо вебхук затримався, переводимо в статус очікування, а не помилки
          setStatus(data.status === "processing" ? "pending" : "pending");
        }
      } catch (err) {
        if (attempts >= maxAttempts) {
          clearInterval(interval);
          setStatus("pending");
        }
      }
    }, 1500);

    return () => clearInterval(interval);
  }, [ref]);

  return (
    <div className="max-w-md mx-auto my-16 p-8 rounded-2xl bg-neutral-900 border border-neutral-800 text-center text-white">
      {status === "checking" && (
        <div>
          <div className="w-12 h-12 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
          <h2 className="text-xl font-semibold mb-2">Підтверджуємо оплату...</h2>
          <p className="text-sm text-neutral-400">Отримуємо статус транзакції від банку. Зачекайте кілька секунд.</p>
        </div>
      )}

      {status === "success" && (
        <div>
          <div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">✓</div>
          <h2 className="text-xl font-semibold mb-2">Оплата успішна!</h2>
          <p className="text-sm text-neutral-400 mb-6">Ваше замовлення #{ref} успішно підтверджено.</p>
          <button onClick={() => router.push("/dashboard")} className="px-6 py-2.5 rounded-xl bg-amber-500 hover:bg-amber-400 text-black font-semibold transition">
            Перейти до кабінету
          </button>
        </div>
      )}

      {status === "pending" && (
        <div>
          <div className="w-12 h-12 bg-amber-500/20 text-amber-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">⏳</div>
          <h2 className="text-xl font-semibold mb-2">Платіж в обробці</h2>
          <p className="text-sm text-neutral-400 mb-6">Кошти зарезервовано. Підтвердження надійде протягом 1–2 хвилин.</p>
          <button onClick={() => router.push("/")} className="px-6 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-white font-medium transition">
            На головну
          </button>
        </div>
      )}
    </div>
  );
}
```

---

## 10. Recursos avançados: pRRo embutido, hold e Wallet

O Monobank Acquiring oferece um espectro completo de ferramentas para modelos de negócios mais complexos:

![Página final de pagamento Monobank Hosted Checkout com Apple Pay e cartões](/api/guides-media/automation/monobank-acquiring-vibecoding/images/06-monobank-checkout-page.webp)

### 10.1. Registradora Fiscal Eletrônica (pRRo): Checkbox integrado gratuito na Caixa Monobank

Para a maioria dos FOPs ucranianos dos grupos 2 e 3, a fiscalização de vendas online é requisito legal obrigatório.

A grande vantagem do Monobank para empreendedores é a **integração gratuita embutida com o serviço de pRRo Checkbox**:
- **Ativação em um clique:** No painel `web.monobank.ua`, vá às configurações do terminal criado e ative o switch **«Fiscalização via Checkbox»**. O Monobank cria a caixa registradora e assina os recibos com sua chave KEPP gratuitamente.
- **Fiscalização automática:** Se você tem um catálogo padrão de produtos com alíquota única, nem precisa alterar o código de criação do pagamento — o recibo é formado automaticamente pelo campo `destination` na descrição do pagamento.
- **Fiscalização avançada via API:** Se você vende produtos com diferentes alíquotas de IVA ou itens sujeitos a impostos específicos com códigos UKTZED, passe o array `basketOrder` dentro do objeto `merchantPaymInfo`:

```json
"merchantPaymInfo": {
  "reference": "order_1001",
  "destination": "Оплата онлайн-курсу",
  "customerEmails": ["client@example.com"],
  "basketOrder": [
    {
      "name": "Доступ до курсу з вайбкодингу",
      "qty": 1,
      "sum": 99000,
      "code": "SKU-COURSE-01",
      "unit": "шт.",
      "total": 99000
    }
  ]
}
```

### 10.2. Pagamento em duas etapas (Hold)

Se você vende produtos físicos que podem estar indisponíveis no estoque, use o modo de hold:

- **Bloqueio:** Passe `paymentType: "hold"` ao criar a invoice. Os fundos ficam congelados no cartão do comprador por até 9 dias.
- **Captura (Finalize):** Após verificar a disponibilidade do produto, chame `/api/merchant/invoice/finalize`. Pode-se capturar tanto o valor total quanto menor (por exemplo, se um item não estava disponível).
- **Cancelamento:** Se o produto não estiver disponível, chame `/api/merchant/invoice/cancel` — o pagamento é cancelado sem taxas bancárias para o comprador.

### 10.3. Salvamento de cartões e assinaturas (Wallet)

Se você lança um serviço SaaS com assinatura mensal recorrente, passe `saveCardData: true` ao criar a primeira invoice. Após o pagamento bem-sucedido, o webhook retornará o `walletId`. Use esse token para cobranças automáticas subsequentes sem que o cliente precise reinserir os dados do cartão.

---

## 11. Matriz de segurança e testes automatizados (Vitest / Jest)

Pagamento é zona de máxima responsabilidade financeira. Um erro em um botão comum causa irritação, mas um erro no gateway de pagamento leva a perdas materiais diretas ou bloqueio da caixa pelo banco.

Antes de migrar para produção, execute a matriz de testes de segurança:

| Teste de Segurança | O que é verificado | Comportamento esperado do sistema |
| :--- | :--- | :--- |
| **1. Proteção contra adulteração de preço (Price Tampering)** | Cliente envia `productId: "vip"`, mas tenta injetar na requisição `amount: 100` (1 UAH) | Servidor ignora o campo `amount` do cliente, pega o preço real da config (2490 UAH = 249 000 kopecks). Se `productId` for falsificado — retorna `HTTP 400`. |
| **2. Bloqueio sem assinatura** | Requisição POST chega ao endpoint `/api/payment/webhook` sem header `x-sign` | Requisição é bloqueada imediatamente com resposta `HTTP 400 Bad Request`. Nenhuma processamento do pedido ocorre. |
| **3. Bloqueio de assinaturas falsas** | Atacante enviou assinatura fake em `x-sign` com `status: "success"` | Verificação `crypto.verify(SHA256, ...)` retorna `false`. Servidor responde `HTTP 400/401`, acesso ou produto não são liberados. |
| **4. Idempotência do webhook (Duplicate Delivery)** | Monobank enviou o mesmo webhook `success` duas ou três vezes seguidas por lag de rede | Acesso ou produto ativam **apenas uma vez**. Webhook repetido não duplica entrega, respondendo ao banco `HTTP 200 OK`. |
| **5. Resiliência a Race Condition** | Webhook do banco chegou antes de finalizar a requisição HTTP inicial de criação da invoice no banco de dados | Handler do webhook é resiliente à ausência do registro no BD (usa `UPSERT` ou cria pedido on-the-fly). |
| **6. Conformidade com Rate Limits (Buffer)** | Em falha do webhook, inicia-se polling de status `/api/merchant/invoice/status` | Requisições executadas com pausa **mínima de 15 segundos**, protegendo o terminal de bloqueio `HTTP 429 Too Many Requests`. |

### 11.1. Conjunto pronto de testes automatizados para seu projeto

Peça ao seu agente de IA para adicionar o seguinte suite de testes ao projeto (`monobank-acquiring.test.ts`):

```typescript
import { describe, it, expect } from "vitest";
import crypto from "crypto";

// Генерація тестової пари ECDSA secp256r1 для симуляції підписів Monobank
const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", {
  namedCurve: "prime256v1",
  publicKeyEncoding: { type: "spki", format: "pem" },
  privateKeyEncoding: { type: "pkcs8", format: "pem" },
});

describe("Monobank Acquiring Security Tests", () => {
  it("Тест 1: Не дозволяє клієнту маніпулювати ціною", () => {
    const CATALOG: Record<string, { priceUah: number }> = { plan_pro: { priceUah: 990 } };
    const clientPayload = { productId: "plan_pro", amount: 100 }; // Спроба підсунути 1 грн
    
    const safeAmount = Math.round(CATALOG[clientPayload.productId].priceUah * 100);
    expect(safeAmount).toBe(99000); // 990.00 грн, а не 1.00 грн
  });

  it("Тест 2: Відхиляє вебхук без обов'язкового заголовка x-sign", () => {
    const headers: Record<string, string> = {};
    const hasSignature = Boolean(headers["x-sign"]);
    expect(hasSignature).toBe(false);
  });

  it("Тест 3: Успішно верифікує валідний цифровий підпис банку", () => {
    const rawPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 });
    
    const signer = crypto.createSign("SHA256");
    signer.update(rawPayload);
    const validSignatureBase64 = signer.sign(privateKey, "base64");

    const verifier = crypto.createVerify("SHA256");
    verifier.update(rawPayload);
    const isValid = verifier.verify(publicKey, Buffer.from(validSignatureBase64, "base64"));

    expect(isValid).toBe(true);
  });

  it("Тест 4: Блокує підроблений підпис або модифіковане тіло запиту", () => {
    const originalPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 });
    const tamperedPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 1000 });

    const signer = crypto.createSign("SHA256");
    signer.update(originalPayload);
    const signature = signer.sign(privateKey, "base64");

    const verifier = crypto.createVerify("SHA256");
    verifier.update(tamperedPayload);
    const isValid = verifier.verify(publicKey, Buffer.from(signature, "base64"));

    expect(isValid).toBe(false);
  });

  it("Тест 5: Ідемпотентність — повторний вебхук не видає товар двічі", () => {
    const processedOrders = new Set<string>();
    
    function handleOrder(invoiceId: string): { processed: boolean } {
      if (processedOrders.has(invoiceId)) {
        return { processed: false }; // Дубль відхилено
      }
      processedOrders.add(invoiceId);
      return { processed: true };
    }

    expect(handleOrder("inv_001").processed).toBe(true);  // Перший вебхук
    expect(handleOrder("inv_001").processed).toBe(false); // Повторний прихід дубліката
    expect(processedOrders.size).toBe(1);
  });
});
```

---

## 12. Checklist final de engenharia antes do lançamento

Verifique seu módulo de pagamento nestes 10 pontos antes de alternar para pagamentos reais:

- [ ] **Conformidade do site:** Rodapé contém links para Oferta Pública, Política de Privacidade, Condições de Reembolso e dados completos do FOP com CNPJ/IE.
- [ ] **Valores em centavos:** Todos os campos `amount` multiplicados por 100 ($1\text{ UAH} = 100\text{ kopecks}$) via `Math.round`.
- [ ] **Segurança da chave:** Token movido para `.env` sob o nome `MONOBANK_TOKEN` e adicionado ao `.gitignore`.
- [ ] **Preços do backend (SSOT):** Cliente envia apenas ID do produto; valor vem exclusivamente do banco ou config.
- [ ] **Tratamento rawBody:** Webhook verifica buffer textual ou binário original (`req.text()` ou `req.rawBody`), evitando double parsing via `JSON.stringify`.
- [ ] **Proteção criptográfica:** Webhook valida assinatura `x-sign` via chave pública do banco com algoritmo `SHA256` ECDSA.
- [ ] **URL pública para webhook:** Servidor acessível externamente via certificado SSL HTTPS válido (para testes locais, Cloudflare Tunnel ou ngrok servem).
- [ ] **Idempotência:** Chegada repetida do mesmo webhook não causa dupla entrega de produto ou assinatura.
- [ ] **UX da página de resultado:** Em `/payment-result`, configurado loader de espera e polling curto de status para proteger usuário de race condition.
- [ ] **Pagamento real de 1–5 UAH:** Transação de teste realizada com cartão real, verificando débito e exibição no painel do banco.

---

## Recursos úteis e arquivos para download

- [Baixar pacote de skills monobank-acquiring.zip (38 KB) →](/downloads/monobank-acquiring.zip)
- [Página oficial de Prompts de IA do Monobank →](https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts)
- [Painel pessoal Monobank Business →](https://web.monobank.ua/)
- [Vídeo prático no YouTube com análise completa do fluxo →](https://youtu.be/GMh_fOCiQ4E)
- [Portal de teste da API Monobank →](https://api.monobank.ua/)