# API de PDV — Moeda Nobre (documentação completa)

---



---

Endereço base: https://moedanobre.com/api/v1

---



---

# Comece aqui

A API de PDV da Moeda Nobre liga o **sistema de caixa** de uma loja credenciada ao **app do cliente**. O caixa cria a cobrança com o valor da venda, mostra o QR Code, o cliente paga com o saldo do benefício pelo app, e o seu sistema fica sabendo na hora.

Você não mexe com dinheiro, cartão nem dado pessoal: a Moeda Nobre confirma o pagamento e o valor entra na loja pelo caminho de sempre.

## Como uma venda acontece

1. O operador fecha a venda no caixa.
2. O seu servidor chama `POST /cobrancas` com o valor e recebe a cobrança com o QR Code.
3. O caixa mostra o QR (na tela ou no cupom).
4. O cliente lê o QR com o app da Moeda Nobre e confirma com o PIN.
5. O seu sistema descobre que foi paga — do jeito recomendado, segurando uma consulta aberta (`GET /cobrancas/{id}?aguardar=25`), ou recebendo o webhook `cobranca.paga`.
6. O caixa imprime o comprovante e segue.

## Dois ambientes, o mesmo contrato

| | Testes | Produção |
|---|---|---|
| Chave | `mn_test_…` | `mn_live_…` |
| Lojas e clientes | fictícios, criados por você no portal | lojas reais credenciadas |
| Dinheiro | nenhum | real |
| Liberação | na hora, ao entrar no portal | pela TribeX, depois da homologação |

É a **chave** que decide o ambiente — a URL é a mesma. Um QR de teste nunca é pago pelo app real, e uma chave de teste nunca enxerga uma loja real.

## Endereço

```
https://moedanobre.com/api/v1
```

Todas as rotas deste guia são relativas a esse endereço (ex.: `https://moedanobre.com/api/v1/ping`).

## Autenticação

Toda chamada leva a chave no cabeçalho:

```
Authorization: Bearer mn_test_a8Kq2x9LmP0Z_…
```

- A chave fica **no seu servidor**. Nunca no navegador, no aplicativo do caixa distribuído para as lojas ou em código-fonte público. Uma chamada vinda de navegador é recusada (`403 CHAVE_EM_NAVEGADOR`) e avisa a TribeX.
- A chave completa aparece **uma única vez**, quando você a cria no portal. Guarde-a num cofre de segredos.
- Mantenha duas chaves ativas para trocar sem parar o caixa: crie a nova, publique no servidor, revogue a antiga.

## Formato

- JSON em UTF-8, nomes em português, `camelCase`.
- Dinheiro sempre em **centavos inteiros** (`valorCentavos: 4590` = R$ 45,90).
- Datas em ISO-8601 com o fuso de Brasília (`2026-09-25T14:03:11-03:00`).
- Campos novos podem aparecer nas respostas sem aviso: **ignore o que você não conhece**. Campos desconhecidos no que você **envia** são recusados (`400 CAMPO_DESCONHECIDO`).

## Cabeçalhos de toda resposta

| Cabeçalho | Para quê |
|---|---|
| `Moeda-Nobre-Request-Id` | Identifica a chamada. Guarde nos seus logs e mande ao suporte quando algo der errado. |
| `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` | Seu limite por minuto e quanto falta. |
| `Idempotent-Replayed: true` | A resposta é a repetição de um `POST /cobrancas` que já tinha dado certo. |

## Próximo passo

Siga o [primeiro teste em 10 minutos](/desenvolvedores/primeiro-teste). Tudo o que ele usa você cria sozinho no portal, sem falar com ninguém.


---

# Primeiro teste em 10 minutos

Tudo aqui acontece no **ambiente de testes**: loja e cliente fictícios, nenhum dinheiro real.

## 1. Crie uma chave de teste

No portal, abra **Aplicativos → (seu aplicativo) → Chaves de API** com o seletor em **Testes** e clique em **Criar chave**. Confirme com o código enviado ao seu e-mail e copie a chave `mn_test_…` — ela não aparece de novo.

```bash
export MN_CHAVE="mn_test_…"
export MN_API="https://moedanobre.com/api/v1"
```

## 2. Confira a chave

```bash
curl -s "$MN_API/ping" -H "Authorization: Bearer $MN_CHAVE"
```

```json
{ "ok": true, "ambiente": "TESTE", "aplicativo": { "id": "…", "nome": "Meu sistema de PDV" }, "servidorEm": "2026-09-25T14:03:11-03:00" }
```

## 3. Gere dados de exemplo

No portal, em **Dados de teste**, clique em **Gerar dados de exemplo**. Você recebe uma loja de teste, dois clientes de teste (R$ 500,00 cada) e um **código de ativação** `T-XXXX-XXXX`, mostrado uma única vez.

Na vida real, quem gera o código é o gerente da loja, no painel dele, e o técnico digita no caixa.

## 4. Ligue um caixa

```bash
curl -s -X POST "$MN_API/pontos-de-venda/ativar" \
  -H "Authorization: Bearer $MN_CHAVE" -H "Content-Type: application/json" \
  -d '{ "codigoAtivacao": "T-ABCD-EFGH", "identificadorExterno": "CAIXA-01", "nome": "Caixa da entrada" }'
```

A resposta é o caixa (`status: "ATIVO"`). Guarde o `id`: é o `pontoDeVendaId` das cobranças.

## 5. Crie uma cobrança

