# HubPix API

API REST para receber e pagar via **PIX** e sacar em **USDT (BSC)**, com webhooks assinados e gerenciáveis pela própria API. Tudo o que o painel faz com dinheiro e integração também pode ser feito por aqui, inclusive por agentes de IA.

**Começando:** crie a conta em [hubpixtech.com/cadastro](/cadastro), ative o autenticador (2FA), gere sua chave em **API e segurança** e cadastre o IP do seu servidor na whitelist de saque.

| | |
|---|---|
| **Base URL** | `https://hubpixtech.com/v1` |
| **Formato** | JSON. Sucesso: `{ "success": true, "data": ... }`. Erro: `{ "success": false, "error": { "code", "message" } }` |
| **Valores** | Sempre **string decimal com ponto**, em reais: `"100.00"` |
| **Datas** | ISO 8601 em UTC: `2026-10-02T12:00:00.000Z` |
| **OpenAPI** | [`/openapi.json`](/openapi.json) (OpenAPI 3.1), e esta documentação em Markdown em [`/llms.txt`](/llms.txt) |

## Autenticação

As chaves são geradas no painel, em **API e segurança → Nova chave** (pede o código do autenticador). Cada chave é um par: a pública e o segredo de saque. Elas são exibidas **uma única vez**: guarde em local seguro e nunca as coloque no frontend. Você pode ter até 5 chaves ativas (por exemplo, uma por sistema) e revogar qualquer uma a qualquer momento.

| Header | Credencial | Onde é exigida |
|---|---|---|
| `X-API-Key` | `hpx_pk_...` | Todas as requisições |
| `X-Secret-Key` | `hpx_sk_...` | Saques e ações sensíveis (`POST /v1/pix/withdrawals`, `POST /v1/usdt/withdrawals`, `PUT /v1/ip-allowlist`, `DELETE /v1/api-keys/{id}`) |

O `X-Secret-Key` precisa ser o do **mesmo par** da `X-API-Key` usada na requisição.

## Whitelist de IP

São duas listas, ambas editáveis no painel (com 2FA). Cada lista aceita até 20 entradas, com IP único (`203.0.113.10`) ou faixa CIDR (`203.0.113.0/24`, até `/16` no IPv4 e `/48` no IPv6).

| Lista | Efeito |
|---|---|
| **Saque** | **Obrigatória para sacar pela API.** Só esses IPs podem chamar os endpoints que exigem `X-Secret-Key`. Sem nenhum IP cadastrado, o saque pela API é recusado com `403 IP_NOT_ALLOWED`, e a mensagem traz o IP que chegou até nós |
| **API** | Opcional. Se tiver algum IP, **todas** as chamadas da API (inclusive consultas) passam a ser aceitas só desses IPs. Vazia: qualquer IP |

A primeira lista de saque é cadastrada **pelo painel**. Depois disso você também pode trocá-la pela API (`PUT /v1/ip-allowlist`), mas só a partir de um IP que já esteja nela. Assim, uma chave vazada não consegue liberar o IP de quem a roubou. Para descobrir o IP que o seu servidor usa para chegar até nós, chame `GET /v1/account` dele e veja `requestIp`.

## Idempotência

Envie o header `Idempotency-Key` (um UUID gerado por você, de 8 a 100 caracteres) em todo `POST` que cria cobrança ou saque. Se a rede cair e você repetir a chamada com a mesma chave, **a operação não é duplicada**: devolvemos a mesma transação, com HTTP `200` em vez de `201`.

```
Idempotency-Key: 9f3c2a1e-7b4d-4f1a-9c3e-2d5b8a7f6e01
```

O `externalRef` também é único por tipo de operação: repetir um `externalRef` já usado devolve `409 DUPLICATE`.

## Erros

