openapi: 3.1.0
info:
  title: VolgaPay Gateway API
  version: 1.0.0
  description: |
    API de pagamentos com PIX, cartão, cripto e links, ledger de partidas
    dobradas, idempotência, payouts com aprovação e webhooks assinados.
    Valores monetários são inteiros na menor unidade (centavos).

    Segurança de credenciais: chaves são criadas apenas no painel, exigem
    2FA (TOTP) ativo e podem ser restritas a IPs/CIDRs (`allowed_ips`).
    O secret é exibido uma única vez; para obter outro, gere uma nova chave.

servers:
  - url: https://volgapay.com
    description: Produção
  - url: http://127.0.0.1:8091
    description: Desenvolvimento

security:
  - bearerAuth: []

tags:
  - name: Payments
  - name: Payouts
  - name: Customers
  - name: Webhooks
  - name: Links
  - name: Admin
  - name: Health

paths:
  /health:
    get:
      tags: [Health]
      security: []
      responses:
        "200": { description: OK }
  /ready:
    get:
      tags: [Health]
      security: []
      responses:
        "200": { description: Ready }

  /v1/payments:
    post:
      tags: [Payments]
      summary: Cria pagamento (cartão ou PIX)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentCreate"
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
    get:
      tags: [Payments]
      summary: Lista pagamentos
      parameters:
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 20 } }
        - { name: before, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Payment" }

  /v1/payments/{id}:
    get:
      tags: [Payments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }
        "404": { $ref: "#/components/responses/Error" }

  /v1/payments/{id}/capture:
    post:
      tags: [Payments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                amount: { type: integer, minimum: 1 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }

  /v1/payments/{id}/cancel:
    post:
      tags: [Payments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }

  /v1/payments/{id}/refund:
    post:
      tags: [Payments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: integer, minimum: 1 }
                reason: { type: string, maxLength: 255 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }

  /v1/payment-methods:
    post:
      tags: [Payments]
      summary: Tokeniza cartão (PAN/CVV não são persistidos)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [card]
              properties:
                card: { $ref: "#/components/schemas/CardInput" }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentMethod" }
  /v1/payment-methods/{id}:
    get:
      tags: [Payments]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentMethod" }

  /v1/customers:
    post:
      tags: [Customers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                external_id: { type: string }
                name: { type: string }
                email: { type: string, format: email }
                metadata: { type: object }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
  /v1/customers/{id}:
    get:
      tags: [Customers]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }

  /v1/balance:
    get:
      tags: [Payments]
      parameters:
        - { name: currency, in: query, schema: { type: string, default: BRL } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: balance }
                  currency: { type: string }
                  available: { type: integer }
                  payout_hold: { type: integer }

  /v1/transactions:
    get:
      tags: [Payments]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  payments:
                    type: array
                    items: { $ref: "#/components/schemas/Payment" }
                  payouts:
                    type: array
                    items: { $ref: "#/components/schemas/Payout" }

  /v1/payouts:
    post:
      tags: [Payouts]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, pix_key_type, pix_key]
              properties:
                amount: { type: integer, minimum: 1 }
                currency: { type: string }
                pix_key_type: { type: string, enum: [cpf, cnpj, email, phone, random] }
                pix_key: { type: string }
                reason: { type: string }
                metadata: { type: object }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }
    get:
      tags: [Payouts]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Payout" }
  /v1/payouts/{id}:
    get:
      tags: [Payouts]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }

  /v1/kyc:
    post:
      tags: [Customers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documents]
              properties:
                documents: { type: object }
                metadata: { type: object }
      responses:
        "201": { description: Criado }
  /v1/kyc/{id}:
    get:
      tags: [Customers]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }

  /v1/webhook-endpoints:
    post:
      tags: [Webhooks]
      summary: Registra endpoint de webhook de saída
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string }
                  default: ["*"]
      responses:
        "201":
          description: Criado (secret exibido apenas uma vez)
    get:
      tags: [Webhooks]
      responses:
        "200": { description: OK }
  /v1/webhook-endpoints/{id}/deactivate:
    post:
      tags: [Webhooks]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/webhook-deliveries:
    get:
      tags: [Webhooks]
      responses:
        "200": { description: OK }

  /v1/webhooks/provider/{provider}:
    post:
      tags: [Webhooks]
      security: []
      summary: Webhook do PSP (HMAC obrigatório)
      parameters:
        - { name: provider, in: path, required: true, schema: { type: string } }
        - { name: X-Provider-Signature, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, type]
              properties:
                id: { type: string }
                type: { type: string }
                data: { type: object }
      responses:
        "200": { description: Processado ou duplicado }

  /v1/admin/payouts:
    get:
      tags: [Admin]
      parameters:
        - { name: status, in: query, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/admin/payouts/{id}/approve:
    post:
      tags: [Admin]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/admin/payouts/{id}/reject:
    post:
      tags: [Admin]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/admin/kyc:
    get:
      tags: [Admin]
      responses:
        "200": { description: OK }
  /v1/admin/merchants/{id}/kyc/approve:
    post:
      tags: [Admin]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/admin/merchants/{id}/kyc/reject:
    post:
      tags: [Admin]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }

  /v1/methods:
    get:
      tags: [Payments]
      summary: Lista métodos locais disponíveis
      responses:
        "200": { description: OK }
  /v1/payment-links:
    post:
      tags: [Links]
      summary: Cria link de pagamento (valor, moeda e métodos)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency]
              properties:
                amount: { type: integer, minimum: 1 }
                currency: { type: string, example: BRL }
                description: { type: string }
                methods:
                  type: array
                  description: "Inclui `volgapay` para QR/parcelamento na própria VolgaPay."
                  items: { type: string }
                max_installments: { type: integer, minimum: 1, maximum: 12, default: 12 }
                interest_bps: { type: integer, minimum: 0, maximum: 10000, default: 0 }
                expires_at: { type: string, nullable: true }
      responses:
        "201": { description: Criado }
    get:
      tags: [Links]
      summary: Lista links de pagamento
      responses:
        "200": { description: OK }
  /v1/payment-links/{id}/deactivate:
    post:
      tags: [Links]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: OK }

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Token vp_<key_id>_<secret>"
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 128 }
  responses:
    Error:
      description: Erro
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type: { type: string }
            code: { type: string }
            message: { type: string }
            details: { type: object }
    CardInput:
      type: object
      required: [number, exp_month, exp_year, cvv]
      properties:
        number: { type: string }
        exp_month: { type: integer, minimum: 1, maximum: 12 }
        exp_year: { type: integer }
        cvv: { type: string }
    PaymentCreate:
      type: object
      required: [amount, currency, method]
      properties:
        amount: { type: integer, minimum: 1 }
        currency: { type: string, example: MXN }
        country: { type: string, description: "ISO 3166-1 alpha-2. Quando omitido, é inferido pelo método (ex.: SPEI -> MX).", example: MX }
        method:
          type: string
          description: "Código retornado em GET /v1/methods (card, pix, crypto, spei, nequi, pse, yape, plin, oxxo, boleto, ach, sepa, ...)."
          example: spei
        description: { type: string }
        capture: { type: boolean, default: true }
        payment_method: { type: string }
        card: { $ref: "#/components/schemas/CardInput" }
        customer_id: { type: string }
        customer:
          type: object
          description: "Pagador. Para PIX, name, document (CPF/CNPJ válido) e phone com DDD são obrigatórios."
          properties:
            external_id: { type: string }
            name: { type: string, example: "Cliente Exemplo" }
            email: { type: string, format: email }
            document: { type: string, example: "529.982.247-25" }
            phone: { type: string, example: "+5511999990000" }
            country: { type: string, example: BR }
            metadata: { type: object }
        metadata: { type: object }
    Payment:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: payment }
        amount: { type: integer }
        fee: { type: integer }
        net: { type: integer }
        currency: { type: string }
        country: { type: string, nullable: true }
        method: { type: string }
        status:
          type: string
          enum: [created, requires_action, processing, authorized, succeeded, failed, canceled, refunded, partially_refunded, expired]
        provider: { type: string }
        provider_credential_id: { type: string, nullable: true }
        provider_reference: { type: string, nullable: true }
        instructions:
          type: array
          description: "Instruções para métodos locais (SPEI, Nequi, OXXO etc.)."
          items:
            type: object
            properties:
              label: { type: string }
              value: { type: string }
        refunded_amount: { type: integer }
        pix:
          type: object
          nullable: true
          properties:
            qr_code: { type: string }
            copia_e_cola: { type: string }
            expires_at: { type: string, nullable: true }
        failure: { type: object, nullable: true }
        metadata: { type: object, nullable: true }
        created_at: { type: string }
        updated_at: { type: string }
        paid_at: { type: string, nullable: true }
    ApiKey:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        key_id: { type: string }
        token: { type: string, description: "Retornado apenas na criação." }
        scopes:
          type: array
          items: { type: string }
        allowed_ips:
          type: array
          description: "IPv4, IPv6 e CIDR. Vazio = qualquer IP."
          items: { type: string }
        active: { type: boolean }
        last_used_at: { type: string, nullable: true }
        created_at: { type: string }
    PaymentMethod:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: payment_method }
        provider: { type: string }
        brand: { type: string }
        last4: { type: string }
        exp_month: { type: integer }
        exp_year: { type: integer }
    Customer:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: customer }
        external_id: { type: string }
        name: { type: string, nullable: true }
        email: { type: string, nullable: true }
        document: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        country: { type: string, nullable: true }
        metadata: { type: object, nullable: true }
    Payout:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: payout }
        amount: { type: integer }
        fee: { type: integer }
        currency: { type: string }
        status:
          type: string
          enum: [pending_approval, processing, paid, failed, rejected]
        pix_key_type: { type: string }
        pix_key_last4: { type: string }
        provider_reference: { type: string, nullable: true }
        failure: { type: object, nullable: true }
        requested_at: { type: string }
        approved_at: { type: string, nullable: true }
        paid_at: { type: string, nullable: true }
