﻿# Exalta Pay API v1

Guia oficial para integrar lojas, ERPs e sistemas externos ao gateway Exalta
Pay. Troque `https://morenintop.shop` pelo domínio da instalação.

## Regras de segurança

- Chame a API somente a partir do backend da loja.
- Nunca envie ou incorpore `sk_test`/`sk_live` em HTML, aplicativos públicos ou
  JavaScript executado no navegador.
- Use chaves `test` no sandbox e `live` apenas em produção. Uma chave nunca
  acessa cobranças do outro ambiente.
- Armazene a `secret key` em cofre de segredos ou variável de ambiente. Ela é
  exibida uma única vez pelo painel.
- Use HTTPS em produção.

## Endpoints

| Método | Rota | Uso |
| --- | --- | --- |
| `POST` | `/api/v1/payments` | Criar PIX/link |
| `GET` | `/api/v1/payments?reference=...` | Consultar cobrança |
| `POST` | `/api/v1/webhooks` | Criar endpoint ou reenviar evento |
| `POST` | `/api/v1/payment-simulations` | Aprovar/falhar cobrança `test` |
| `GET` | `/api/v1/health` | Conferir API e worker de webhooks |

`/api/payments` continua temporariamente como alias depreciado da v1.

## Autenticação e limites

Envie nas rotas privadas:

```http
Content-Type: application/json
X-Public-Key: pk_test_xxxxxxxxx
X-Secret-Key: sk_test_xxxxxxxxx
```

O limite padrão é 30 requisições `POST` e 120 `GET` por minuto, por chave/IP.
A resposta expõe `X-RateLimit-Limit`, `X-RateLimit-Remaining` e, em bloqueios,
`Retry-After`. Toda resposta também possui `X-Request-ID`.

## Criar pagamento

```http
POST /api/v1/payments
Idempotency-Key: pedido-1001-pix-1
```

`Idempotency-Key` é obrigatório, deve ter de 8 a 128 caracteres e ser único
para a operação. Repetir a mesma chave com o mesmo JSON devolve a resposta
original, sem criar outro PIX. Reutilizá-la com um JSON diferente retorna
conflito HTTP 409.

### Payload

```json
{
  "method": "pix",
  "amount": 49.90,
  "external_reference": "pedido-1001",
  "description": "Pedido #1001",
  "expires_in_minutes": 30,
  "success_url": "https://loja.example/pedidos/1001/sucesso",
  "cancel_url": "https://loja.example/pedidos/1001/cancelado",
  "return_url": "https://loja.example/pedidos/1001",
  "payer": {
    "name": "Maria Silva",
    "email": "maria@email.com",
    "document": "52998224725",
    "phone": "11987654321"
  }
}
```

Validações:

- `method`: exclusivamente `pix` ou `link`;
- `amount`: entre R$ 3,00 e R$ 100.000,00;
- `external_reference`: obrigatória, 3–120 caracteres;
- `payer.document`: CPF/CNPJ válido, incluindo dígitos verificadores;
- `payer.email`: e-mail válido;
- `payer.phone`: telefone brasileiro com DDD, 10 ou 11 dígitos;
- URLs: HTTPS em produção; HTTP é aceito somente para localhost no sandbox;
- `expires_in_minutes`: 5–1440.

### cURL

```bash
curl -X POST "https://morenintop.shop/api/v1/payments" \
  -H "Content-Type: application/json" \
  -H "X-Public-Key: pk_test_xxxxxxxxx" \
  -H "X-Secret-Key: sk_test_xxxxxxxxx" \
  -H "Idempotency-Key: pedido-1001-pix-1" \
  -d '{
    "method": "pix",
    "amount": 49.90,
    "external_reference": "pedido-1001",
    "description": "Pedido #1001",
    "payer": {
      "name": "Maria Silva",
      "email": "maria@email.com",
      "document": "52998224725",
      "phone": "11987654321"
    }
  }'
```

### Node.js no backend

```js
const response = await fetch(`${process.env.EXALTA_URL}/api/v1/payments`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Public-Key": process.env.EXALTA_PUBLIC_KEY,
    "X-Secret-Key": process.env.EXALTA_SECRET_KEY,
    "Idempotency-Key": `pedido-${pedido.id}-pix-1`
  },
  body: JSON.stringify({
    method: "pix",
    amount: pedido.total,
    external_reference: String(pedido.id),
    description: `Pedido #${pedido.id}`,
    success_url: `${process.env.LOJA_URL}/pedidos/${pedido.id}/sucesso`,
    cancel_url: `${process.env.LOJA_URL}/pedidos/${pedido.id}/cancelado`,
    payer: pedido.cliente
  })
});