| HTTP | `code` | Quando |
|---|---|---|
| 400 | `INVALID_JSON` / `BAD_REQUEST` | Corpo mal formado |
| 401 | `INVALID_KEY` | `X-API-Key` ausente, inválida ou revogada |
| 401 | `INVALID_SECRET` | `X-Secret-Key` ausente ou inválida em um saque |
| 403 | `ACCOUNT_BLOCKED` | Conta bloqueada |
| 403 | `ACCOUNT_PENDING` | Conta aguardando aprovação |
| 403 | `SCOPE_DENIED` | Operação não habilitada para sua conta |
| 403 | `IP_NOT_ALLOWED` | IP fora da whitelist (de saque ou da API) |
| 404 | `NOT_FOUND` | Recurso inexistente ou de outra conta |
| 409 | `DUPLICATE` | `externalRef` ou `Idempotency-Key` já usados |
| 422 | `VALIDATION_ERROR` | Campo inválido (detalhe em `message`) |
| 422 | `AMOUNT_OUT_OF_RANGE` | Valor fora dos limites da sua conta |
| 422 | `INSUFFICIENT_BALANCE` | Saldo insuficiente para o saque |
| 429 | `RATE_LIMITED` | Limite de requisições excedido |
| 502 | `CHARGE_UNAVAILABLE` | Não foi possível gerar a cobrança agora; tente de novo |
| 503 | `QUOTE_UNAVAILABLE` | Cotação de USDT indisponível no momento |

## Limites de requisição

Até **600 requisições por minuto por conta**. Acima disso a resposta é `429 RATE_LIMITED`. Para acompanhar o status de uma transação, **use webhooks**. Se precisar consultar, faça no máximo 1 consulta por minuto por transação.

## Conta e saldo

### `GET /v1/account`

Dados da conta: `status`, saldo, **suas taxas** (`fees`), limites por operação, recursos habilitados e `requestIp` (o IP de onde a chamada chegou).

```json
{
  "success": true,
  "data": {
    "id": "c802502e-...", "name": "Minha Loja", "email": "voce@empresa.com", "status": "ACTIVE",
    "balance": "1520.40", "heldBalance": "250.00",
    "fees": { "pixIn": { "percent": "7", "fixed": "0.00", "minimum": "0.00" }, "pixOut": { "percent": "0", "fixed": "0.00" }, "usdt": { "spreadPercent": "0" } },
    "limits": { "pixInMin": "10.00", "pixInMax": null, "pixOutMin": "1.00", "pixOutMax": null },
    "features": { "pixIn": true, "pixOut": true, "usdtOut": true, "usdtNetworks": ["BEP20"] },
    "requestIp": "203.0.113.10"
  }
}
```

A taxa do PIX In é `max(mínimo, valor × percent% + fixo)`. A taxa do saque PIX é `valor × percent% + fixo`, somada por fora.

### `GET /v1/balance`

```bash
curl https://hubpixtech.com/v1/balance -H "X-API-Key: hpx_pk_SUA_CHAVE"
```

```json
{ "success": true, "data": { "balance": "1520.40", "heldBalance": "250.00" } }
```

`balance` é o que está disponível para saque. `heldBalance` é o que está reservado em saques ainda em andamento.

## PIX In (cobranças)

### `POST /v1/pix/charges`

Cria uma cobrança PIX e devolve o **copia e cola** (`qrCode`), que você mostra ao pagador como texto ou QR Code.

| Campo | | Descrição |
|---|---|---|
| `amount` | obrigatório | Valor bruto, string: `"100.00"` |
| `externalRef` | opcional | Seu identificador (id do pedido), até 100 caracteres. Volta nos webhooks e permite consulta |
| `description` | opcional | Até 140 caracteres |
| `expiresIn` | opcional | Segundos até expirar, de 60 a 1800. Padrão: 1800 (30 min) |

```bash
curl -X POST https://hubpixtech.com/v1/pix/charges \
  -H "X-API-Key: hpx_pk_SUA_CHAVE" \
  -H "Idempotency-Key: 9f3c2a1e-7b4d-4f1a-9c3e-2d5b8a7f6e01" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "100.00", "externalRef": "pedido-1042", "description": "Pedido #1042" }'
```

Resposta `201`:

