# Introdução à API ValidaPay

API REST para receber pagamentos por Pix, cartão de crédito e boleto, com suporte a assinaturas, split de pagamento, subcontas e notas fiscais.

## Regras que valem para toda a API

- REST + JSON sobre HTTPS.
- **Valores monetários em reais (BRL), nunca em centavos.** Duas casas decimais com ponto: `10.00`.
- Autenticação OAuth2 `client_credentials`; o token vai em `Authorization: Bearer {access_token}`.
- Peça apenas os scopes necessários, separados por espaço.
- Datas em ISO 8601 (`2026-07-14T21:39:36.322Z`).

## Ambientes

| Ambiente | API | OAuth |
|---|---|---|
| Produção | `https://api.validapay.com.br` | `https://oauth2.validapay.com.br/auth/token` |
| Sandbox | `https://sandbox.validapay.com.br` | `https://oauth2-sandbox.validapay.com.br/auth/token` |

Painel: `https://app.validapay.com.br` · Credenciais: `https://app.validapay.com.br/integracao/api`

## Autenticação

```http
POST https://oauth2-sandbox.validapay.com.br/auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}&scope={scope}
```

Resposta: `{ "access_token", "expires_in", "token_type": "Bearer" }`.

Use a URL completa da tabela acima; ela **já inclui** `/auth/token`.

**`expires_in` não é fixo em 3600.** O servidor reaproveita tokens já emitidos e devolve o tempo restante do token vigente, que pode ser bem menor. Calcule a validade por `min(expires_in, exp do JWT)` com margem de segurança.

Não gere um token por requisição, e não reautentique em laço ao tomar 401 — a nova chamada pode devolver o mesmo token.

**O token sai mesmo com scope não liberado.** O endpoint OAuth não recusa scope indevido; a checagem acontece na chamada de negócio. Não use "o token saiu" como prova de permissão.

### Identificadores em sandbox

Em sandbox os IDs vêm com o prefixo do ambiente: `SANDBOX_cha_1788621827782_rdo55ozlh`. Trate identificadores como string opaca — validar com `startsWith('cha_')` funciona em produção e quebra em sandbox.

### Scopes

| Scope | Uso |
|---|---|
| `pix.cob/write` `pix.cob/read` | QR Pix imediato e consulta |
| `checkouts/write` `checkouts/read` | Links, sessões e checkout transparente |
| `products/write` `products/read` | Produtos e preços |
| `customers/write` `customers/read` `customers/delete` | Cadastro de clientes |
| `subscriptions/write` `subscriptions/read` | Gestão de assinaturas (não cria) |
| `proposals/write` | Onboarding de subcontas |
| `subaccounts/read` | Listar subcontas e cobranças |
| `wallet/write` `wallet/read` | Saldo, extrato, saque, devolução, pagar em sandbox |
| `nota.fiscal/write` `nota.fiscal/read` | Notas fiscais |

## Prefixos de identificador

| Prefixo | Recurso |
|---|---|
| `cha_` | cobrança |
| `prod_` | produto |
| `price_` | preço |
| `pl_` | link de pagamento |
| `cs_` | sessão de checkout |
| `cus_` | cliente |
| `pm_` | cartão tokenizado |
| `sub_` | assinatura |
| `item_` | item de assinatura |
| `ref_` | devolução |
| `wdr_` | saque |

## Header de subconta

`X-Sub-Account: {numero}` faz a requisição operar sobre uma subconta em vez da conta master. Usado, por exemplo, para gerar uma cobrança Pix do seller com split para a master.

## Idempotência: existe em uma rota, não na outra

| Rota | Idempotência |
|---|---|
| `POST /v1/charges` | **Sim.** Envie `externalId`; duplicata é recusada com `409 DUPLICATE_CHARGE`, retornando o `chargeId` original |
| `POST /v1/charges/pix` | **Não.** `externalId` é aceito e descartado; duas chamadas criam **duas cobranças** |