```bash
curl -s -X POST "$MN_API/cobrancas?semImagem=true" \
  -H "Authorization: Bearer $MN_CHAVE" -H "Content-Type: application/json" \
  -H "Idempotency-Key: cupom-000123" \
  -d '{ "pontoDeVendaId": "<id do caixa>", "valorCentavos": 4590, "referenciaExterna": "CUPOM-000123" }'
```

A resposta traz `qrCode.conteudo`: é o texto que vira o QR Code no caixa (sem `?semImagem=true`, vem também `qrCode.imagemPng` pronto para exibir).

## 6. Pague pelo celular de teste

No portal, abra **Dados de teste → Celular de teste**, aponte a câmera para o QR (ou cole o conteúdo), escolha um cliente de teste e use o PIN **1234**.

## 7. Descubra que foi paga

Segure a consulta aberta por até 25 segundos — ela volta assim que o status muda:

```bash
curl -s "$MN_API/cobrancas/<id da cobrança>?aguardar=25&semImagem=true" -H "Authorization: Bearer $MN_CHAVE"
```

```json
{ "id": "…", "status": "PAGA", "valorCentavos": 4590, "pagaEm": "2026-09-25T14:04:02-03:00", … }
```

Pronto: esse é o fluxo inteiro. Depois, veja [webhooks](/desenvolvedores/webhooks) (opcional), os [cenários automáticos de teste](/desenvolvedores/ambiente-de-testes) e as [boas práticas](/desenvolvedores/boas-praticas) — a tela **Homologação** do portal acompanha o que já foi provado.


---

# Referência da API

Endereço base: `https://moedanobre.com/api/v1` · Autenticação: `Authorization: Bearer <chave>` · [OpenAPI 3.1 (JSON)](/desenvolvedores/openapi.json)

## GET /ping

A chave funciona?

| HTTP | Resposta |
|---|---|
| 200 | `Ping` — OK |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## POST /pontos-de-venda/ativar

Ligar um caixa com o código de ativação

Produção: código XXXX-XXXX gerado pelo gerente da loja. Testes: código T-XXXX-XXXX gerado no portal. Uso único; 5 códigos errados por chave em 1 hora bloqueiam por 1 hora.

Corpo:

```json
{
  "type": "object",
  "properties": {
    "codigoAtivacao": {
      "type": "string",
      "minLength": 1,
      "maxLength": 40
    },
    "identificadorExterno": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{1,64}$"
    },
    "nome": {
      "type": "string",
      "minLength": 1,
      "maxLength": 60
    }
  },
  "required": [
    "codigoAtivacao",
    "identificadorExterno"
  ],
  "additionalProperties": false
}
```

| HTTP | Resposta |
|---|---|
| 201 | `PontoDeVenda` — Caixa ativo |
| 400 | `Erro` — VALIDACAO · CAMPO_DESCONHECIDO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 409 | `Erro` — PDV_JA_ATIVO |
| 410 | `Erro` — CODIGO_ATIVACAO_EXPIRADO |
| 422 | `Erro` — CODIGO_ATIVACAO_INVALIDO · LOJA_INATIVA |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## GET /pontos-de-venda

Listar os caixas

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `status` | query | não |  |
| `cursor` | query | não |  |
| `limite` | query | não |  |

| HTTP | Resposta |
|---|---|
| 200 | `PaginaPontosDeVenda` — Página |
| 400 | `Erro` — VALIDACAO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## GET /pontos-de-venda/{id}

Um caixa

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `id` | caminho | sim | Id do caixa |

| HTTP | Resposta |
|---|---|
| 200 | `PontoDeVenda` — OK |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — PDV_NAO_ENCONTRADO |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## POST /pontos-de-venda/{id}/desativar

Desligar um caixa (cancela a pendente dele)

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `id` | caminho | sim | Id do caixa |

| HTTP | Resposta |
|---|---|
| 200 | `PontoDeVenda` — Caixa desligado |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — PDV_NAO_ENCONTRADO |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## POST /cobrancas

Criar uma cobrança

Uma cobrança pendente por caixa. Com cancelarPendenteAnterior=true a anterior é cancelada (se o cliente não tiver acabado de pagá-la).

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `Idempotency-Key` | cabeçalho | sim | Único por cobrança que você quer criar (ex.: o id do cupom). Repetir com o mesmo corpo devolve a mesma cobrança. |
| `Moeda-Nobre-Teste-Cenario` | cabeçalho | não | Só com chave de teste. |

Corpo:

```json
{
  "type": "object",
  "properties": {
    "pontoDeVendaId": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "valorCentavos": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9999999
    },
    "referenciaExterna": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "expiraEmSegundos": {
      "default": 300,
      "type": "integer",
      "minimum": 60,
      "maximum": 900
    },
    "metadados": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "minLength": 1,
        "maxLength": 40
      },
      "additionalProperties": {
        "type": "string",
        "maxLength": 100
      }
    },
    "cancelarPendenteAnterior": {
      "default": false,
      "type": "boolean"
    }
  },
  "required": [
    "pontoDeVendaId",
    "valorCentavos"
  ],
  "additionalProperties": false
}
```

| HTTP | Resposta |
|---|---|
| 200 | `Cobranca` — Repetição com a mesma Idempotency-Key (cabeçalho Idempotent-Replayed: true) |
| 201 | `Cobranca` — Cobrança criada (com o QR) |
| 400 | `Erro` — VALIDACAO · CAMPO_DESCONHECIDO · IDEMPOTENCY_KEY_OBRIGATORIA · CENARIO_SO_EM_TESTE · CENARIO_INVALIDO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — PDV_NAO_ENCONTRADO |
| 409 | `Erro` — COBRANCA_PENDENTE_EXISTE · COBRANCA_NAO_PENDENTE · REQUISICAO_EM_ANDAMENTO |
| 422 | `Erro` — IDEMPOTENCY_CONFLITO · PDV_INATIVO · LOJA_INATIVA · VINCULO_REVOGADO |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## GET /cobrancas

