{
  "openapi": "3.0.3",
  "info": {
    "title": "Vexa Pagamentos — API interna do Checkout",
    "version": "1.0.0",
    "contact": {
      "name": "Vexa Pagamentos",
      "email": "contato@vexapagamentos.com",
      "url": "https://vexapagamentos.com"
    },
    "description": "Documentação técnica da **API interna** do software de checkout\nVexa Pagamentos.\n\n**Posicionamento — leitura obrigatória antes da análise técnica**\n\nA Vexa Pagamentos não é gateway, sub-adquirente ou IP (Instituição\nde Pagamento). Operamos como **software de checkout de uso interno**\ndo CNPJ 51.848.548/0001-27. Os endpoints aqui descritos:\n\n- são consumidos **exclusivamente** pelo nosso próprio checkout\n  (frontend e backoffice da Vexa Pagamentos);\n- **não** estão expostos como produto comercial a terceiros;\n- **não** possuem fluxo de auto-cadastro ou geração pública de\n  credenciais — o cadastro de usuários operadores é feito\n  manualmente, sob convite, pela administração;\n- **não** roteiam transações de outros lojistas (single-merchant\n  master, sem split, sem multi-tenant em produção);\n- liquidam ao titular único (saque manual).\n\nO domínio https://vexapagamentos.com funciona como **central de\nsuporte ao cliente** — não é página institucional/comercial,\napenas atendimento (e-mail, WhatsApp, horário, FAQ). Sem\n`/register` público, sem marketing comercial, sem precificação\npública.\n\n**Auth**\n\n`Authorization: Bearer <token-interno>` — token opaco emitido\nmanualmente para o operador titular. Não há endpoint de signup ou\nde emissão self-service. Tokens são revogáveis pelo painel interno.\n\n**Idempotência**\n\n`Idempotency-Key` é obrigatório em todo POST que cria recurso\nfinanceiro. Replay de chave já vista retorna a resposta original\ncom `Idempotency-Replayed: true`.\n\n**Versionamento**\n\nPath-prefixed (`/v1/...`). Mudanças incompatíveis publicam `/v2/...`\n— sem breaking change silencioso.\n\n**Ambiente**\n\nProdução única. Não publicamos sandbox externo (a homologação roda\nem ambiente controlado internamente).\n"
  },
  "servers": [
    {
      "url": "https://api.vexapagamentos.com/v1",
      "description": "Produção (uso interno do operador titular)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Profile",
      "description": "Quem você é. Útil pra validar que a chave (`sk_test_*`/`sk_live_*`)\nfoi configurada certa antes de chamar rotas que mexem em dinheiro.\n"
    },
    {
      "name": "Charges",
      "description": "Cobranças geradas pelo próprio checkout Vexa quando o cliente\nfinaliza compra de um produto cadastrado pelo operador titular.\n"
    },
    {
      "name": "Subscriptions",
      "description": "Assinaturas recorrentes vinculadas a produtos do operador\ntitular. Renovação e cancelamento operados pelo próprio painel\ninterno.\n"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "Token opaco de uso interno. Emitido manualmente pela\nadministração da Vexa Pagamentos para o operador titular.\nNão há fluxo público de obtenção.\n"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "invalid_input",
            "description": "Código curto e estável para tratamento programático."
          },
          "message": {
            "type": "string",
            "description": "Descrição legível do erro."
          },
          "details": {
            "type": "object",
            "description": "Campos adicionais específicos do erro."
          }
        }
      },
      "Charge": {
        "type": "object",
        "required": [
          "id",
          "object",
          "amount_cents",
          "currency",
          "method",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "charge"
            ]
          },
          "product_id": {
            "type": "string",
            "format": "uuid",
            "description": "Produto do operador titular ao qual a cobrança pertence."
          },
          "amount_cents": {
            "type": "string",
            "description": "Valor em centavos (string para preservar precisão em JSON).",
            "example": "1990"
          },
          "currency": {
            "type": "string",
            "enum": [
              "BRL"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "pix",
              "card",
              "boleto"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "authorized",
              "paid",
              "failed",
              "expired",
              "refunded",
              "partially_refunded",
              "chargeback"
            ]
          },
          "external_id": {
            "type": "string",
            "nullable": true,
            "description": "Identificador interno da cobrança no processador."
          },
          "paid_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "failed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "pix": {
            "type": "object",
            "nullable": true,
            "description": "Presente apenas quando `method = pix`.",
            "properties": {
              "qr_code": {
                "type": "string",
                "description": "Código EMV copia-e-cola."
              },
              "copy_paste_code": {
                "type": "string"
              },
              "qr_code_image_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "card": {
            "type": "object",
            "nullable": true,
            "description": "Presente apenas quando `method = card`.",
            "properties": {
              "brand": {
                "type": "string",
                "description": "Bandeira detectada pelo processador.",
                "enum": [
                  "visa",
                  "mastercard",
                  "elo",
                  "hipercard",
                  "amex",
                  "diners",
                  "unknown"
                ]
              },
              "last4": {
                "type": "string",
                "example": "4242",
                "description": "Últimos 4 dígitos do cartão."
              },
              "bin": {
                "type": "string",
                "example": "411111",
                "description": "BIN (6 primeiros dígitos)."
              },
              "installments": {
                "type": "integer",
                "minimum": 1,
                "maximum": 12,
                "example": 3
              },
              "failure_code": {
                "type": "string",
                "nullable": true,
                "description": "Código curto do processador quando `status = failed`."
              },
              "failure_reason": {
                "type": "string",
                "nullable": true,
                "description": "Razão legível do recusado."
              }
            }
          },
          "boleto": {
            "type": "object",
            "nullable": true,
            "description": "Presente apenas quando `method = boleto`.",
            "properties": {
              "digitable_line": {
                "type": "string",
                "description": "Linha digitável (47 dígitos formatados).",
                "example": "23793.39001 60000.123456 78901.234567 8 90120000019900"
              },
              "barcode": {
                "type": "string",
                "description": "Código de barras (44 dígitos)."
              },
              "pdf_url": {
                "type": "string",
                "format": "uri",
                "description": "PDF do boleto pra impressão."
              },
              "our_number": {
                "type": "string",
                "nullable": true,
                "description": "Nosso número (gerado pelo banco emissor)."
              },
              "due_date": {
                "type": "string",
                "format": "date-time",
                "description": "Data de vencimento (D+3 default, configurável)."
              },
              "instructions": {
                "type": "string",
                "nullable": true,
                "description": "Texto livre impresso no boleto (até 200 chars)."
              }
            }
          },
          "customer": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              }
            }
          }
        }
      },
      "CreateChargeRequest": {
        "type": "object",
        "required": [
          "productId",
          "customer"
        ],
        "properties": {
          "productId": {
            "type": "string",
            "format": "uuid",
            "description": "Produto cadastrado pelo operador titular no painel interno.\nA API rejeita qualquer `productId` que não pertença ao\ntitular autenticado.\n"
          },
          "method": {
            "type": "string",
            "enum": [
              "pix",
              "card",
              "boleto"
            ],
            "default": "pix"
          },
          "amount_cents": {
            "type": "integer",
            "minimum": 100,
            "description": "Valor em centavos. Se omitido, usa o preço cadastrado do produto.\nOverride só permitido para produtos com `allow_amount_override = true`.\n"
          },
          "expires_in_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400,
            "default": 1800,
            "description": "Aplicável a `method = pix`. Janela em que o QR Code é\nválido — após expirar, status migra pra `expired`.\n"
          },
          "installments": {
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "default": 1,
            "description": "Aplicável a `method = card`. Parcelamento sem juros até 12x\n(juros configurados a nível de produto). Pré-seleciona a\nquantidade de parcelas no Hosted Checkout pra onde o comprador\né redirecionado.\n"
          },
          "return_url": {
            "type": "string",
            "format": "uri",
            "description": "Aplicável a `method = card`. URL absoluta pra onde o Hosted\nCheckout redireciona após o comprador finalizar (com ou sem\nsucesso). **Não confie em parâmetros de query da redirect** —\nestado final é só via webhook.\n"
          },
          "boleto_due_date": {
            "type": "string",
            "format": "date",
            "description": "Aplicável a `method = boleto`. Vencimento desejado (default\nD+3). Aceita até D+30.\n"
          },
          "boleto_instructions": {
            "type": "string",
            "maxLength": 200,
            "description": "Aplicável a `method = boleto`. Texto livre impresso no boleto\n(instruções de pagamento, mensagem comercial).\n"
          },
          "customer": {
            "type": "object",
            "required": [
              "name",
              "email",
              "cpf"
            ],
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 255
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "cpf": {
                "type": "string",
                "pattern": "^[\\d\\.\\-]{11,14}$",
                "description": "CPF do comprador (com ou sem máscara)."
              },
              "phone": {
                "type": "string",
                "maxLength": 32,
                "description": "Celular com DDD (10–11 dígitos). **Obrigatório de fato**\n— o motor antifraude do processador rejeita pedido sem\ntelefone real (flag automática de risco).\n"
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/charges": {
      "get": {
        "tags": [
          "Charges"
        ],
        "summary": "Listar cobranças do operador titular",
        "description": "Lista cobranças do seller dono da Bearer, ordenadas por\n`created_at DESC, id DESC`. Paginação keyset via `cursor`.\nRetorna 401 se a Bearer for ausente/inválida.\n\nFiltros opcionais via query string. Sempre escopado ao titular\nautenticado (anti-IDOR).\n",
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string"
            },
            "description": "CSV de statuses canônicos\n(`pending,authorized,paid,failed,expired,refunded,partially_refunded,chargeback`).\nAceita 1+ valores separados por vírgula.\n"
          },
          {
            "in": "query",
            "name": "method",
            "schema": {
              "type": "string",
              "enum": [
                "pix",
                "card",
                "boleto"
              ]
            },
            "description": "Filtra por método de pagamento."
          },
          {
            "in": "query",
            "name": "since",
            "schema": {
              "type": "string"
            },
            "description": "Janela temporal — preset (`24h`, `7d`, `30d`) **ou** ISO 8601\ntimestamp (ex: `2026-04-01T00:00:00Z`). Filtra\n`created_at >= since`.\n"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Quantidade máxima de itens por página (1–100, default 25)."
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            },
            "description": "Cursor de paginação keyset, formato base64url de\n`<created_at_iso>|<uuid>` (devolvido como `next_cursor` na\npágina anterior). Use só o valor recebido — não construa\nmanualmente.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Página de cobranças.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Charge"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Cursor pra próxima página, ou null se acabou."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Filtro inválido (status, method, since, limit ou cursor).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Bearer ausente ou inválida."
          },
          "403": {
            "description": "Operador inativo ou scope insuficiente."
          },
          "429": {
            "description": "Limite de requisições excedido."
          }
        }
      },
      "post": {
        "tags": [
          "Charges"
        ],
        "summary": "Criar cobrança (consumido pelo checkout Vexa)",
        "description": "Cria uma cobrança para um produto do operador titular. Endpoint\nconsumido pelo **próprio checkout Vexa** quando o comprador\nfinaliza a compra. Não está exposto como API pública para\nterceiros gerarem cobranças.\n\nRetorna o QR Code PIX (quando `method=pix`), código de barras\n(quando `method=boleto`) ou identificador do 3DS (quando\n`method=card`).\n",
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 64,
              "pattern": "^[A-Za-z0-9_-]+$"
            },
            "description": "Chave fornecida pelo checkout para deduplicar requisições."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateChargeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replay idempotente — resposta original retornada.",
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Cobrança criada.",
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "false"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Charge"
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (CPF, valor, idempotency-key malformada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "403": {
            "description": "Operador inativo, motor antifraude rejeitou a transação ou\ntentativa de operar produto que não pertence ao titular\nautenticado.\n"
          },
          "404": {
            "description": "Produto não encontrado para o titular autenticado."
          },
          "409": {
            "description": "Idempotency-Key reutilizada com corpo diferente."
          },
          "429": {
            "description": "Limite de requisições excedido."
          },
          "502": {
            "description": "Erro retornado pelo processador."
          }
        }
      }
    },
    "/charges/{id}": {
      "get": {
        "tags": [
          "Charges"
        ],
        "summary": "Consultar cobrança",
        "description": "Lê uma cobrança previamente criada. Retorna 404 caso a cobrança\nnão pertença ao titular autenticado.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Charge"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "404": {
            "description": "Cobrança não encontrada para o titular autenticado."
          }
        }
      }
    },
    "/charges/{id}/capture": {
      "post": {
        "tags": [
          "Charges"
        ],
        "summary": "Capturar cobrança autorizada (cartão pré-autorizado)",
        "description": "Captura cobrança em estado `authorized` (pré-autorização de\ncartão). Idempotência opcional via cabeçalho `Idempotency-Key`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 64,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Captura concluída.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Charge"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "404": {
            "description": "Cobrança não encontrada para o titular autenticado."
          },
          "409": {
            "description": "Cobrança não está em estado capturável."
          },
          "502": {
            "description": "Erro retornado pelo processador."
          }
        }
      }
    },
    "/charges/{id}/simulate": {
      "post": {
        "tags": [
          "Charges"
        ],
        "summary": "Simular pagamento (apenas sandbox)",
        "description": "Força a transição de status de uma cobrança em sandbox sem\nexigir pagamento real. Disponível APENAS com chaves `sk_test_*`\n— chamadas com `sk_live_*` recebem `403 forbidden_in_live`.\n\nReusa o mesmo caminho do webhook real do processador:\ncredita/debita carteira, registra evento, dispara webhook\nde saída ao seller e enfileira o e-mail de compra. Ideal pra\nfechar o ciclo de integração sem abrir banco.\n\nCobrança precisa estar em estado não-terminal\n(`pending` ou `authorized`). Charges já finalizadas devolvem\n`409 invalid_state`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 64,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to_status": {
                    "type": "string",
                    "enum": [
                      "paid",
                      "failed",
                      "refunded"
                    ],
                    "default": "paid",
                    "description": "Estado destino. `refunded` aplica `paid` antes do\nestorno (dois passos) pra manter ledger consistente.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Simulação aplicada — cobrança no estado solicitado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Charge"
                }
              }
            }
          },
          "400": {
            "description": "Body inválido ou `to_status` fora de [paid, failed, refunded]."
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "403": {
            "description": "`forbidden_in_live` — chamada feita com chave `sk_live_*`.\nSimulação só é permitida em sandbox.\n"
          },
          "404": {
            "description": "Cobrança não encontrada para o titular autenticado."
          },
          "409": {
            "description": "Cobrança em estado terminal ou transição inválida."
          }
        }
      }
    },
    "/subscriptions/{id}/cancel": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Cancelar assinatura",
        "description": "Cancela uma assinatura recorrente do operador titular. Operação\nconsumida pelo painel interno (não exposta como produto a\nterceiros).\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Assinatura cancelada."
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "404": {
            "description": "Assinatura não encontrada para o titular autenticado."
          }
        }
      }
    },
    "/me": {
      "get": {
        "tags": [
          "Profile"
        ],
        "summary": "Identificar a chave atual",
        "description": "Retorna o operador titular da chave + metadados da própria chave.\nNão move dinheiro — pode ser chamado sem medo. Recomendado como\nprimeiro request na integração: confirma que o `Authorization`\nestá bem montado e que a chave tem pelo menos `profile:read` no\nscope.\n",
        "responses": {
          "200": {
            "description": "Identidade e chave OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        },
                        "tier": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string"
                        }
                      }
                    },
                    "apiKey": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "prefix": {
                          "type": "string",
                          "example": "sk_test_"
                        },
                        "last4": {
                          "type": "string",
                          "example": "9f3a"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "ok": {
                    "value": {
                      "user": {
                        "id": "01HW8K5T...",
                        "email": "operador@exemplo.com",
                        "tier": "growth",
                        "role": "user"
                      },
                      "apiKey": {
                        "id": "01HW8K6Z...",
                        "prefix": "sk_test_",
                        "last4": "9f3a"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token ausente ou inválido."
          },
          "403": {
            "description": "Chave válida mas sem scope `profile:read`."
          }
        }
      }
    }
  }
}