openapi: 3.1.0
info:
  title: ExivaPay API
  version: "1.0.0"
  description: >
    API REST do ExivaPay — gateway/subadquirente Pix multi-tenant.
    Versionada em /v1. Auth server-to-server por chave secreta (Bearer sk_...).
    Idempotency-Key obrigatório em POSTs financeiros. Valores em centavos (inteiros).
    Toda resposta inclui X-Request-Id e X-API-Version.
  contact: { name: ExivaPay, url: https://exivapay.com }
servers:
  - url: https://sandbox.exivapay.com/v1
    description: "Sandbox — chaves sk_test, PSP simulador (sem dinheiro real), endpoints /simulations/* habilitados. Comece por aqui."
  - url: https://exivapay.com/v1
    description: "Produção — chaves sk_live, Pix real."
tags:
  - { name: Charges, description: Cobranças Pix }
  - { name: Refunds, description: Estornos }
  - { name: Payouts, description: Saques (Pix out) }
  - { name: Balance, description: Saldos e extrato }
  - { name: KYC, description: Verificação e limites }
  - { name: Webhooks, description: Endpoints de webhook do lojista }
  - { name: PaymentLinks, description: Links de pagamento }
  - { name: Sandbox, description: Simuladores de eventos do PSP }
  - { name: Admin, description: Backoffice (maker-checker, risco, conciliação) }
  - { name: Infra, description: Health e métricas }

security:
  - SecretKey: []

components:
  securitySchemes:
    SecretKey:
      type: http
      scheme: bearer
      description: "Authorization: Bearer sk_live_... | sk_test_..."
    AdminToken:
      type: apiKey
      in: header
      name: X-Admin-Token
      description: Token do backoffice (rotas /v1/admin/*).
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, maxLength: 255 }
      description: Mesma chave + payload diferente ⇒ 409. Reenvio idêntico ⇒ resposta cacheada.
    Cursor: { name: cursor, in: query, schema: { type: string } }
    Limit:  { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
  schemas:
    Money:
      type: integer
      description: Valor em centavos (BRL). Ex.= 10000 ⇒ R$100,00
      minimum: 1
      examples: [10000]
    Error:
      type: object
      required: [type, code, message, request_id]
      properties:
        type: { type: string, examples: [invalid_request_error] }
        code: { type: string, examples: [amount_too_small] }
        message: { type: string }
        param: { type: string, nullable: true }
        request_id: { type: string }
        doc_url: { type: string, nullable: true }
    Charge:
      type: object
      properties:
        id: { type: string, examples: [chg_018f...] }
        object: { type: string, examples: [charge] }
        status: { type: string, enum: [created, paid, expired, failed, under_review, partially_refunded, refunded, disputed] }
        amount: { $ref: '#/components/schemas/Money' }
        currency: { type: string, examples: [BRL] }
        method: { type: string, examples: [pix] }
        external_reference: { type: string, nullable: true }
        description: { type: string, nullable: true }
        checkout_url: { type: string, nullable: true }
        paid_amount: { type: integer, nullable: true }
        fees: { type: object, nullable: true }
        expires_at: { type: string, format: date-time }
        pix:
          type: object
          properties:
            brcode: { type: string, description: copia-e-cola EMV }
            qr_code_url: { type: string }
            txid: { type: string }
            e2e_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
    ChargeCreate:
      type: object
      required: [amount]
      properties:
        amount: { $ref: '#/components/schemas/Money' }
        method: { type: string, enum: [pix], default: pix }
        external_reference: { type: string }
        description: { type: string }
        expires_in: { type: integer, default: 3600, description: segundos até expirar }
        customer:
          type: object
          description: "Dados do pagador (opcional). 'document' (CPF/CNPJ, só dígitos) é recomendado; se omitido, a cobrança usa o documento da conta."
          properties:
            name: { type: string }
            document: { type: string, description: "CPF (11) ou CNPJ (14), apenas dígitos" }
            email: { type: string, format: email }
        metadata: { type: object, additionalProperties: true }
    Refund:
      type: object
      properties:
        id: { type: string, examples: [ref_018f...] }
        object: { type: string, examples: [refund] }
        charge_id: { type: string }
        amount: { $ref: '#/components/schemas/Money' }
        status: { type: string, enum: [requested, processing, done, failed, rejected] }
        reason: { type: string, nullable: true }
        e2e_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
    Payout:
      type: object
      properties:
        id: { type: string, examples: [pay_018f...] }
        object: { type: string, examples: [payout] }
        amount: { $ref: '#/components/schemas/Money' }
        fee: { $ref: '#/components/schemas/Money' }
        pix_key: { type: string }
        kind: { type: string, enum: [self, third_party], description: "self = própria conta; third_party = transferência para terceiro" }
        pix_key_owner_document: { type: string, nullable: true }
        pix_key_owner_name: { type: string, nullable: true }
        description: { type: string, nullable: true }
        status: { type: string, enum: [requested, requires_approval, approved, processing, paid, failed, rejected] }
        e2e_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
    Balance:
      type: object
      properties:
        object: { type: string, examples: [balance] }
        available: { $ref: '#/components/schemas/Money' }
        pending: { $ref: '#/components/schemas/Money' }
        reserved: { $ref: '#/components/schemas/Money' }
        in_transit: { $ref: '#/components/schemas/Money' }
        currency: { type: string, examples: [BRL] }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string }
        object: { type: string, examples: [webhook_endpoint] }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string }, nullable: true }
        status: { type: string, enum: [active, disabled] }
        secret: { type: string, description: exibido só na criação }
        secret_last4: { type: string }
    PaymentLink:
      type: object
      properties:
        id: { type: string }
        object: { type: string, examples: [payment_link] }
        url: { type: string }
        slug: { type: string }
        amount: { $ref: '#/components/schemas/Money' }
        description: { type: string, nullable: true }
        status: { type: string, enum: [active, disabled] }
    List:
      type: object
      properties:
        object: { type: string, examples: [list] }
        data: { type: array, items: { type: object } }
        has_more: { type: boolean }
        next_cursor: { type: string, nullable: true }
    WebhookChargeEvent:
      type: object
      description: Corpo (raw) enviado pela ExivaPay ao endpoint do lojista nos eventos de cobrança.
      properties:
        id: { type: string, description: id da cobrança, examples: [chg_018f...] }
        status: { type: string, enum: [paid, expired, under_review, refunded, partially_refunded, disputed] }
        amount: { $ref: '#/components/schemas/Money', description: valor cobrado (centavos) }
        paid_amount: { type: integer, nullable: true, description: valor efetivamente pago }
        e2e_id: { type: string, nullable: true, description: identificador Pix do BACEN }
        external_reference: { type: string, nullable: true, description: sua referência de pedido enviada na criação }
        metadata: { type: object, nullable: true, additionalProperties: true, description: metadata enviada na criação }
  responses:
    Conflict: { description: Conflito de idempotência, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    BadRequest: { description: Requisição inválida, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Unauthorized: { description: Credencial ausente/inválida, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

paths:
  /charges:
    post:
      tags: [Charges]
      summary: Criar cobrança Pix
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ChargeCreate' } } } }
      responses:
        '201': { description: Criada, content: { application/json: { schema: { $ref: '#/components/schemas/Charge' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
    get:
      tags: [Charges]
      summary: Listar cobranças
      parameters:
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
        - { name: status, in: query, schema: { type: string } }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /charges/{id}:
    get:
      tags: [Charges]
      summary: Consultar cobrança
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Charge' } } } }
        '404': { description: Não encontrada }
  /charges/{id}/refunds:
    post:
      tags: [Refunds]
      summary: Estornar (total/parcial)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        content: { application/json: { schema: { type: object, properties: { amount: { $ref: '#/components/schemas/Money', description: omitir = total }, reason: { type: string } } } } }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/Refund' } } } }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { description: Excede valor pago }
  /refunds/{id}:
    get:
      tags: [Refunds]
      summary: Consultar estorno
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Refund' } } } } }
  /payouts:
    post:
      tags: [Payouts]
      summary: Solicitar saque (Pix out) ou transferência para terceiro
      description: >
        Saque para a própria conta (padrão) ou **transferência para terceiro** (ex.: pagar
        freelancers) quando `pix_key_owner_document` difere do documento da conta — isso
        exige a permissão habilitada no backoffice. **Execução manual:** a solicitação entra
        na fila do operador da ExivaPay (`status: requires_approval`); o Pix é feito e
        confirmado manualmente. Pode ser solicitado via API ou pelo painel.
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, pix_key]
              properties:
                amount: { $ref: '#/components/schemas/Money' }
                pix_key: { type: string, description: chave Pix de destino }
                pix_key_owner_document: { type: string, description: "CPF/CNPJ do TITULAR da chave. Se diferente do documento da conta = transferência para terceiro (requer permissão)." }
                pix_key_owner_name: { type: string, description: "nome do recebedor (recomendado em transferência para terceiro)" }
                description: { type: string, description: "finalidade (ex.: Pagamento projeto X)" }
      responses:
        '201': { description: Solicitado — em modo manual entra como requires_approval, content: { application/json: { schema: { $ref: '#/components/schemas/Payout' } } } }
        '402': { description: Saldo insuficiente }
        '403': { description: Bloqueado por risco }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { description: "Limite/KYC ou transferência para terceiro não permitida (kyc_upgrade_required, pix_key_owner_mismatch)" }
    get:
      tags: [Payouts]
      summary: Listar saques
      parameters: [ { $ref: '#/components/parameters/Limit' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /payouts/{id}:
    get:
      tags: [Payouts]
      summary: Consultar saque
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Payout' } } } } }
  /balance:
    get:
      tags: [Balance]
      summary: Saldos do merchant
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Balance' } } } } }
  /transactions:
    get:
      tags: [Balance]
      summary: Extrato / ledger
      parameters: [ { $ref: '#/components/parameters/Cursor' }, { $ref: '#/components/parameters/Limit' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /kyc:
    get:
      tags: [KYC]
      summary: Nível e limites de KYC
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, examples: [kyc] }
                  level: { type: integer, examples: [1] }
                  payout_max: { type: integer, description: teto de saque do nível (centavos) }
  /kyc/verifications:
    post:
      tags: [KYC]
      summary: Solicitar upgrade de nível
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [level]
              properties:
                level: { type: integer, enum: [1, 2, 3] }
                documents:
                  type: array
                  items: { type: object, properties: { type: { type: string }, file_ref: { type: string } } }
      responses: { '201': { description: Pedido criado } }
  /webhook-endpoints:
    post:
      tags: [Webhooks]
      summary: Criar endpoint de webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string }, description: omitir = todos }
      responses: { '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookEndpoint' } } } } }
    get:
      tags: [Webhooks]
      summary: Listar endpoints
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /webhook-endpoints/{id}:
    get: { tags: [Webhooks], summary: Consultar, parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '200': { description: OK } } }
    patch:
      tags: [Webhooks]
      summary: Atualizar
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { content: { application/json: { schema: { type: object, properties: { url: { type: string }, status: { type: string }, events: { type: array, items: { type: string } } } } } } }
      responses: { '200': { description: OK } }
    delete: { tags: [Webhooks], summary: Remover, parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '204': { description: Removido } } }
  /payment-links:
    post:
      tags: [PaymentLinks]
      summary: Criar link de pagamento
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { $ref: '#/components/schemas/Money' }
                description: { type: string }
                expires_in: { type: integer }
                max_uses: { type: integer }
      responses: { '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/PaymentLink' } } } } }
    get:
      tags: [PaymentLinks]
      summary: Listar links
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /simulations/charge-paid:
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simular pagamento"
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [charge_id], properties: { charge_id: { type: string }, amount: { $ref: '#/components/schemas/Money', description: para simular divergência } } } } } }
      responses: { '200': { description: Evento simulado } }
  /simulations/charge-expired:
    post: { tags: [Sandbox], summary: "[Sandbox] Simular expiração", requestBody: { content: { application/json: { schema: { type: object, properties: { charge_id: { type: string } } } } } }, responses: { '200': { description: OK } } }
  /simulations/payout-paid:
    post: { tags: [Sandbox], summary: "[Sandbox] Simular saque pago", requestBody: { content: { application/json: { schema: { type: object, properties: { payout_id: { type: string } } } } } }, responses: { '200': { description: OK } } }
  /simulations/payout-failed:
    post: { tags: [Sandbox], summary: "[Sandbox] Simular saque falho", requestBody: { content: { application/json: { schema: { type: object, properties: { payout_id: { type: string } } } } } }, responses: { '200': { description: OK } } }
  /admin/payouts/{id}/approve:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Aprovar saque (maker-checker), parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '200': { description: OK } } }
  /admin/payouts/{id}/reject:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Rejeitar saque, parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], requestBody: { content: { application/json: { schema: { type: object, properties: { reason: { type: string } } } } } }, responses: { '200': { description: OK } } }
  /admin/disputes:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Abrir disputa (Pix MED), requestBody: { required: true, content: { application/json: { schema: { type: object, required: [charge_id], properties: { charge_id: { type: string }, amount: { $ref: '#/components/schemas/Money' }, reason: { type: string } } } } } }, responses: { '201': { description: Aberta } } }
  /admin/disputes/{id}/accept:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Aceitar disputa (devolve), parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '200': { description: OK } } }
  /admin/disputes/{id}/reject:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Rejeitar disputa (libera hold), parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '200': { description: OK } } }
  /admin/reconciliation/run:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Rodar conciliação, responses: { '200': { description: OK } } }
  /admin/blocklist:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Adicionar à blocklist, requestBody: { required: true, content: { application/json: { schema: { type: object, required: [kind, value], properties: { kind: { type: string, enum: [document, pix_key, ip, device] }, value: { type: string }, reason: { type: string } } } } } }, responses: { '201': { description: Criado } } }
  /admin/kyc/{id}/level:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: Definir nível de KYC do merchant, parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [level], properties: { level: { type: integer } } } } } }, responses: { '200': { description: OK } } }
  /admin/merchants/{id}/payout-policy:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: "Liberar/bloquear transferência para terceiros do merchant", parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], requestBody: { required: true, content: { application/json: { schema: { type: object, required: [allow_third_party_payouts], properties: { allow_third_party_payouts: { type: boolean } } } } } }, responses: { '200': { description: OK } } }
  /admin/customers/{id}/erase:
    post: { tags: [Admin], security: [ { AdminToken: [] } ], summary: "LGPD: anonimizar pagador", parameters: [ { name: id, in: path, required: true, schema: { type: string } } ], responses: { '200': { description: OK } } }

# Webhooks de SAÍDA (ExivaPay -> lojista). O lojista registra a URL em /webhook-endpoints
# e recebe o "secret" (whsec_...). Cada entrega traz os headers Exiva-Event, Exiva-Event-Id
# e Exiva-Signature. Verifique a assinatura HMAC-SHA256 de "<t>.<corpo_bruto>" com o whsec,
# rejeite se |agora-t|>300s, responda 2xx rápido e trate de forma idempotente (Exiva-Event-Id).
webhooks:
  charge.paid:
    post:
      security: []
      summary: Pix pago (cobrança confirmada)
      description: >
        Enviado ao endpoint registrado quando uma cobrança é paga. Valide a assinatura,
        responda 2xx rápido e processe de forma assíncrona/idempotente. Falhas são
        reentregues com backoff.
      parameters:
        - { name: Exiva-Event, in: header, schema: { type: string }, description: "tipo do evento (ex.: charge.paid)" }
        - { name: Exiva-Event-Id, in: header, schema: { type: string }, description: "id único do evento — use para deduplicar reentregas" }
        - { name: Exiva-Signature, in: header, schema: { type: string }, description: 't=<unix_ts>,v1=<hmac_sha256("<ts>.<corpo_bruto>", whsec)>' }
      requestBody:
        content: { application/json: { schema: { $ref: '#/components/schemas/WebhookChargeEvent' } } }
      responses:
        '200': { description: Reconhecido (qualquer 2xx confirma o recebimento) }
  charge.expired:
    post:
      security: []
      summary: Cobrança expirada sem pagamento
      parameters:
        - { name: Exiva-Signature, in: header, schema: { type: string } }
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/WebhookChargeEvent' } } } }
      responses: { '200': { description: Reconhecido } }
  charge.under_review:
    post:
      security: []
      summary: Pago a menor — NÃO liberar; exige revisão manual
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/WebhookChargeEvent' } } } }
      responses: { '200': { description: Reconhecido } }
  charge.refunded:
    post:
      security: []
      summary: Estorno total (e charge.partially_refunded para parcial)
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/WebhookChargeEvent' } } } }
      responses: { '200': { description: Reconhecido } }