Listar cobranças

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `pontoDeVendaId` | query | não |  |
| `status` | query | não |  |
| `criadoDe` | query | não |  |
| `criadoAte` | query | não |  |
| `referenciaExterna` | query | não |  |
| `cursor` | query | não |  |
| `limite` | query | não |  |

| HTTP | Resposta |
|---|---|
| 200 | `PaginaCobrancas` — Página |
| 400 | `Erro` — VALIDACAO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## GET /cobrancas/{id}

Uma cobrança — com ?aguardar=N espera até N segundos por uma mudança (long-poll)

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `id` | caminho | sim | Id da cobrança |
| `aguardar` | query | não |  |
| `semImagem` | query | não |  |

| HTTP | Resposta |
|---|---|
| 200 | `Cobranca` — OK |
| 400 | `Erro` — VALIDACAO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — COBRANCA_NAO_ENCONTRADA |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## POST /cobrancas/{id}/cancelar

Cancelar uma cobrança pendente

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `id` | caminho | sim | Id da cobrança |

| HTTP | Resposta |
|---|---|
| 200 | `Cobranca` — Cancelada |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — COBRANCA_NAO_ENCONTRADA |
| 409 | `Erro` — COBRANCA_NAO_PENDENTE |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## GET /eventos

Eventos dos últimos 30 dias, em ordem (conciliação / quem não recebe webhook)

| Parâmetro | Onde | Obrigatório | Descrição |
|---|---|---|---|
| `apos` | query | não |  |
| `limite` | query | não |  |

| HTTP | Resposta |
|---|---|
| 200 | `PaginaEventos` — Página |
| 400 | `Erro` — VALIDACAO |
| 401 | `Erro` — CHAVE_INVALIDA · CHAVE_REVOGADA · PARCEIRO_SUSPENSO · PRODUCAO_NAO_LIBERADA · APLICATIVO_DESATIVADO |
| 403 | `Erro` — CHAVE_EM_NAVEGADOR |
| 404 | `Erro` — RECURSO_NAO_ENCONTRADO |
| 429 | `Erro` — LIMITE_EXCEDIDO |
| 500 | `Erro` — ERRO_INTERNO |

## Objetos

