{
  "info": {
    "name": "Vmix Pay — API Pública (integrador)",
    "_postman_id": "b1d4e7a2-9c3f-4f1a-8e21-vmixpay000002",
    "description": "Collection da **API pública do Vmix Pay** — a superfície usada pelos integradores.\n\nAutenticação OAuth2 `client_credentials`:\n1. **API Pública → Obter token** (`POST /v1/oauth/token`, HTTP Basic com `client_id`/`client_secret`) → captura `access_token` na variável `api_access_token`.\n2. As demais requests usam esse token como `Authorization: Bearer {{api_access_token}}`.\n\nFluxo sugerido (rode na ordem):\n1. **Health → /health/live** — confirme conectividade (sem auth).\n2. **API Pública → Obter token** — preencha `client_id`/`client_secret` no environment, rode → `api_access_token` é capturado.\n3. **API Pública → /me** — confirma a identidade do integrador (Bearer).\n4. **Charges → Criar cobrança PIX** (ou **Boleto → Emitir boleto**) — captura `charge_id`.\n5. **Consultas & Refunds**, **Wallet** — consultas e devoluções.\n\nOs scripts de teste já salvam tokens/ids nas variáveis da collection — basta rodar na ordem.\n\n⚠️ Esta collection contém SOMENTE endpoints da API pública. As credenciais (`client_id`/`client_secret`) são criadas no painel; o `client_secret` é exibido uma única vez.\n\n⚠️ Por padrão `base_url` aponta para a API REAL de produção. Não há sandbox: cobranças/refunds criados são reais.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.vmixpay.com.br",
      "type": "string"
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "client_secret",
      "value": "",
      "type": "string"
    },
    {
      "key": "api_access_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "account_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "charge_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "refund_id",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Health",
      "item": [
        {
          "name": "GET /health/live",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [],
            "url": {
              "raw": "{{base_url}}/health/live",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health",
                "live"
              ]
            },
            "description": "Liveness probe (não toca dependências). Útil para confirmar conectividade/`base_url`."
          }
        },
        {
          "name": "GET /health/ready",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [],
            "url": {
              "raw": "{{base_url}}/health/ready",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "health",
                "ready"
              ]
            },
            "description": "Readiness probe."
          }
        }
      ]
    },
    {
      "name": "API Pública (integrador)",
      "description": "Fluxo OAuth2 client_credentials que os integradores usam. Use `client_id`/`client_secret` da credencial criada no painel.",
      "item": [
        {
          "name": "POST /v1/oauth/token (Obter token)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j.access_token) { pm.collectionVariables.set('api_access_token', j.access_token); console.log('api_access_token capturado (expira em ' + j.expires_in + 's, scope: ' + j.scope + ')'); }"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "auth": {
              "type": "basic",
              "basic": [
                {
                  "key": "username",
                  "value": "{{client_id}}",
                  "type": "string"
                },
                {
                  "key": "password",
                  "value": "{{client_secret}}",
                  "type": "string"
                }
              ]
            },
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "grant_type",
                  "value": "client_credentials",
                  "type": "text"
                }
              ]
            },
            "url": {
              "raw": "{{base_url}}/v1/oauth/token",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "oauth",
                "token"
              ]
            },
            "description": "Troca client_id/client_secret (HTTP Basic) por um `access_token` Bearer curto. Resposta `{ access_token, token_type, expires_in, scope }` (captura `api_access_token`). Credencial inválida → 401 `{ \"error\": \"invalid_client\" }` (formato RFC 6749). grant_type errado → 400 `{ \"error\": \"unsupported_grant_type\" }`. Aceita também body JSON `{ \"grant_type\": \"client_credentials\" }`. Rate-limit 5/min por IP."
          }
        },
        {
          "name": "GET /v1/me",
          "request": {
            "method": "GET",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{api_access_token}}",
                  "type": "string"
                }
              ]
            },
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "Identidade do integrador autenticado: `{ clientId, merchantId, scopes }`. Token expirado/revogado/aud errado → 401."
          }
        }
      ]
    },
    {
      "name": "API Pública — Charges (PIX)",
      "description": "Cobranças PIX (Core Financeiro). Usa o `api_access_token` (Bearer) obtido em /oauth/token. `amount` é STRING ('199.90'). `POST pix` exige header Idempotency-Key (use o mesmo valor para testar o replay idempotente → 200). Requer um api_client com os scopes charges:create/charges:read.",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{api_access_token}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "POST /v1/charges/pix",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j.charge_id) pm.collectionVariables.set('charge_id', j.charge_id);",
                  "console.log('charge_id=' + pm.collectionVariables.get('charge_id') + ' status=' + j.status);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "uuid de idempotência; fixe o valor para testar replay (→200)"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"account_id\": \"{{account_id}}\",\n  \"amount\": \"199.90\",\n  \"expires_in\": 3600,\n  \"external_id\": \"pedido-123\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/charges/pix",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges",
                "pix"
              ]
            },
            "description": "Cria uma cobrança PIX. Requer `charges:create` + header Idempotency-Key. 201 (nova) / 200 (replay mesma chave+corpo). Captura `charge_id`. Conta de outro merchant → 404; sem o scope → 403; amount inválido / sem Idempotency-Key → 400."
          }
        },
        {
          "name": "GET /v1/charges/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/charges/{{charge_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges",
                "{{charge_id}}"
              ]
            },
            "description": "Consulta a cobrança (lazy-expire na leitura). Requer `charges:read`."
          }
        },
        {
          "name": "POST /v1/charges/:id/cancel",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/charges/{{charge_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges",
                "{{charge_id}}",
                "cancel"
              ]
            },
            "description": "Cancela a cobrança se `pending` (senão 409). Requer `charges:create`."
          }
        }
      ]
    },
    {
      "name": "API Pública — Charges (Boleto)",
      "description": "Cobranças por BOLETO (boleto normal, híbrido ou boleto+PIX). Usa o `api_access_token` (Bearer). `valorCents` carrega uma STRING DECIMAL EM REAIS (ex.: '199.90') — apesar do nome — convertida em centavos no servidor. `POST boleto` exige header Idempotency-Key (mesmo valor → replay idempotente → 200). `pagador` é PII (LGPD). Scopes: emitir = `charges:create`; baixar PDF = `charges:read`.",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{api_access_token}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "POST /v1/charges/boleto",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j.charge_id) pm.collectionVariables.set('charge_id', j.charge_id);",
                  "console.log('charge_id=' + pm.collectionVariables.get('charge_id') + ' status=' + j.status + ' linha_digitavel=' + (j.boleto && j.boleto.linha_digitavel));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "uuid de idempotência; fixe o valor para testar replay (→200)"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"tipo\": \"boleto_pix\",\n  \"valorCents\": \"199.90\",\n  \"dataVencimento\": \"2026-12-31\",\n  \"account_id\": \"{{account_id}}\",\n  \"descricao\": \"Pedido 123\",\n  \"pagador\": {\n    \"nome\": \"Fulano de Tal\",\n    \"documento\": \"123.456.789-09\",\n    \"tipoPessoa\": \"PESSOA_FISICA\",\n    \"cep\": \"01001-000\",\n    \"cidade\": \"São Paulo\",\n    \"logradouro\": \"Praça da Sé\",\n    \"numero\": \"100\",\n    \"uf\": \"SP\"\n  }\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/charges/boleto",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges",
                "boleto"
              ]
            },
            "description": "Emite uma cobrança por boleto. `tipo` ∈ { `boleto` (normal), `boleto_hibrido`, `boleto_pix` }. `valorCents` é STRING decimal em reais ('199.90'); `dataVencimento` no formato `YYYY-MM-DD`; `account_id` opcional (cai na conta do token se omitido); `pagador` (PII) com nome/documento/tipoPessoa/cep/cidade/logradouro/uf obrigatórios e `numero` opcional. Requer `charges:create` + header Idempotency-Key. 201 (nova) / 200 (replay mesma chave+corpo). Resposta inclui `charge_id`, `payment_method`, `amount`, e o bloco `boleto` { nosso_numero, seu_numero, linha_digitavel, codigo_barras, qr_code, tipo_cobranca, data_vencimento } — `qr_code` só nos tipos híbrido/pix. Captura `charge_id`. Sem o scope → 403; sem Idempotency-Key → 400; data inválida → 400 INVALID_DUE_DATE."
          }
        },
        {
          "name": "GET /v1/charges/boleto/:id/pdf",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/charges/boleto/{{charge_id}}/pdf",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges",
                "boleto",
                "{{charge_id}}",
                "pdf"
              ]
            },
            "description": "Baixa o PDF do boleto de uma cobrança (`application/pdf`, como anexo). Tenant-safe: se a charge não existir, for de outro merchant ou não for um boleto → 404 genérico (não vaza existência entre tenants). Requer `charges:read`. No Postman use \"Send and Download\" para salvar o arquivo."
          }
        }
      ]
    },
    {
      "name": "API Pública — Consultas & Refunds",
      "description": "Lado PULL da API pública (ADR-022): listar cobranças/pagamentos/eventos com cursor pagination + devoluções (refunds). Usa o `api_access_token` (Bearer). As listas devolvem `{ data, next_cursor }`; pagine repassando `next_cursor` em `?cursor=`. Scopes: listas de charges = `charges:read`; payments/events/GET refund = `payments:read`; POST refund = `refunds:create`. ⚠️ o GET de refund usa `payments:read` (não há `refunds:read`).",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{api_access_token}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "GET /v1/charges (lista, cursor)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/charges?status=paid&limit=2",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "charges"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "paid"
                },
                {
                  "key": "limit",
                  "value": "2"
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true,
                  "description": "Repasse o next_cursor da página anterior para paginar."
                }
              ]
            },
            "description": "Lista cobranças do merchant com cursor pagination (created_at,id desc). Query opcional: status (pending|paid|expired|cancelled|refunded), account_id, limit (default 25, máx 100), cursor. Resposta { data, next_cursor }. Requer `charges:read`. cursor inválido → 400 CURSOR_INVALID; limit inválido → 400 LIMIT_INVALID."
          }
        },
        {
          "name": "GET /v1/payments (lista, cursor)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/payments?limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "payments"
              ],
              "query": [
                {
                  "key": "account_id",
                  "value": "{{account_id}}",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Lista pagamentos do merchant (status sempre `confirmed`). Query opcional: account_id, limit, cursor. Item: { payment_id, charge_id, account_id, amount, status, paid_at, bank_reference }. Requer `payments:read`."
          }
        },
        {
          "name": "GET /v1/events (feed pull)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/events?limit=25",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "events"
              ],
              "query": [
                {
                  "key": "type",
                  "value": "payment.received",
                  "disabled": true
                },
                {
                  "key": "account_id",
                  "value": "{{account_id}}",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Feed PULL de eventos de domínio (alternativa durável ao webhook). Query opcional: type (payment.received|charge.expired|charge.cancelled|refund.completed), account_id, limit, cursor. Item: { event_id, type, account_id, occurred_at, data }. `data` é o mesmo payload do webhook (mesmo event_id → dedup push/pull). Requer `payments:read`."
          }
        },
        {
          "name": "POST /v1/refunds (total/parcial)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const j = pm.response.json();",
                  "if (j.refund_id) pm.collectionVariables.set('refund_id', j.refund_id);",
                  "console.log('refund_id=' + pm.collectionVariables.get('refund_id') + ' status=' + j.status);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "uuid de idempotência; fixe o valor para testar replay (→200)"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"charge_id\": \"{{charge_id}}\",\n  \"amount\": \"199.90\"\n}"
            },
            "url": {
              "raw": "{{base_url}}/v1/refunds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "refunds"
              ]
            },
            "description": "Cria uma devolução (total ou parcial) de uma cobrança PAGA. `amount` é STRING em reais; OMITA `amount` para estornar o saldo restante (total). Exige `refunds:create` + header Idempotency-Key. 201 (novo) / 200 (replay mesma chave+corpo). Captura `refund_id`. Refund total → cobrança vira `refunded`; parcial → segue `paid`. Erros: 409 CHARGE_NOT_REFUNDABLE (cobrança não-paga ou já refunded), 422 REFUND_EXCEEDS (excede o saldo numa cobrança paid), 409 IDEMPOTENCY_KEY_REUSED (mesma chave, corpo diferente), 404 (cobrança de outro merchant), 403 (sem o scope), 502 BANK_UNAVAILABLE (banco falhou; refund failed re-tentável)."
          }
        },
        {
          "name": "GET /v1/refunds/:id",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/refunds/{{refund_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "refunds",
                "{{refund_id}}"
              ]
            },
            "description": "Consulta um refund por id (tenant-safe; refund de outro merchant → 404 REFUND_NOT_FOUND). ⚠️ Usa o scope `payments:read` (NÃO `refunds:create`) — não há scope `refunds:read` dedicado; um client só com `refunds:create` recebe 403 aqui."
          }
        }
      ]
    },
    {
      "name": "API Pública — Wallet",
      "description": "Consulta de saldo e extrato da wallet da conta (Wallet/Ledger — ADR-023). Usa o `api_access_token` (Bearer). Scope: `payments:read`.",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{api_access_token}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "GET /v1/accounts/:accountId/balance",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/accounts/{{account_id}}/balance",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "{{account_id}}",
                "balance"
              ]
            },
            "description": "Saldo da wallet da conta. Resposta { balance: \"194.50\", currency: \"BRL\" } (string em reais). Requer `payments:read`. Tenant-safe: conta inexistente/de outro merchant → 404; conta própria sem movimento → balance \"0.00\" (não 404)."
          }
        },
        {
          "name": "GET /v1/accounts/:accountId/ledger (cursor)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/accounts/{{account_id}}/ledger?limit=2",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "accounts",
                "{{account_id}}",
                "ledger"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "2"
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true
                }
              ]
            },
            "description": "Extrato da wallet (cursor pagination, { data, next_cursor }). Item: { entry_id, transaction_kind (payment|refund), direction (credit|debit), amount, balance_after, reference_id, created_at }. limit default 25, máximo 100. Requer `payments:read`. 400 LIMIT_INVALID/CURSOR_INVALID; 404 se conta de outro merchant."
          }
        }
      ]
    }
  ]
}