Em `/v1/charges/pix`, controle duplicidade do seu lado — um índice `pedido → cobrança`, verificado antes de criar. Duplo clique no front, retry de rede ou reprocessamento de fila geram cobranças Pix repetidas para o mesmo pedido.

Não confunda com `externalTxid`, que existe nessa rota mas serve para identificar loja, caixa ou vendedor.

## Formatos de erro

A API usa quatro formatos. Trate sempre pelo código, nunca pela mensagem.

**Envelope** — dominante; toda exceção lançada nos módulos v1 sai assim:

```json
{
  "error": {
    "message": "Cobrança duplicada: já existe uma cobrança para este pedido",
    "code": "DUPLICATE_CHARGE",
    "details": { "chargeId": "cha_1784065113577_5dw2oyfic" },
    "timestamp": "2026-07-14T21:39:36.322Z"
  }
}
```

**OAuth2** (`POST /auth/token`) — segue a RFC 6749, com `error` como string:

```json
{ "error": "Invalid credentials" }
```

**Token sem o scope necessário** (401) — quinto formato, sem envelope e sem código:

```json
{ "message": "TOKEN EXPIRADO OU INVALIDO" }
```

A mensagem engana: o token pode estar válido e recém-emitido, faltando apenas o scope. Se ela aparecer logo após trocar de rota, confira os scopes pedidos na autenticação antes de investigar cache ou renovação de token.

**Cartão recusado no checkout transparente** (402):

```json
{ "success": false, "chargeId": "cha_...", "status": "failed", "error": "Cartão recusado" }
```

**Propostas de subconta** — recusas vêm sem envelope; na mesma rota, validações usam o envelope:

```json
{ "message": "Proposta rejeitada", "code": "FORM_NOT_FOUND" }
```

### Recusa de cartão em assinaturas

Vem como **400**, não 402, com o motivo em `details.declinedCode`. Valores possíveis: `insufficient_funds`, `card_declined`, `expired_card`, `invalid_cvv`, `fraud_suspected`.

### Erros 404 comuns

`ACCOUNT_NOT_FOUND` — a conta não foi resolvida a partir das credenciais. Aparece em quase toda rota autenticada.

`NOT_FOUND` — código genérico, usado quando a exceção é lançada sem código próprio. Um 404 na primeira chamada de uma credencial recém-criada costuma vir assim. **Trate os dois**, e sempre com um fallback para código desconhecido.

## Primeiro fluxo ponta a ponta

1. Obtenha o token com o scope `pix.cob/write`.
2. `POST /v1/charges/pix` com `{ "amount": 10.00 }`.
3. A resposta traz `emv` (copia e cola) e `qrCode` (data URL PNG pronta) **na raiz**. Não é preciso gerar a imagem do QR Code.
4. Em sandbox, dispare o pagamento com `POST /v1/wallet/pay/:chargeId` — a resposta é `PROCESSING`; a confirmação chega pelo webhook. Só funciona em conta sandbox.
5. Aguarde o webhook `payment.success` ou consulte `GET /v1/charges/:chargeId`.

Envie `metadata` na criação para correlacionar o pagamento com o pedido: **nesta rota** ele é persistido e volta na resposta e no webhook. Em `POST /v1/charges` o `metadata` é descartado — ali use `externalId`.

### Duas rotas, duas respostas

| Rota | Resposta |
|---|---|
| `POST /v1/charges/pix` | `emv` e `qrCode` na **raiz** |
| `POST /v1/charges` (checkout transparente) | aninhados sob **`pix`**: `pix.emv`, `pix.qrCode` |

Confira sempre o contrato da rota que você está usando; os dois formatos são válidos, cada um no seu endpoint.

## Próximos passos

- [Webhooks](./webhooks.md) — receber eventos em vez de fazer polling
- [Split de pagamento](./split-pagamento.md)
- [Tokenização de cartão](./tokenizacao.md)
- Contrato OpenAPI: `/openapi.json`