```json
{
  "success": true,
  "data": {
    "id": "6f1d2c3b-9a8e-4f7d-b6c5-1a2b3c4d5e6f",
    "type": "PIX_IN",
    "status": "PENDING",
    "amount": "100.00",
    "fee": "7.00",
    "netAmount": "93.00",
    "externalRef": "pedido-1042",
    "description": "Pedido #1042",
    "qrCode": "00020126580014br.gov.bcb.pix...",
    "expiresAt": "2026-10-02T12:30:00.000Z",
    "paidAt": null,
    "endToEndId": null,
    "payer": null,
    "createdAt": "2026-10-02T12:00:00.000Z",
    "updatedAt": "2026-10-02T12:00:00.000Z"
  }
}
```

`netAmount` é o que entra no seu saldo depois da taxa.

### `GET /v1/pix/charges/{id}`

Consulta uma cobrança pelo `id`. Para consultar pelo seu identificador, use `GET /v1/pix/charges?externalRef=pedido-1042`.

**Status:** `PENDING` → `PAID` | `EXPIRED` | `CANCELLED`. Uma cobrança que não pôde ser gerada fica `FAILED`.

Quando `PAID`, a resposta traz `paidAt`, `endToEndId` e `payer` (`name`, `document`, `institution`, quando disponíveis).

> O pagamento só é marcado como `PAID`, e o saldo só é creditado, **depois de confirmado na liquidação**. Por isso o status muda alguns segundos depois do pagamento, nunca antes.

### `GET /v1/pix/charges`

Lista paginada. Query: `status`, `from`, `to` (ISO 8601), `page` (padrão 1) e `limit` (padrão 50, máximo 100).

```json
{ "success": true, "data": { "items": [ ... ], "page": 1, "limit": 50, "total": 132 } }
```

## PIX Out (saques)

### `POST /v1/pix/withdrawals`