### Cobranca

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "Id da cobrança (é o `tid` do QR)"
    },
    "status": {
      "type": "string",
      "enum": [
        "PENDENTE",
        "PAGA",
        "CANCELADA",
        "EXPIRADA",
        "ESTORNADA"
      ]
    },
    "valorCentavos": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "pontoDeVendaId": {
      "type": "string"
    },
    "lojaId": {
      "type": "string"
    },
    "referenciaExterna": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "metadados": {
      "anyOf": [
        {
          "type": "object",
          "propertyNames": {
            "type": "string"
          },
          "additionalProperties": {
            "type": "string"
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "qrCode": {
      "description": "Só enquanto PENDENTE",
      "type": "object",
      "properties": {
        "conteudo": {
          "type": "string",
          "description": "Texto do QR — desenhe com qualquer biblioteca de QR"
        },
        "imagemPng": {
          "description": "data:image/png;base64,… (omitida com ?semImagem=true)",
          "type": "string"
        }
      },
      "required": [
        "conteudo"
      ],
      "additionalProperties": false
    },
    "criadoEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "expiraEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "pagaEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "canceladaEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "expiradaEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "estornadaEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "ambiente": {
      "type": "string",
      "enum": [
        "TESTE",
        "PRODUCAO"
      ]
    }
  },
  "required": [
    "id",
    "status",
    "valorCentavos",
    "pontoDeVendaId",
    "lojaId",
    "referenciaExterna",
    "metadados",
    "criadoEm",
    "expiraEm",
    "pagaEm",
    "canceladaEm",
    "expiradaEm",
    "estornadaEm",
    "ambiente"
  ],
  "additionalProperties": false
}
```

### PontoDeVenda

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "identificadorExterno": {
      "type": "string"
    },
    "nome": {
      "type": "string"
    },
    "status": {
      "type": "string",
      "enum": [
        "ATIVO",
        "DESATIVADO"
      ]
    },
    "loja": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "nomeFantasia": {
          "type": "string"
        },
        "cidade": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "estado": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "nomeFantasia",
        "cidade",
        "estado"
      ],
      "additionalProperties": false
    },
    "ativadoEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "desativadoEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "ambiente": {
      "type": "string",
      "enum": [
        "TESTE",
        "PRODUCAO"
      ]
    }
  },
  "required": [
    "id",
    "identificadorExterno",
    "nome",
    "status",
    "loja",
    "ativadoEm",
    "desativadoEm",
    "ambiente"
  ],
  "additionalProperties": false
}
```

### Evento

```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "evt_… — use para não processar o mesmo evento duas vezes"
    },
    "tipo": {
      "type": "string",
      "enum": [
        "cobranca.paga",
        "cobranca.cancelada",
        "cobranca.expirada",
        "cobranca.estornada",
        "ponto_de_venda.desativado",
        "ponto_de_venda.reativado",
        "ping"
      ]
    },
    "criadoEm": {
      "anyOf": [
        {
          "type": "string",
          "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
        },
        {
          "type": "null"
        }
      ]
    },
    "ambiente": {
      "type": "string",
      "enum": [
        "TESTE",
        "PRODUCAO"
      ]
    },
    "dados": {
      "type": "object",
      "properties": {
        "cobranca": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "description": "Id da cobrança (é o `tid` do QR)"
            },
            "status": {
              "type": "string",
              "enum": [
                "PENDENTE",
                "PAGA",
                "CANCELADA",
                "EXPIRADA",
                "ESTORNADA"
              ]
            },
            "valorCentavos": {
              "type": "integer",
              "minimum": -9007199254740991,
              "maximum": 9007199254740991
            },
            "pontoDeVendaId": {
              "type": "string"
            },
            "lojaId": {
              "type": "string"
            },
            "referenciaExterna": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "metadados": {
              "anyOf": [
                {
                  "type": "object",
                  "propertyNames": {
                    "type": "string"
                  },
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ]
            },
            "qrCode": {
              "description": "Só enquanto PENDENTE",
              "type": "object",
              "properties": {
                "conteudo": {
                  "type": "string",
                  "description": "Texto do QR — desenhe com qualquer biblioteca de QR"
                },
                "imagemPng": {
                  "description": "data:image/png;base64,… (omitida com ?semImagem=true)",
                  "type": "string"
                }
              },
              "required": [
                "conteudo"
              ],
              "additionalProperties": false
            },
            "criadoEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "expiraEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "pagaEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "canceladaEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "expiradaEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "estornadaEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "ambiente": {
              "type": "string",
              "enum": [
                "TESTE",
                "PRODUCAO"
              ]
            }
          },
          "required": [
            "id",
            "status",
            "valorCentavos",
            "pontoDeVendaId",
            "lojaId",
            "referenciaExterna",
            "metadados",
            "criadoEm",
            "expiraEm",
            "pagaEm",
            "canceladaEm",
            "expiradaEm",
            "estornadaEm",
            "ambiente"
          ],
          "additionalProperties": false
        },
        "pontoDeVenda": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string"
            },
            "identificadorExterno": {
              "type": "string"
            },
            "nome": {
              "type": "string"
            },
            "status": {
              "type": "string",
              "enum": [
                "ATIVO",
                "DESATIVADO"
              ]
            },
            "loja": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "nomeFantasia": {
                  "type": "string"
                },
                "cidade": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "estado": {
                  "anyOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "id",
                "nomeFantasia",
                "cidade",
                "estado"
              ],
              "additionalProperties": false
            },
            "ativadoEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "desativadoEm": {
              "anyOf": [
                {
                  "type": "string",
                  "description": "Data ISO-8601 com fuso de Brasília, ex. 2026-09-25T14:03:11-03:00"
                },
                {
                  "type": "null"
                }
              ]
            },
            "ambiente": {
              "type": "string",
              "enum": [
                "TESTE",
                "PRODUCAO"
              ]
            }
          },
          "required": [
            "id",
            "identificadorExterno",
            "nome",
            "status",
            "loja",
            "ativadoEm",
            "desativadoEm",
            "ambiente"
          ],
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }
  },
  "required": [
    "id",
    "tipo",
    "criadoEm",
    "ambiente",
    "dados"
  ],
  "additionalProperties": false
}
```

### Erro

```json
{
  "type": "object",
  "properties": {
    "erro": {
      "type": "object",
      "properties": {
        "codigo": {
          "type": "string",
          "enum": [
            "VALIDACAO",
            "CAMPO_DESCONHECIDO",
            "IDEMPOTENCY_KEY_OBRIGATORIA",
            "CENARIO_SO_EM_TESTE",
            "CENARIO_INVALIDO",
            "CHAVE_INVALIDA",
            "CHAVE_REVOGADA",
            "PARCEIRO_SUSPENSO",
            "PRODUCAO_NAO_LIBERADA",
            "APLICATIVO_DESATIVADO",
            "CHAVE_EM_NAVEGADOR",
            "PDV_NAO_ENCONTRADO",
            "COBRANCA_NAO_ENCONTRADA",
            "RECURSO_NAO_ENCONTRADO",
            "METODO_NAO_PERMITIDO",
            "COBRANCA_PENDENTE_EXISTE",
            "COBRANCA_NAO_PENDENTE",
            "REQUISICAO_EM_ANDAMENTO",
            "PDV_JA_ATIVO",
            "CODIGO_ATIVACAO_EXPIRADO",
            "CORPO_GRANDE",
            "CONTENT_TYPE",
            "IDEMPOTENCY_CONFLITO",
            "PDV_INATIVO",
            "LOJA_INATIVA",
            "VINCULO_REVOGADO",
            "CODIGO_ATIVACAO_INVALIDO",
            "LIMITE_EXCEDIDO",
            "ERRO_INTERNO",
            "INDISPONIVEL"
          ]
        },
        "mensagem": {
          "type": "string"
        },
        "detalhes": {
          "type": "object",
          "propertyNames": {
            "type": "string"
          },
          "additionalProperties": {}
        },
        "requestId": {
          "type": "string",
          "description": "Mande este id ao suporte"
        }
      },
      "required": [
        "codigo",
        "mensagem",
        "requestId"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "erro"
  ],
  "additionalProperties": false
}
```


---

# Webhooks

Webhook é **opcional**. O caixa fica atrás da internet da loja, então o jeito recomendado de saber se pagou é a consulta aberta (`GET /cobrancas/{id}?aguardar=25`). O webhook serve para o seu **servidor** saber de tudo o que acontece — inclusive o que não parte do caixa, como um estorno feito pela loja ou uma cobrança que expirou.

## Configurar

No portal: **Aplicativos → (seu aplicativo) → Webhooks → Adicionar endereço**.

- Só `https://`, na porta 443 ou 8443, com um nome de domínio (não um IP). Endereços de rede interna são recusados.
- Até 3 endereços por aplicativo e ambiente.
- Ao salvar, mandamos um evento `ping` na hora. **Só salva se o seu servidor responder 2xx.**
- O **segredo** (`whsec_…`) aparece uma única vez. Você usa esse segredo para conferir a assinatura de cada aviso.

## Eventos

| Tipo | Quando |
|---|---|
| `cobranca.paga` | O cliente pagou pelo app |
| `cobranca.cancelada` | O PDV, a loja ou uma cascata (caixa desligado) cancelou |
| `cobranca.expirada` | Venceu o prazo sem pagamento |
| `cobranca.estornada` | A loja estornou uma venda paga (pelo painel dela) |
| `ponto_de_venda.desativado` | O caixa foi desligado (pelo gerente, pela TribeX ou pela API) |
| `ponto_de_venda.reativado` | O caixa voltou a funcionar |

## O que chega

```http
POST /seu/endereco HTTP/1.1
Content-Type: application/json
User-Agent: MoedaNobre-Webhooks/1
Moeda-Nobre-Evento-Id: evt_4f1c…
Moeda-Nobre-Tipo: cobranca.paga
Moeda-Nobre-Assinatura: t=1790000000,v1=5d41402abc4b2a76b9719d911017c592…

{"id":"evt_4f1c…","tipo":"cobranca.paga","criadoEm":"2026-09-25T14:04:02-03:00","ambiente":"PRODUCAO","dados":{"cobranca":{"id":"…","status":"PAGA","valorCentavos":4590,…}}}
```

O objeto dentro de `dados` é o **mesmo** da API (`cobranca` ou `pontoDeVenda`). Nunca vem nome, CPF ou saldo de cliente.

## Confira a assinatura (sempre)

A assinatura é um HMAC-SHA256, com o **segredo inteiro** (inclusive o prefixo `whsec_`), sobre `"{t}.{corpo cru}"`. Use o corpo **exatamente como chegou**, antes de qualquer parse. Recuse se o `t` tiver mais de 5 minutos de diferença do seu relógio, e compare em tempo constante.

```javascript
// Node.js
import crypto from "node:crypto"

export function assinaturaValida(segredo, corpoCru, cabecalho) {
  const partes = Object.fromEntries(cabecalho.split(",").map((p) => p.split("=").map((s) => s.trim())))
  const t = Number(partes.t)
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false
  const esperado = crypto.createHmac("sha256", segredo).update(`${t}.${corpoCru}`).digest("hex")
  const a = Buffer.from(esperado, "hex")
  const b = Buffer.from(partes.v1 ?? "", "hex")
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
```

```python
# Python
import hashlib, hmac, time

def assinatura_valida(segredo: str, corpo_cru: bytes, cabecalho: str) -> bool:
    partes = dict(p.strip().split("=", 1) for p in cabecalho.split(",") if "=" in p)
    t = int(partes.get("t", "0"))
    if abs(time.time() - t) > 300:
        return False
    esperado = hmac.new(segredo.encode(), f"{t}.".encode() + corpo_cru, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, partes.get("v1", ""))
```

```php
// PHP
function assinaturaValida(string $segredo, string $corpoCru, string $cabecalho): bool {
    $partes = [];
    foreach (explode(',', $cabecalho) as $p) {
        [$k, $v] = array_pad(explode('=', $p, 2), 2, '');
        $partes[trim($k)] = trim($v);
    }
    $t = (int) ($partes['t'] ?? 0);
    if (abs(time() - $t) > 300) return false;
    $esperado = hash_hmac('sha256', $t . '.' . $corpoCru, $segredo);
    return hash_equals($esperado, $partes['v1'] ?? '');
}
```

```csharp
// C#
using System.Security.Cryptography;
using System.Text;

static bool AssinaturaValida(string segredo, string corpoCru, string cabecalho)
{
    var partes = cabecalho.Split(',')
        .Select(p => p.Split('=', 2))
        .Where(p => p.Length == 2)
        .ToDictionary(p => p[0].Trim(), p => p[1].Trim());
    if (!partes.TryGetValue("t", out var ts) || !long.TryParse(ts, out var t)) return false;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(segredo));
    var esperado = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{corpoCru}"))).ToLowerInvariant();
    partes.TryGetValue("v1", out var recebido);
    return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(esperado), Encoding.ASCII.GetBytes(recebido ?? ""));
}
```

```java
// Java 17+
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

static boolean assinaturaValida(String segredo, String corpoCru, String cabecalho) throws Exception {
    String t = null, v1 = null;
    for (String p : cabecalho.split(",")) {
        String[] kv = p.split("=", 2);
        if (kv.length < 2) continue;
        if (kv[0].trim().equals("t")) t = kv[1].trim();
        if (kv[0].trim().equals("v1")) v1 = kv[1].trim();
    }
    if (t == null || v1 == null) return false;
    if (Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(t)) > 300) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(segredo.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    String esperado = HexFormat.of().formatHex(mac.doFinal((t + "." + corpoCru).getBytes(StandardCharsets.UTF_8)));
    return MessageDigest.isEqual(esperado.getBytes(StandardCharsets.US_ASCII), v1.getBytes(StandardCharsets.US_ASCII));
}
```

## Responda rápido e processe uma vez

- Responda **2xx em até 10 segundos** — antes de processar, se o processamento for demorado (enfileire do seu lado).
- O mesmo evento pode chegar **mais de uma vez** (retentativa, reenvio manual). Use `Moeda-Nobre-Evento-Id` para ignorar repetidos.
- A ordem de chegada não é garantida. Use o `status` da cobrança (ou consulte `GET /cobrancas/{id}`) como a verdade.
- Redirecionamento (3xx) não é seguido: conta como falha.

## Retentativas e desativação automática

Se o seu servidor não responder 2xx, tentamos de novo: **na hora, depois de 1 min, 5 min, 30 min, 2 h e 12 h** (6 tentativas). Na 6ª falha a entrega vira `FALHOU` e os administradores da sua empresa recebem um e-mail. Você pode reenviar qualquer entrega pelo portal.

Se um endereço só tiver falhas por **72 horas**, ele é desativado automaticamente (e-mail aos administradores). Corrija o servidor e clique em **Reativar** — mandamos um `ping` antes de religar.

Nada se perde: todos os eventos ficam em `GET /eventos` por 30 dias.


---

# Erros

Todo erro vem no mesmo envelope:

```json
{
  "erro": {
    "codigo": "COBRANCA_PENDENTE_EXISTE",
    "mensagem": "Já existe uma cobrança aberta neste caixa. Cancele ou use cancelarPendenteAnterior.",
    "detalhes": { "cobrancaId": "…" },
    "requestId": "req_7Qm…"
  }
}
```

Decida pelo **`codigo`** (estável), não pela `mensagem` (pode mudar de texto). Guarde o `requestId`: é o que o suporte precisa para achar a chamada.

## Todos os códigos

| HTTP | Código | Mensagem |
|---|---|---|
| 400 | `VALIDACAO` | Um ou mais campos são inválidos. |
| 400 | `CAMPO_DESCONHECIDO` | O corpo tem campos que esta rota não aceita. |
| 400 | `IDEMPOTENCY_KEY_OBRIGATORIA` | Envie o cabeçalho Idempotency-Key (até 128 caracteres). |
| 400 | `CENARIO_SO_EM_TESTE` | O cabeçalho Moeda-Nobre-Teste-Cenario só vale com chave de teste. |
| 400 | `CENARIO_INVALIDO` | Cenário de teste desconhecido. |
| 401 | `CHAVE_INVALIDA` | A chave não existe ou está errada. Confira se copiou inteira. |
| 401 | `CHAVE_REVOGADA` | Esta chave foi revogada. Crie uma nova no portal. |
| 401 | `PARCEIRO_SUSPENSO` | O acesso da sua empresa está suspenso. Fale com a TribeX. |
| 401 | `PRODUCAO_NAO_LIBERADA` | Sua conta ainda só usa chaves de teste. Conclua a homologação. |
| 401 | `APLICATIVO_DESATIVADO` | O aplicativo desta chave está desativado. |
| 403 | `CHAVE_EM_NAVEGADOR` | A chave foi usada a partir de um navegador. Ela deve ficar no servidor. |
| 404 | `PDV_NAO_ENCONTRADO` | Ponto de venda não encontrado. |
| 404 | `COBRANCA_NAO_ENCONTRADA` | Cobrança não encontrada. |
| 404 | `RECURSO_NAO_ENCONTRADO` | Recurso não encontrado. |
| 405 | `METODO_NAO_PERMITIDO` | Método não permitido nesta rota. |
| 409 | `COBRANCA_PENDENTE_EXISTE` | Já existe uma cobrança aberta neste caixa. Cancele ou use cancelarPendenteAnterior. |
| 409 | `COBRANCA_NAO_PENDENTE` | A cobrança não está mais pendente. |
| 409 | `REQUISICAO_EM_ANDAMENTO` | Uma requisição com esta Idempotency-Key ainda está em andamento. Repita em 1 segundo. |
| 409 | `PDV_JA_ATIVO` | Este caixa já está ativo neste aplicativo. |
| 410 | `CODIGO_ATIVACAO_EXPIRADO` | O código de ativação expirou. Peça um novo ao gerente da loja. |
| 413 | `CORPO_GRANDE` | O corpo da requisição passa de 64 KB. |
| 415 | `CONTENT_TYPE` | Envie o corpo como application/json. |
| 422 | `IDEMPOTENCY_CONFLITO` | Você reutilizou uma Idempotency-Key com dados diferentes. |
| 422 | `PDV_INATIVO` | O gerente da loja desativou este caixa. |
| 422 | `LOJA_INATIVA` | A loja está inativa na Moeda Nobre. |
| 422 | `VINCULO_REVOGADO` | A loja desconectou o seu sistema. |
| 422 | `CODIGO_ATIVACAO_INVALIDO` | Código de ativação inválido. |
| 429 | `LIMITE_EXCEDIDO` | Limite de requisições excedido. Aguarde e tente de novo. |
| 500 | `ERRO_INTERNO` | Erro interno. Informe o requestId ao suporte. |
| 503 | `INDISPONIVEL` | Serviço temporariamente indisponível. Tente de novo em instantes. |

## O que fazer em cada caso

| Código | O que fazer |
|---|---|
| `CHAVE_INVALIDA` | A chave não existe ou está errada. Confira se copiou inteira e se é do ambiente certo. |
| `CHAVE_REVOGADA` | Crie uma chave nova no portal e troque no servidor. |
| `PRODUCAO_NAO_LIBERADA` | Sua conta ainda só usa chaves de teste. Conclua a homologação e peça a liberação. |
| `PARCEIRO_SUSPENSO` | O acesso da sua empresa está suspenso. Fale com a TribeX. |
| `PDV_INATIVO` | O gerente da loja desligou este caixa. Mostre isso ao operador — **não** fique repetindo. |
| `LOJA_INATIVA` | A loja está inativa na Moeda Nobre. |
| `VINCULO_REVOGADO` | A loja desconectou o seu sistema. Só volta com um código novo do gerente. |
| `COBRANCA_PENDENTE_EXISTE` | Já há uma cobrança aberta neste caixa (id em `detalhes.cobrancaId`). Cancele-a ou mande `cancelarPendenteAnterior: true`. |
| `COBRANCA_NAO_PENDENTE` | A cobrança mudou de estado (`detalhes.statusAtual`). Se for `PAGA`, o cliente pagou: siga com a venda. |
| `IDEMPOTENCY_CONFLITO` | Você reutilizou uma `Idempotency-Key` com dados diferentes. Use uma chave nova para uma cobrança nova. |
| `REQUISICAO_EM_ANDAMENTO` | A mesma `Idempotency-Key` ainda está sendo processada. Repita depois do `Retry-After` (1 s). |
| `CHAVE_EM_NAVEGADOR` | A chave foi usada a partir de um navegador. Ela deve ficar no servidor. |
| `LIMITE_EXCEDIDO` | Espere o `Retry-After` (em segundos) e tente de novo. |
| `ERRO_INTERNO` / `INDISPONIVEL` | Tente de novo com espera crescente (1 s, 2 s, 4 s…). Em `POST /cobrancas`, repita com a **mesma** `Idempotency-Key` — nunca cria duas. |

## Quando repetir

- **Repita** (com espera crescente): `429`, `500`, `503`, `409 REQUISICAO_EM_ANDAMENTO` e falhas de rede.
- **Não repita** a mesma chamada: os demais `4xx`. Eles só mudam se algo mudar (o operador, a loja, o seu código). A homologação confere isso: depois de um `422 PDV_INATIVO`, no máximo 3 chamadas iguais no minuto seguinte.


---

# Ambiente de testes

Com uma chave `mn_test_…`, a API responde **igual à produção**, mas sobre lojas, caixas e clientes fictícios que você mesmo cria. Nada encosta em loja real, cliente real ou dinheiro.

## Dados de teste (portal → Dados de teste)

| O quê | Como | Limite |
|---|---|---|
| Lojas de teste | **Nova loja** (o nome é inventado se você deixar em branco). CNPJ fictício, com selo TESTE. | 10 por aplicativo |
| Código de ativação | Na loja, **Gerar código** → `T-XXXX-XXXX`, válido por 24 h, uso único. | — |
| Caixas de teste | Ligados pela API com o código `T-…` (`POST /pontos-de-venda/ativar`). | 50 por aplicativo |
| Clientes de teste | **Novo cliente** — saldo fictício editável (padrão R$ 500,00). | 20 por aplicativo |

**Gerar dados de exemplo** cria 1 loja, 2 clientes e 1 código de uma vez. **Apagar todos os dados de teste** limpa tudo (pede o código do e-mail). Dados sem uso há 30 dias são apagados sozinhos.

Não use dados de pessoas reais em nomes ou `metadados`.

## Três jeitos de pagar uma cobrança de teste

1. **Celular de teste** (portal → Dados de teste → Celular de teste): lê o QR que o seu caixa mostrou ou imprimiu, pela câmera ou colando o conteúdo. Escolha o cliente e use o PIN **1234**. Erros iguais aos do app real: QR inválido, expirado, já pago, saldo insuficiente.
2. **Pelo portal**, em **Cobranças** (seletor em Testes): **Pagar como…**, **Expirar agora**, **Loja cancela**, **Estornar**.
3. **Cenário automático**, com um cabeçalho no `POST /cobrancas`:

| `Moeda-Nobre-Teste-Cenario` | O que acontece |
|---|---|
| `pagar_em_3s` | Paga depois de 3 segundos |
| `expirar_em_10s` | Expira depois de 10 segundos |
| `loja_cancela_em_3s` | A loja cancela depois de 3 segundos |
| `pagar_e_estornar` | Paga em 3 s e a loja estorna em 10 s |

Com chave de produção, o cabeçalho é recusado (`400 CENARIO_SO_EM_TESTE`). Se você cancelar antes, o cenário perde a corrida e não faz nada.

## O que a loja faria (portal → Dados de teste → loja)

Para ver o que o seu PDV recebe quando a loja mexe nas coisas:

| Simulação | O que o PDV recebe |
|---|---|
| Desligar caixa | `422 PDV_INATIVO` nas cobranças e o evento `ponto_de_venda.desativado` |
| Inativar loja | `422 LOJA_INATIVA` |
| Revogar integração | `422 VINCULO_REVOGADO` (até ativar um código novo) |
| Estornar uma cobrança paga | evento `cobranca.estornada` e `status: "ESTORNADA"` no `GET` |

## Limites em testes

60 chamadas por minuto e 500 cobranças por dia por aplicativo; 30 cobranças por minuto por caixa. Em produção os limites são maiores e definidos pela TribeX.

## Console de testes

Na **Documentação** do portal há um console que executa as chamadas da API em testes pelo servidor, com a chave que você escolher pelo nome — a chave nunca vai para o navegador. As chamadas do console **não contam** para a homologação: ela tem de ser provada pelo seu PDV.

## Isolamento

- Código real (`XXXX-XXXX`) com chave de teste → `422 CODIGO_ATIVACAO_INVALIDO` com a dica "use sua chave de produção"; código `T-…` com chave de produção → a dica inversa.
- Um QR de teste lido pelo app real é recusado como "QR inválido".
- Chave de teste pedindo caixa ou cobrança real → `404`.


---

# Boas práticas

## A chave fica no servidor

O caixa da loja fala com o **seu** servidor; o seu servidor fala com a Moeda Nobre. Nunca coloque a chave no aplicativo instalado nos caixas, num site ou num repositório. Mantenha duas chaves ativas para trocar sem parar a operação.

## Uma Idempotency-Key por venda

Use um identificador da venda que você já tem (número do cupom, id do pedido) como `Idempotency-Key`. Se a rede cair no meio do `POST /cobrancas`, repita com a **mesma** chave e o **mesmo** corpo: você recebe a mesma cobrança (`200` com `Idempotent-Replayed: true`), nunca duas. Chave igual com corpo diferente → `422 IDEMPOTENCY_CONFLITO`.

## Uma cobrança aberta por caixa

Cada caixa tem no máximo **uma** cobrança pendente. Se o operador desistir da venda, cancele (`POST /cobrancas/{id}/cancelar`) ou crie a próxima com `cancelarPendenteAnterior: true`. Se a resposta for `409 COBRANCA_NAO_PENDENTE` com `statusAtual: "PAGA"`, o cliente pagou a anterior — siga com aquela venda, não cobre de novo.

## Saber se pagou: segure a consulta

```
GET /cobrancas/{id}?aguardar=25&semImagem=true
```

A resposta volta assim que o status muda (ou em 25 s, com o status atual). Em loop, enquanto `PENDENTE` e antes do `expiraEm`. É mais rápido e mais barato que consultar a cada segundo, e funciona atrás de qualquer internet de loja. O webhook é um complemento para o servidor.

## Prazo da cobrança

`expiraEmSegundos` vai de 60 a 900 (padrão 300). Depois dele a cobrança vira `EXPIRADA` sozinha e o cliente não consegue mais pagar. Mostre ao operador um contador baseado no `expiraEm`.

## Não entre em loop no erro

Um `4xx` de regra de negócio (`PDV_INATIVO`, `LOJA_INATIVA`, `VINCULO_REVOGADO`, `CHAVE_REVOGADA`…) não vai mudar se você repetir. Mostre a mensagem ao operador e pare. Repita só `429`, `5xx`, `409 REQUISICAO_EM_ANDAMENTO` e falhas de rede, com espera crescente.

## Relógio certo

A assinatura dos webhooks recusa diferenças acima de 5 minutos. Mantenha o relógio do servidor sincronizado (NTP).

## Sem dado pessoal

`metadados` e `referenciaExterna` servem para o **seu** controle (operador, número do cupom, mesa). Não coloque nome, CPF, telefone ou e-mail de cliente. A Moeda Nobre também não manda nenhum dado de cliente para você.

## Guarde o requestId

Registre o `Moeda-Nobre-Request-Id` de cada chamada nos seus logs. Com ele o suporte (e você, em **Monitoramento → Registros** no portal) encontra a chamada exata.

## Homologação

Antes de pedir a produção, o seu PDV prova no ambiente de testes, sozinho, estes 9 itens (a tela **Homologação** do portal acompanha ao vivo):

1. Conectou (`GET /ping`)
2. Ativou um caixa com código `T-…`
3. Criou cobrança com `Idempotency-Key`
4. Repetiu a criação sem duplicar (resposta com `Idempotent-Replayed`)
5. Detectou o pagamento (leu `PAGA` depois de pago, ou recebeu o webhook)
6. Cancelou uma cobrança
7. Leu uma cobrança `EXPIRADA`
8. Viu um estorno (leu `ESTORNADA` ou recebeu o webhook)
9. Depois de um `422 PDV_INATIVO`, fez no máximo 3 chamadas iguais no minuto seguinte

Com os 9 verdes, um administrador da sua empresa pede a liberação. A TribeX também confere, numa conversa, onde a chave fica guardada e se a assinatura do webhook é verificada.


---

# Changelog

A versão está no caminho (`/v1`). Campo novo numa resposta, evento novo ou código de erro novo **não** quebram a v1 — ignore o que você não conhece. Qualquer mudança incompatível vira `/v2`, com aviso e período de convivência.

## 1.0.0 — 26/09/2026

Primeira versão pública.

- Caixas: ativar com código (`POST /pontos-de-venda/ativar`), listar, consultar, desligar.
- Cobranças: criar com `Idempotency-Key`, consultar com espera (`?aguardar=`), listar, cancelar.
- Eventos: `GET /eventos` (30 dias) e webhooks assinados (HMAC-SHA256, `t=…,v1=…`).
- Ambiente de testes com dados fictícios, celular de teste, cenários automáticos e simulações da loja.
- Homologação automática com 9 itens e pedido de produção pelo portal.
- Repetir `POST /cobrancas` com a mesma `Idempotency-Key` e o mesmo corpo responde `200` com `Idempotent-Replayed: true` e a cobrança no estado de agora (a criação responde `201`).
- O `nome` do caixa tem até 60 caracteres e não aceita endereço de site nem e-mail: ele aparece para o gerente da loja nos avisos da Moeda Nobre.
- Rota que não existe responde `404 RECURSO_NAO_ENCONTRADO` no mesmo envelope de erro, com `requestId`.
