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
          nullable: true
          properties:
            brcode: { type: string, description: copia-e-cola EMV }
            qr_code_url: { type: string }
            txid: { type: string }
            e2e_id: { type: string, nullable: true }
        boleto:
          type: object
          nullable: true
          description: "presente quando method=boleto"
          properties:
            line: { type: string, description: linha digitável }
            barcode: { type: string }
            pdf_url: { type: string, nullable: true }
            due_date: { type: string, format: date }
        splits:
          type: array
          nullable: true
          items:
            type: object
            properties:
              recipient: { type: string }
              amount: { type: integer }
              description: { type: string, nullable: true }
              status: { type: string, enum: [pending, credited, reversed] }
        created_at: { type: string, format: date-time }
    ChargeCreate:
      type: object
      required: [amount]
      properties:
        amount: { $ref: '#/components/schemas/Money' }
        method: { type: string, enum: [pix, boleto], default: pix, description: "pix (padrão) ou boleto" }
        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 }
        splits:
          type: array
          description: "Split de pagamento — divide o líquido (após tarifa e reserva) com outros recebedores da plataforma (marketplace/afiliados/co-produção). A soma não pode exceder o líquido."
          items:
            type: object
            required: [recipient]
            properties:
              recipient: { type: string, description: "public_id (mch_...) do recebedor (outro merchant ativo)" }
              amount: { type: integer, description: "valor em centavos — use amount OU percentage" }
              percentage: { type: number, description: "percentual do bruto (0..100) — use amount OU percentage" }
              description: { type: string }
    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] }
    Plan:
      type: object
      properties:
        id: { type: string, examples: [plan_018f...] }
        object: { type: string, examples: [plan] }
        name: { type: string }
        amount: { $ref: '#/components/schemas/Money' }
        interval: { type: string, enum: [day, week, month, year] }
        interval_count: { type: integer }
        method: { type: string, enum: [pix, card, boleto], description: "somente pix habilitado hoje" }
        status: { type: string, enum: [active, archived] }
    Subscription:
      type: object
      properties:
        id: { type: string, examples: [sub_018f...] }
        object: { type: string, examples: [subscription] }
        status: { type: string, enum: [active, past_due, canceled] }
        amount: { $ref: '#/components/schemas/Money' }
        interval: { type: string }
        interval_count: { type: integer }
        method: { type: string }
        customer: { type: object, properties: { name: { type: string }, document: { type: string }, email: { type: string } } }
        cycles_billed: { type: integer }
        current_period_end: { type: string, format: date-time, nullable: true }
        next_charge_at: { type: string, format: date-time, nullable: true }
        first_charge: { $ref: '#/components/schemas/Charge', description: "presente só na criação (cobrança do 1º ciclo)" }
    Product:
      type: object
      properties:
        id: { type: string, examples: [prod_018f...] }
        object: { type: string, examples: [product] }
        name: { type: string }
        tagline: { type: string, nullable: true }
        description: { type: string, nullable: true }
        price: { $ref: '#/components/schemas/Money' }
        type: { type: string, enum: [digital, physical, service] }
        image_url: { type: string, nullable: true }
        cta_label: { type: string }
        support_email: { type: string, nullable: true }
        status: { type: string, enum: [draft, active, archived] }
        members_enabled: { type: boolean, description: "área de membros ativa (entrega de conteúdo pós-pagamento)" }
        sales_count: { type: integer, description: "vendas confirmadas (pagas)" }
        sales_url: { type: string, description: "URL pública da página de vendas (/p/{slug})" }
        slug: { type: string }
    AffiliateProgram:
      type: object
      properties:
        id: { type: string, examples: [afp_018f...] }
        object: { type: string, examples: [affiliate_program] }
        product: { type: string, description: "id do produto (prod_...)" }
        commission_bps: { type: integer, description: "comissão em basis points (3000 = 30%)" }
        commission_pct: { type: number }
        auto_approve: { type: boolean }
        status: { type: string, enum: [open, closed] }
    Affiliate:
      type: object
      properties:
        id: { type: string, examples: [aff_018f...] }
        object: { type: string, examples: [affiliate] }
        product: { type: string }
        product_name: { type: string, nullable: true }
        ref_code: { type: string, description: "código do link de afiliado" }
        link: { type: string, description: "/p/{slug}?ref={ref_code}" }
        commission_bps: { type: integer, nullable: true }
        status: { type: string, enum: [pending, approved, blocked] }
        sales_count: { type: integer }
    OrderBump:
      type: object
      properties:
        id: { type: string, examples: [obmp_018f...] }
        object: { type: string, examples: [order_bump] }
        bump_product: { type: string, description: "id do produto ofertado como adicional (prod_...)" }
        bump_name: { type: string, nullable: true }
        headline: { type: string, nullable: true, description: "chamada exibida no checkout" }
        price_cents: { type: integer, description: "preço efetivo do adicional (especial ou o do produto)" }
        custom_price: { type: boolean, description: "true se há preço especial diferente do produto" }
        status: { type: string, enum: [active, inactive] }
    OrderBumpCreate:
      type: object
      required: [bump_product]
      properties:
        bump_product: { type: string, description: "id de outro produto seu (prod_...) a oferecer no checkout" }
        headline: { type: string, nullable: true, maxLength: 160 }
        price_cents: { type: integer, nullable: true, description: "preço especial do adicional; omitir = preço do produto" }
    CourseModule:
      type: object
      properties:
        id: { type: string, examples: [mod_018f...] }
        object: { type: string, examples: [course_module] }
        title: { type: string }
        lessons: { type: array, items: { $ref: '#/components/schemas/CourseLesson' } }
    CourseLesson:
      type: object
      properties:
        id: { type: string, examples: [les_018f...] }
        object: { type: string, examples: [course_lesson] }
        title: { type: string }
        content_type: { type: string, enum: [video, text] }
        video_url: { type: string, nullable: true }
        content: { type: string, nullable: true, description: "conteúdo em texto (quando content_type=text)" }
    TrackingPixel:
      type: object
      properties:
        id: { type: string, examples: [pxl_018f...] }
        object: { type: string, examples: [tracking_pixel] }
        platform: { type: string, enum: [meta, ga4, google_ads, tiktok] }
        pixel_id: { type: string, description: "ID do pixel/medição (ex.: 1234567890, G-XXXX, AW-123)" }
        label: { type: string, nullable: true, description: "rótulo da conversão (Google Ads)" }
        status: { type: string, enum: [active, inactive] }
    TrackingPixelCreate:
      type: object
      required: [platform, pixel_id]
      properties:
        platform: { type: string, enum: [meta, ga4, google_ads, tiktok] }
        pixel_id: { type: string }
        label: { type: string, description: "obrigatório para google_ads (rótulo da conversão)" }
    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 }
        splits: { type: array, nullable: true, description: "recebedores do split e valores creditados (quando houver)", items: { type: object } }
  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' } } } } }
  /plans:
    post:
      tags: [Subscriptions]
      summary: Criar plano de assinatura
      description: "Modelo de cobrança recorrente. Hoje apenas method=pix está habilitado (card/boleto retornam 400)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, amount, interval]
              properties:
                name: { type: string }
                amount: { $ref: '#/components/schemas/Money' }
                interval: { type: string, enum: [day, week, month, year] }
                interval_count: { type: integer, default: 1 }
                method: { type: string, enum: [pix], default: pix }
      responses: { '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/Plan' } } } } }
    get:
      tags: [Subscriptions]
      summary: Listar planos
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /subscriptions:
    post:
      tags: [Subscriptions]
      summary: Criar assinatura
      description: >
        Cria a assinatura e já gera a cobrança Pix do 1º ciclo (retornada em first_charge).
        A cada Pix pago a assinatura renova (current_period_end avança e o motor gera o próximo
        ciclo no vencimento). Em caso de não pagamento há até 3 tentativas (past_due) antes do cancelamento.
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan, customer]
              properties:
                plan: { type: string, description: "id do plano (plan_...)" }
                customer: { type: object, required: [name, document], properties: { name: { type: string }, document: { type: string, description: "CPF/CNPJ só dígitos" }, email: { type: string } } }
      responses: { '201': { description: Criada, content: { application/json: { schema: { $ref: '#/components/schemas/Subscription' } } } } }
    get:
      tags: [Subscriptions]
      summary: Listar assinaturas
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /subscriptions/{id}:
    get:
      tags: [Subscriptions]
      summary: Consultar assinatura
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Subscription' } } } } }
  /subscriptions/{id}/cancel:
    post:
      tags: [Subscriptions]
      summary: Cancelar assinatura
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: Cancelada, content: { application/json: { schema: { $ref: '#/components/schemas/Subscription' } } } } }
  /products:
    post:
      tags: [Products]
      summary: Criar produto
      description: >
        Cria um produto com página de vendas pública própria em sales_url (/p/{slug}),
        no estilo Hotmart/Braip. Quem abre a página vê o produto e paga via Pix na hora.
        Requer que a função esteja habilitada (global no backoffice e para a conta); caso
        contrário retorna 403. Disponível também no painel (menu Produtos).
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, price]
              properties:
                name: { type: string }
                price: { $ref: '#/components/schemas/Money' }
                type: { type: string, enum: [digital, physical, service], default: digital }
                tagline: { type: string }
                description: { type: string }
                image_url: { type: string }
                cta_label: { type: string, default: "Comprar agora" }
                support_email: { type: string }
                status: { type: string, enum: [draft, active, archived], default: active }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/Product' } } } }
        '403': { description: "Função de Produtos desabilitada para a conta" }
    get:
      tags: [Products]
      summary: Listar produtos
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /products/{id}:
    get:
      tags: [Products]
      summary: Consultar produto
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Product' } } } } }
    post:
      tags: [Products]
      summary: Atualizar produto
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                price: { $ref: '#/components/schemas/Money' }
                type: { type: string, enum: [digital, physical, service] }
                tagline: { type: string }
                description: { type: string }
                image_url: { type: string }
                cta_label: { type: string }
                support_email: { type: string }
                status: { type: string, enum: [draft, active, archived] }
      responses: { '200': { description: Atualizado, content: { application/json: { schema: { $ref: '#/components/schemas/Product' } } } } }
  /products/{id}/order-bumps:
    post:
      tags: [Products]
      summary: Adicionar order bump ao produto
      description: >
        Anexa outro produto seu como oferta adicional (order bump) na página de vendas
        deste produto. No checkout o comprador marca o adicional e tudo entra na mesma
        cobrança Pix. Máx. 3 order bumps por produto.
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/OrderBumpCreate' } } } }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/OrderBump' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
    get:
      tags: [Products]
      summary: Listar order bumps do produto
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /order-bumps/{id}:
    delete:
      tags: [Products]
      summary: Remover order bump
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: Removido } }
  /products/{id}/content:
    get:
      tags: [Products]
      summary: Conteúdo da área de membros (módulos -> aulas)
      description: >
        Árvore de conteúdo do produto. A área de membros é entregue ao comprador
        após o pagamento (link com token em /members/{token}); habilite-a em
        members_enabled no produto.
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /products/{id}/modules:
    post:
      tags: [Products]
      summary: Criar módulo da área de membros
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [title], properties: { title: { type: string } } } } } }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/CourseModule' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /modules/{id}:
    delete:
      tags: [Products]
      summary: Remover módulo (e suas aulas)
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: Removido } }
  /modules/{id}/lessons:
    post:
      tags: [Products]
      summary: Criar aula em um módulo
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, content_type]
              properties:
                title: { type: string }
                content_type: { type: string, enum: [video, text] }
                video_url: { type: string, description: "obrigatório se content_type=video (YouTube/Vimeo/MP4)" }
                content: { type: string, description: "obrigatório se content_type=text" }
      responses:
        '201': { description: Criada, content: { application/json: { schema: { $ref: '#/components/schemas/CourseLesson' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /lessons/{id}:
    delete:
      tags: [Products]
      summary: Remover aula
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: Removida } }
  /products/{id}/pixels:
    post:
      tags: [Products]
      summary: Adicionar pixel de conversão ao produto
      description: >
        Cadastra um pixel (Meta/GA4/Google Ads/TikTok) que dispara PageView e
        InitiateCheckout na página de vendas e Purchase no pagamento confirmado.
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/TrackingPixelCreate' } } } }
      responses:
        '201': { description: Criado, content: { application/json: { schema: { $ref: '#/components/schemas/TrackingPixel' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
    get:
      tags: [Products]
      summary: Listar pixels do produto
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /products/{id}/attribution:
    get:
      tags: [Products]
      summary: Relatório de origem das vendas (UTM)
      description: "Vendas pagas agrupadas por utm_source/utm_campaign, com contagem e receita."
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /pixels/{id}:
    delete:
      tags: [Products]
      summary: Remover pixel
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: Removido } }
  /products/{id}/affiliate-program:
    post:
      tags: [Affiliates]
      summary: Habilitar/atualizar programa de afiliados do produto
      description: >
        O produtor habilita afiliados para o produto, definindo a comissão (%). Afiliados
        aprovados promovem com /p/{slug}?ref={ref_code} e a comissão é paga via split no
        recebimento (parte do líquido do produtor vai ao merchant afiliado). Máx. 90%.
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                commission_pct: { type: number, description: "ex.: 30 = 30% (ou use commission_bps)" }
                commission_bps: { type: integer }
                auto_approve: { type: boolean, default: true }
                status: { type: string, enum: [open, closed], default: open }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AffiliateProgram' } } } } }
    get:
      tags: [Affiliates]
      summary: Consultar programa de afiliados do produto
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AffiliateProgram' } } } } }
  /products/{id}/affiliates:
    get:
      tags: [Affiliates]
      summary: Listar afiliados do produto (produtor)
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/List' } } } } }
  /affiliates/{id}/status:
    post:
      tags: [Affiliates]
      summary: Aprovar/bloquear afiliado (produtor)
      parameters: [ { name: id, in: path, required: true, schema: { type: string } } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [status], properties: { status: { type: string, enum: [approved, blocked, pending] } } }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Affiliate' } } } } }
  /affiliations:
    post:
      tags: [Affiliates]
      summary: Afiliar-se a um produto (afiliado)
      description: "Cria a afiliação a um produto de outro produtor. Retorna o ref_code e o link. Se o programa aprovar automaticamente, já vem aprovado."
      parameters:
        - { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [product], properties: { product: { type: string, description: "id do produto (prod_...)" } } }
      responses: { '201': { description: Criada, content: { application/json: { schema: { $ref: '#/components/schemas/Affiliate' } } } } }
    get:
      tags: [Affiliates]
      summary: Listar minhas afiliações (afiliado)
      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 } }
  subscription.renewed:
    post:
      security: []
      summary: Assinatura renovada após Pix pago (current_period_end avançou)
      description: "Família de eventos de assinatura: subscription.created, subscription.charge_created (novo ciclo gerado), subscription.renewed (ciclo pago), subscription.payment_failed (tentativa falhou — past_due) e subscription.canceled."
      requestBody: { content: { application/json: { schema: { $ref: '#/components/schemas/Subscription' } } } }
      responses: { '200': { description: Reconhecido } }