const payment = await response.json();
if (!response.ok) throw new Error(payment.mensagem);
```

O navegador chama um endpoint do backend da própria loja. Ele nunca chama a
Exalta Pay com a chave secreta.

### Resposta

```json
{
  "sucesso": true,
  "mensagem": "Pagamento criado com sucesso.",
  "pagamento": {
    "referencia": "PIX-8F27A91B0C42",
    "external_reference": "pedido-1001",
    "ambiente": "test",
    "status": "pending",
    "expirado": false,
    "metodo": "pix",
    "valor_bruto": 49.9,
    "taxa": 0,
    "valor_liquido": 49.9,
    "moeda": "BRL",
    "checkout_url": "https://morenintop.shop/checkout?token=chk_...",
    "pix": {
      "copia_e_cola": "EXALTA_PAY_SANDBOX_PIX-8F27A91B0C42_..."
    },
    "urls": {
      "success": "https://loja.example/pedidos/1001/sucesso",
      "cancel": "https://loja.example/pedidos/1001/cancelado",
      "return": null
    },
    "expira_em": "2026-07-28T20:30:00-03:00"
  }
}
```

O `checkout_url` usa um token aleatório não enumerável. Não monte essa URL
manualmente e não exponha a referência como credencial pública.

## Sandbox

Cobranças criadas com `pk_test` nunca chamam adquirente de produção e nunca
movimentam dinheiro real. Para simular:

```http
POST /api/v1/payment-simulations
X-Public-Key: pk_test_...
X-Secret-Key: sk_test_...

{"reference":"PIX-8F27A91B0C42","status":"approved"}
```

O sandbox percorre o mesmo fluxo interno de status, crédito idempotente e
webhook. A rota recusa chaves `live`.

## Consultar pagamento

```http
GET /api/v1/payments?reference=PIX-8F27A91B0C42
```

A consulta só encontra cobranças do lojista e do ambiente da chave utilizada.
Cobranças criadas pela API aparecem em **Transações**, com origem `API`.

## Webhooks

### Cadastrar endpoint

```http
POST /api/v1/webhooks

{
  "url": "https://loja.example/webhooks/exalta-pay",
  "events": ["pagamento.aprovado"]
}
```

URLs privadas, reservadas ou sem HTTPS são rejeitadas em produção. Guarde o
`secret` retornado; ele não deve ser público.

### Payload entregue

```json
{
  "id": "evt_f4e159c5f8db7f4d6c6628ae",
  "event": "pagamento.aprovado",
  "created_at": "2026-07-28T19:42:18-03:00",
  "data": {
    "reference": "PIX-8F27A91B0C42",
    "external_reference": "pedido-1001",
    "environment": "live",
    "status": "approved",
    "method": "pix",
    "amount": 49.90,
    "fee": 0,
    "net_amount": 49.90,
    "currency": "BRL",
    "description": "Pedido #1001",
    "origin": "api",
    "checkout_url": "https://morenintop.shop/checkout?token=chk_...",
    "success_url": "https://loja.example/pedidos/1001/sucesso",
    "cancel_url": "https://loja.example/pedidos/1001/cancelado",
    "return_url": null,
    "expires_at": "2026-07-28T20:30:00-03:00",
    "payer": {
      "name": "Maria Silva",
      "email": "maria@email.com",
      "document": "52998224725",
      "phone": "11987654321"
    }
  }
}
```

Cada entrega envia:

- `X-Exalta-Event`;
- `X-Exalta-Delivery`;
- `X-Exalta-Timestamp`;
- `X-Exalta-Signature: sha256=<HMAC>`.

Valide `HMAC-SHA256(timestamp + "." + corpo_json_bruto, secret)` com
`hash_equals`, rejeite timestamps antigos e responda HTTP 2xx apenas depois de
persistir o evento. O consumidor também deve tratar `X-Exalta-Delivery` de
forma idempotente.

```php
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_EXALTA_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_EXALTA_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac(
    'sha256',
    $timestamp . '.' . $raw,
    getenv('EXALTA_WEBHOOK_SECRET')
);

if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

http_response_code(204);
```

Não há IP fixo garantido; a assinatura é a autenticação do evento.

### Retentativas e monitoramento

Falhas são retentadas em 1, 5, 15, 60, 360 e 1440 minutos. Em hospedagem com
cron, execute a cada minuto:

```cron
* * * * * /usr/bin/php /caminho/bin/webhook-worker.php 50
```

Em servidor com supervisor/systemd, mantenha:

```bash
php bin/webhook-worker-loop.php 15 50
```

Monitore `GET /api/v1/health` com autenticação. `webhook_worker.ok: false`
indica que o worker não registrou atividade nos últimos 180 segundos.

## Testes automatizados

Em um banco descartável com o schema e migrations aplicados:

```bash
EXALTA_RUN_INTEGRATION_TESTS=1 php tests/gateway_v1_integration.php
```

O teste cobre criação sandbox, separação de ambiente, idempotência, aprovação,
crédito único na carteira, enfileiramento do webhook e assinatura HMAC.

## Checklist antes de produção

1. Aplicar `database/gateway_api_v1_migration.sql`.
2. Gerar novas chaves no painel; não reutilizar credenciais demo.
3. Integrar e aprovar o fluxo completo com `pk_test`.
4. Guardar `external_reference` e `referencia` no pedido.
5. Validar assinatura e idempotência no receptor de webhook.
6. Instalar e monitorar o worker de retentativas.
7. Executar os testes automatizados em banco descartável.
8. Só então criar uma chave `live` e fazer uma transação real de baixo valor.