Envia um PIX a partir do seu saldo. Exige `X-API-Key`, `X-Secret-Key` e o IP na [whitelist de saque](#whitelist-de-ip).

`amount` é o valor que **chega ao recebedor**. A taxa é somada por fora, e o total (`totalAmount`) sai do seu saldo no momento do pedido.

| Campo | | Descrição |
|---|---|---|
| `amount` | obrigatório | Valor que o recebedor recebe, string |
| `pixKey` + `pixKeyType` | opção A | `pixKeyType`: `CPF`, `CNPJ`, `EMAIL`, `PHONE` ou `EVP` (chave aleatória) |
| `brCode` | opção B | Código PIX copia e cola do recebedor |
| `externalRef` | opcional | Seu identificador. Volta nos webhooks |
| `description` | opcional | Até 140 caracteres |

```bash
curl -X POST https://hubpixtech.com/v1/pix/withdrawals \
  -H "X-API-Key: hpx_pk_SUA_CHAVE" \
  -H "X-Secret-Key: hpx_sk_SEU_SECRET" \
  -H "Idempotency-Key: 41d1f2ab-90c7-4e2b-b7d3-0f6a8c9d1e22" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "250.00", "pixKey": "12345678900", "pixKeyType": "CPF", "externalRef": "repasse-778" }'
```

Resposta `201`:

```json
{
  "success": true,
  "data": {
    "id": "0b7e6f2a-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
    "type": "PIX_OUT",
    "status": "PROCESSING",
    "amount": "250.00",
    "fee": "3.00",
    "totalAmount": "253.00",
    "pixKey": "12345678900",
    "pixKeyType": "CPF",
    "externalRef": "repasse-778",
    "endToEndId": null,
    "failureReason": null,
    "createdAt": "2026-10-02T12:05:00.000Z"
  }
}
```

**Formato da chave:** CPF e CNPJ só com dígitos (pontuação é removida). Telefone com DDD; aceitamos `11999998888`, `5511999998888` ou `+5511999998888`.

### Status do saque

| Status | Significado | Saldo |
|---|---|---|
| `PROCESSING` | Pagamento enviado, aguardando liquidação | `totalAmount` reservado |
| `COMPLETED` | Pago. `endToEndId` é o comprovante | debitado |
| `FAILED` | Não foi pago. Motivo em `failureReason` | **devolvido** ao saldo |
| `UNDER_REVIEW` | Resultado não confirmado; em análise manual pela nossa equipe | continua reservado |

Cada saque é **enviado uma única vez**. Nunca reenviamos um pagamento automaticamente. Quando o resultado não pode ser confirmado com segurança, o saque vai para `UNDER_REVIEW` e o valor fica reservado até a nossa equipe concluir a análise. Ao final, ele vira `COMPLETED` ou `FAILED`, e você recebe o webhook correspondente. **Não crie outro saque para o mesmo pagamento enquanto ele estiver em análise.**

### `GET /v1/pix/withdrawals/{id}`

Consulta pelo `id`, ou por `GET /v1/pix/withdrawals?externalRef=...`. `GET /v1/pix/withdrawals` lista os saques, com os mesmos filtros da listagem de cobranças.

## Saque em USDT

Saque do seu saldo em reais convertido para **USDT**, enviado para uma carteira sua na **BNB Smart Chain (BEP20)**. É a única rede aceita: o campo `network` pode ser omitido ou enviado como `"BEP20"`.

### `GET /v1/usdt/quote`

Simula o saque. Informe `network` e **um** destes: `amountUsdt` (quanto você quer receber) ou `amountBrl` (quanto quer gastar, já com a taxa de rede).

```bash
curl "https://hubpixtech.com/v1/usdt/quote?amountUsdt=100" -H "X-API-Key: hpx_pk_SUA_CHAVE"
```

```json
{
  "success": true,
  "data": {
    "network": "BEP20",
    "rate": "5.4210",
    "amountUsdt": "100.00",
    "networkFeeUsdt": "1.00",
    "amountBrl": "542.10",
    "feeBrl": "5.43",
    "totalBrl": "547.53",
    "validForSeconds": 30,
    "networks": ["BEP20"]
  }
}
```

A cotação é indicativa. O valor final é calculado no momento do `POST`.

### `POST /v1/usdt/withdrawals`

Exige `X-API-Key`, `X-Secret-Key` e o IP na whitelist de saque.

| Campo | | Descrição |
|---|---|---|
| `network` | opcional | `BEP20` (única rede; é o padrão) |
| `address` | obrigatório | Endereço `0x...` (40 hex) da sua carteira na BNB Smart Chain |
| `amountUsdt` ou `amountBrl` | obrigatório | Um dos dois, como na cotação |
| `externalRef`, `description` | opcional | Como nos outros saques |

```bash
curl -X POST https://hubpixtech.com/v1/usdt/withdrawals \
  -H "X-API-Key: hpx_pk_SUA_CHAVE" -H "X-Secret-Key: hpx_sk_SEU_SECRET" \
  -H "Idempotency-Key: 7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" \
  -H "Content-Type: application/json" \
  -d '{ "address": "0x9f8e7d6c5b4a39281706f5e4d3c2b1a098765432", "amountUsdt": "100.00", "externalRef": "cripto-55" }'
```

O total em reais (`totalAmount`) é debitado na hora e o pedido nasce com status `REQUESTED`. Depois do envio na blockchain, ele vira `COMPLETED` com `usdt.txHash` (consulte em `https://bscscan.com/tx/<txHash>`). Se não puder ser enviado, vira `FAILED` e o valor volta ao seu saldo. **Confira o endereço e a rede:** USDT enviado para endereço ou rede errados não tem como ser recuperado.

`GET /v1/usdt/withdrawals/{id}` consulta o pedido. `GET /v1/usdt/withdrawals` lista os pedidos.

## Extrato

### `GET /v1/transactions`

Todas as suas transações. Filtros: `type` (`PIX_IN`, `PIX_OUT`, `USDT_OUT`), `status`, `externalRef`, `from`, `to`, `page` e `limit`. `GET /v1/transactions/{id}` consulta uma transação de qualquer tipo.

## Webhooks

Você cadastra, edita e remove as URLs que recebem os eventos **pela própria API** (ou pelo painel). Pode ter até 10 URLs, cada uma com sua lista de eventos e seu próprio segredo de assinatura. `GET /v1/webhooks/events` lista os eventos disponíveis.

### Eventos

| Evento | Quando dispara |
|---|---|
| `pix_in.paid` | Cobrança paga e confirmada. O valor líquido já está no seu saldo |
| `pix_in.expired` | Cobrança expirou ou foi cancelada sem pagamento |
| `pix_out.completed` | Saque PIX pago (com `endToEndId`) |
| `pix_out.failed` | Saque PIX não foi pago. O valor voltou ao seu saldo |
| `pix_out.under_review` | Saque PIX entrou em análise manual. O valor segue reservado |
| `usdt_out.completed` | Saque USDT enviado (com `usdt.txHash`) |
| `usdt_out.failed` | Saque USDT cancelado. O valor voltou ao seu saldo |
| `webhook.test` | Disparo de teste feito por você |

Use `"*"` para assinar todos os eventos.

### `POST /v1/webhooks`: cadastrar

| Campo | | Descrição |
|---|---|---|
| `url` | obrigatório | URL **HTTPS** pública |
| `events` | opcional | Lista de eventos. Padrão: `["*"]` |
| `description` | opcional | Rótulo livre |
| `active` | opcional | Padrão `true` |

```bash
curl -X POST https://hubpixtech.com/v1/webhooks \
  -H "X-API-Key: hpx_pk_SUA_CHAVE" -H "Content-Type: application/json" \
  -d '{ "url": "https://seusite.com.br/webhooks/hubpix", "events": ["pix_in.paid", "pix_out.completed", "pix_out.failed"] }'
```

```json
{
  "success": true,
  "data": {
    "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
    "url": "https://seusite.com.br/webhooks/hubpix",
    "events": ["pix_in.paid", "pix_out.completed", "pix_out.failed"],
    "description": null,
    "active": true,
    "secret": "whsec_3f9a...",
    "createdAt": "2026-10-02T12:00:00.000Z"
  }
}
```

O `secret` aparece **só nesta resposta** e na rotação. Guarde-o para validar a assinatura.

### Demais operações

| Método e rota | O que faz |
|---|---|
| `GET /v1/webhooks` | Lista suas URLs |
| `GET /v1/webhooks/{id}` | Detalhe de uma URL |
| `PATCH /v1/webhooks/{id}` | Altera `url`, `events`, `description` ou `active` (envie só o que muda) |
| `DELETE /v1/webhooks/{id}` | Remove a URL. Entregas pendentes para ela são canceladas |
| `POST /v1/webhooks/{id}/rotate-secret` | Gera um segredo novo. O anterior para de valer na hora |
| `POST /v1/webhooks/{id}/test` | Envia um `webhook.test` agora e devolve o resultado da entrega |
| `GET /v1/webhooks/{id}/deliveries` | Últimas entregas: status, tentativas, último HTTP e erro |
| `POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver` | Reenvia uma entrega |

### Formato da entrega

```
POST https://seusite.com.br/webhooks/hubpix
Content-Type: application/json
X-HubPix-Event: pix_in.paid
X-HubPix-Delivery: evt_5c1a9e0b7f3d2a4e6b8c0d1f
X-HubPix-Signature: t=1791000000,v1=6a1c9d...
```

```json
{
  "id": "evt_5c1a9e0b7f3d2a4e6b8c0d1f",
  "event": "pix_in.paid",
  "createdAt": "2026-10-02T12:03:12.000Z",
  "data": {
    "transaction": {
      "id": "6f1d2c3b-9a8e-4f7d-b6c5-1a2b3c4d5e6f",
      "type": "PIX_IN",
      "status": "PAID",
      "amount": "100.00",
      "fee": "7.00",
      "netAmount": "93.00",
      "externalRef": "pedido-1042",
      "paidAt": "2026-10-02T12:03:10.000Z",
      "endToEndId": "E1234567820261002...",
      "payer": { "name": "Maria Souza", "document": "12345678900", "institution": "BANCO X" }
    }
  }
}
```

`data.transaction` tem o mesmo formato da consulta da transação. Responda com qualquer **2xx em até 10 segundos**. Processe de forma assíncrona se precisar de mais tempo.

O mesmo evento pode chegar mais de uma vez (reentrega). Use `X-HubPix-Delivery` (igual ao `id` do corpo) para processar cada evento **uma vez só**.

### Verificar a assinatura

O header `X-HubPix-Signature` tem o formato `t=<unix>,v1=<hex>`, onde:

```
v1 = HMAC_SHA256(secret, t + "." + corpoBruto)
```

Rejeite entregas com assinatura inválida ou com `t` a mais de 5 minutos do seu relógio.

```js
const crypto = require('node:crypto');
function verificaHubPix(rawBody, header, secret) {
  const t  = (header.match(/t=(\d+)/) || [])[1];
  const v1 = (header.match(/v1=([a-f0-9]+)/) || [])[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const esperado = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
}
```

```php
function verificaHubPix(string $rawBody, string $header, string $secret): bool {
    preg_match('/t=(\d+)/', $header, $t); preg_match('/v1=([a-f0-9]+)/', $header, $v);
    if (!$t || !$v || abs(time() - (int)$t[1]) > 300) return false;
    return hash_equals(hash_hmac('sha256', $t[1] . '.' . $rawBody, $secret), $v[1]);
}
```

Use o **corpo bruto** da requisição, exatamente como chegou, e não o JSON já convertido.

### Reentregas

Se a sua URL não responder 2xx em 10 segundos, reenviamos com intervalos crescentes: **1 min, 5 min, 15 min, 1 h, 6 h e 24 h** (7 tentativas no total). Depois disso a entrega fica como `FAILED` e você pode reenviá-la por `POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver`.

Mesmo sem webhook, você sempre pode conferir pela consulta da transação ou pelo extrato.

## Chaves e whitelist pela API

| Método e rota | Credencial | O que faz |
|---|---|---|
| `GET /v1/api-keys` | `X-API-Key` | Lista suas chaves (nome, final, último uso e IP). `current: true` marca a chave usada na chamada |
| `DELETE /v1/api-keys/{id}` | + `X-Secret-Key`, IP da whitelist de saque | Revoga uma chave na hora |
| `GET /v1/ip-allowlist` | `X-API-Key` | Mostra as duas listas e o `requestIp` |
| `PUT /v1/ip-allowlist` | + `X-Secret-Key`, IP da whitelist de saque | Substitui `api` e/ou `withdraw`. A nova lista de saque precisa continuar contendo o IP da requisição |

**Criar** chave nova só é possível pelo painel, porque exige o código do autenticador.

```bash
curl -X PUT https://hubpixtech.com/v1/ip-allowlist \
  -H "X-API-Key: hpx_pk_SUA_CHAVE" -H "X-Secret-Key: hpx_sk_SEU_SECRET" -H "Content-Type: application/json" \
  -d '{ "withdraw": ["203.0.113.10", "198.51.100.0/24"], "api": [] }'
```

## Integração com IA e automação

A API foi feita para ser operada por código e por agentes de IA, do mesmo jeito que pelo painel:

- **[`/openapi.json`](/openapi.json)**: especificação OpenAPI 3.1 com todos os endpoints, campos e erros. Importe no seu agente, no Postman ou num gerador de SDK.
- **[`/llms.txt`](/llms.txt)**: esta documentação em Markdown puro, para colocar no contexto do modelo.
- Respostas sempre no mesmo envelope (`success`, `data` ou `error.code`) e códigos de erro estáveis, fáceis de tratar automaticamente.
- `Idempotency-Key` torna seguro repetir chamadas: um agente que repete um `POST` não duplica cobrança nem saque.

**Recomendação de segurança para agentes:** dê ao agente uma chave própria (crie uma só para ele) e rode-o num servidor cujo IP esteja na whitelist. Se o agente não deve sacar, não entregue o `X-Secret-Key` a ele: só com a `X-API-Key` ele cria cobranças, consulta e gerencia webhooks, mas não move dinheiro para fora.

## Boas práticas

- Confirme pedidos pelo webhook `pix_in.paid`. Na dúvida, consulte `GET /v1/pix/charges/{id}`, sem polling agressivo.
- Gere uma `Idempotency-Key` por operação e **reutilize a mesma** ao repetir uma chamada que falhou por rede.
- Trate `UNDER_REVIEW` como "aguardando". O dinheiro não sumiu: está reservado até a análise terminar.
- Guarde `X-Secret-Key` só no servidor que faz saques e mantenha na whitelist de saque apenas os IPs dele.
- Use uma chave por sistema. Se uma vazar, revogue só ela.
