openapi: 3.0.3
info:
  title: LiquiBR API
  version: 1.3.0
  description: >-
    API REST para gerar cobranças PIX e receber USDT na sua carteira automaticamente.
    Autenticação via API key (Bearer ou X-API-Key); webhooks assinados via HMAC-SHA256
    com proteção anti-replay por timestamp.
  contact:
    name: LiquiBR Suporte
    url: https://liquibr.com/support
    email: suporte@liquibr.com

servers:
  - url: https://liquibr.com/v1
    description: Produção

security:
  - BearerAuth: []

tags:
  - name: Conta
    description: Informações da conta autenticada
  - name: Cobranças
    description: Criar, consultar, listar e cancelar cobranças PIX
  - name: Conversões USDT
    description: Histórico de conversões BRL→USDT enviadas on-chain
  - name: Webhooks
    description: Auditoria e testes de webhooks outbound

paths:
  /account:
    get:
      summary: Info da conta
      tags: [Conta]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Account' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /balance:
    get:
      summary: Saldo bruto e líquido + status MED
      tags: [Conta]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Balance' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /charges:
    post:
      summary: Cria cobrança PIX
      description: >-
        Gera QR code PIX. Aceita `webhook_url` inline pra receber callback assinado.

        **Idempotência:** se informar `external_id` OU header `Idempotency-Key`, chamadas
        repetidas nas próximas 24h com mesmo `amount` retornam a charge existente
        (HTTP 200 + header `Idempotent-Replay: true`). Amount diferente → 409 idempotency_conflict.
      tags: [Cobranças]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, maxLength: 120 }
          description: Alternativa ao external_id no body. Janela 24h. external_id do body tem prioridade.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateChargeRequest' }
      responses:
        '200':
          description: Resposta idempotente (charge já existia nas últimas 24h)
          headers:
            Idempotent-Replay:
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Charge' }
        '201':
          description: Cobrança criada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Charge' }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '429': { $ref: '#/components/responses/RateLimit' }
        '502': { $ref: '#/components/responses/PspError' }
    get:
      summary: Lista cobranças
      tags: [Cobranças]
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [pending, paid, expired, failed, refunded] }
        - name: date_from
          in: query
          schema: { type: string, format: date }
          description: Filtra created_at >= esta data (YYYY-MM-DD). Inclusive.
        - name: date_to
          in: query
          schema: { type: string, format: date }
          description: Filtra created_at <= fim deste dia. Inclusive.
        - name: payer_document
          in: query
          schema: { type: string }
          description: CPF/CNPJ do pagador (formatado ou só dígitos)
        - name: customer_email
          in: query
          schema: { type: string }
          description: Email do client associado (exato, case-insensitive)
        - name: external_id
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:   { type: array, items: { $ref: '#/components/schemas/ChargeListItem' } }
                  total:  { type: integer }
                  limit:  { type: integer }
                  offset: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /charges/{id}:
    get:
      summary: Consulta cobrança por id ou external_id
      tags: [Cobranças]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: ID retornado pelo LiquiBR OU external_id que você passou na criação.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Charge' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      summary: Cancela cobrança pendente
      description: >-
        Cancela explicitamente uma charge ainda `pending`. Charge vai pra `refunded`.
        Só funciona em pending — pagas precisam de estorno (em roadmap).
      tags: [Cobranças]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Cancelada
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:           { type: string }
                  status:       { type: string, enum: [refunded] }
                  cancelled_at: { type: string, format: date-time }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Charge não está em pending (já paga/expirada/cancelada)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /charges/{id}/refund:
    post:
      summary: Estorno (NÃO IMPLEMENTADO)
      description: >-
        Reservado pra estorno de charge paga. Hoje retorna 501. Estornos devem ser
        feitos via suporte. Endpoint existe pra integradores não refatorar cliente
        quando for liberado.
      tags: [Cobranças]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '501':
          description: Estornos via API não disponíveis ainda
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:                   { type: string, enum: [not_implemented] }
                  message:                 { type: string }
                  support:
                    type: object
                    properties:
                      email:      { type: string }
                      ticket_url: { type: string }
                  charge_reference:        { type: string }
                  estimated_response_time: { type: string, example: '2 horas úteis' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /transactions:
    get:
      summary: Lista conversões USDT
      tags: [Conversões USDT]
      parameters:
        - { name: status,      in: query, schema: { type: string, enum: [sent, confirmed, pending] } }
        - { name: charge_id,   in: query, schema: { type: string } }
        - { name: external_id, in: query, schema: { type: string } }
        - { name: network,     in: query, schema: { type: string, enum: [TRON, POLYGON, BSC] } }
        - { name: limit,       in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: offset,      in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:   { type: array, items: { $ref: '#/components/schemas/Transaction' } }
                  total:  { type: integer }
                  limit:  { type: integer }
                  offset: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /transactions/{id}:
    get:
      summary: Consulta uma conversão USDT
      tags: [Conversões USDT]
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Transaction' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /webhooks/deliveries:
    get:
      summary: Auditoria de disparos de webhook
      description: Lista as últimas tentativas. Útil pra debugar integrações.
      tags: [Webhooks]
      parameters:
        - { name: event,     in: query, schema: { type: string, enum: [pix.received, pix.expired, pix.med, usdt.sent] } }
        - { name: ok,        in: query, schema: { type: boolean }, description: 'true = só 2xx; false = só falhas' }
        - { name: charge_id, in: query, schema: { type: string } }
        - { name: limit,     in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: offset,    in: query, schema: { type: integer, default: 0 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:   { type: array, items: { $ref: '#/components/schemas/WebhookDelivery' } }
                  total:  { type: integer }
                  limit:  { type: integer }
                  offset: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /webhooks/test:
    post:
      summary: Dispara webhook de teste
      description: Envia payload de teste com assinatura HMAC válida pra validar sua integração antes de gerar charges reais.
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [webhook_url]
              properties:
                webhook_url: { type: string, format: uri }
                event_type:
                  type: string
                  enum: [pix.received, pix.expired, pix.med, usdt.sent]
                  default: pix.received
      responses:
        '200':
          description: Resultado do disparo
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:              { type: boolean }
                  status_code:     { type: integer }
                  duration_ms:     { type: integer }
                  error:           { type: string, nullable: true }
                  signature_sent:  { type: string }
                  timestamp_sent:  { type: string }
                  payload_sent:    { type: object }
        '400': { $ref: '#/components/responses/ValidationError' }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  schemas:
    Account:
      type: object
      properties:
        id:    { type: integer }
        name:  { type: string }
        email: { type: string, format: email }
        role:  { type: string }
        usdt_wallet:
          type: object
          properties:
            address:    { type: string,  nullable: true }
            network:    { type: string,  nullable: true, enum: [TRON, POLYGON, BSC] }
            configured: { type: boolean }

    Balance:
      type: object
      properties:
        gross_received_brl: { type: number, example: 12483.50 }
        pending_brl:        { type: number, example: 347.00 }
        sent_usdt_brl:      { type: number, example: 11250.00 }
        total_fees_brl:     { type: number, example: 374.50 }
        net_brl:            { type: number, example: 12109.00 }
        med:
          type: object
          properties:
            debt_brl:            { type: number, example: 0 }
            block_threshold_brl: { type: number, example: 200 }
            account_blocked:     { type: boolean }
        account_status:       { type: string, enum: [active, suspended, blocked_med] }

    CreateChargeRequest:
      type: object
      required: [amount]
      properties:
        amount:
          type: number
          minimum: 1
          maximum: 100000
          example: 49.90
          description: Valor em reais. Limite per-conta default R$ 500 — ultrapassar = 400 amount_above_limit.
        expires_in_minutes:
          type: integer
          minimum: 5
          maximum: 1440
          default: 30
          description: Default 30. Valores abaixo de 30 são silenciosamente elevados.
        external_id:
          type: string
          maxLength: 120
          description: ID do seu sistema. Chave de idempotência (janela 24h).
        description: { type: string, maxLength: 255 }
        webhook_url:
          type: string
          format: uri
          maxLength: 500
          description: Recebe pix.received, pix.expired, pix.med e usdt.sent.
        customer:
          type: object
          properties:
            name:     { type: string }
            email:    { type: string, format: email }
            document: { type: string }

    Charge:
      type: object
      properties:
        id:                  { type: string }
        external_id:         { type: string, nullable: true }
        status:              { type: string, enum: [pending, paid, expired, failed, refunded] }
        amount:              { type: number }
        amount_cents:        { type: integer }
        idempotent_replay:   { type: boolean, description: 'Presente em respostas 200 (replay).' }
        fees:
          type: object
          properties:
            platform_fee_pct:   { type: number }
            percentage_fee_brl: { type: number }
            fixed_fee_brl:      { type: number }
            total_fee_brl:      { type: number }
            net_brl:            { type: number }
        pix:
          type: object
          properties:
            qr_code:            { type: string }
            qr_code_base64:     { type: string, nullable: true }
            qr_code_image_url:  { type: string, nullable: true }
            qr_code_base64_raw: { type: string, nullable: true }
            copy_paste:         { type: string }
        payer:
          type: object
          nullable: true
          properties:
            name:     { type: string }
            document: { type: string }
        expires_at:  { type: string, format: date-time }
        paid_at:     { type: string, format: date-time, nullable: true }
        created_at:  { type: string, format: date-time }
        psp_id:      { type: string, nullable: true }
        webhook_url: { type: string, nullable: true }

    ChargeListItem:
      type: object
      properties:
        id:          { type: string }
        external_id: { type: string, nullable: true }
        status:      { type: string }
        amount:      { type: number }
        created_at:  { type: string, format: date-time }
        paid_at:     { type: string, format: date-time, nullable: true }
        expires_at:  { type: string, format: date-time }

    Transaction:
      type: object
      properties:
        id:              { type: string }
        charge_id:       { type: string, nullable: true }
        external_id:     { type: string, nullable: true }
        psp_id:          { type: string, nullable: true }
        amount_brl:      { type: number }
        usdt_amount:     { type: number }
        rate_brl:        { type: number }
        spread_pct:      { type: number }
        network:         { type: string, enum: [TRON, POLYGON, BSC] }
        to_address:      { type: string }
        tx_hash:         { type: string, nullable: true }
        explorer_url:    { type: string, nullable: true }
        network_fee_usd: { type: number }
        status:          { type: string, enum: [pending, sent, confirmed, failed] }
        sent_at:         { type: string, format: date-time, nullable: true }
        confirmed_at:    { type: string, format: date-time, nullable: true }
        created_at:      { type: string, format: date-time }

    WebhookEvent:
      type: object
      description: >-
        Payload do callback. Cliente DEVE validar: (1) X-Webhook-Timestamp dentro ±5min,
        (2) X-Webhook-Signature == HMAC-SHA256('{timestamp}.{body_bruto}', webhook_secret),
        comparação em tempo constante.

        Headers enviados: X-Webhook-Signature, X-Webhook-Timestamp, X-Webhook-Source: pixusdt,
        X-Webhook-Id (uuid — use pra deduplicar caso retry entregue o mesmo evento mais de uma vez).
      properties:
        event:
          type: string
          enum: [pix.received, pix.expired, pix.med, usdt.sent, webhook.test]
          description: pix.med dispara quando operador marca devolução PIX.
        timestamp: { type: string, format: date-time }
        data:
          type: object
          properties:
            charge:
              type: object
              properties:
                id:             { type: integer }
                external_id:    { type: string, nullable: true }
                amount_brl:     { type: number }
                status:         { type: string }
                paid_at:        { type: string, nullable: true }
                created_at:     { type: string }
                psp_id:         { type: string, nullable: true }
                payer_name:     { type: string, nullable: true }
                payer_document: { type: string, nullable: true }
            transaction:
              type: object
              nullable: true
              description: Presente em usdt.sent.
              properties:
                id:           { type: integer }
                usdt_amount:  { type: number }
                rate_brl:     { type: number }
                network:      { type: string }
                to_address:   { type: string }
                tx_hash:      { type: string }
                network_fee:  { type: number }
                explorer_url: { type: string }
                batch:        { type: boolean }
                batch_size:   { type: integer, nullable: true }
                status:       { type: string }
            med:
              type: object
              nullable: true
              description: Presente em pix.med. Marque pedido associado como cancelado/estornado.
              properties:
                id:         { type: integer }
                amount_brl: { type: number }
                reason:     { type: string }
                evidence:   { type: string, nullable: true }
                marked_at:  { type: string, format: date-time }

    WebhookDelivery:
      type: object
      properties:
        id:            { type: integer }
        event:         { type: string }
        url:           { type: string }
        charge_id:     { type: integer, nullable: true }
        attempt:       { type: integer }
        status_code:   { type: integer }
        duration_ms:   { type: integer }
        ok:            { type: boolean }
        error:         { type: string, nullable: true }
        response_body: { type: string, nullable: true }
        created_at:    { type: string, format: date-time }

    Error:
      type: object
      properties:
        error:   { type: string }
        message: { type: string }
        details: { type: object }

  responses:
    Unauthorized:
      description: API key ausente, inválida ou revogada
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: 'Conta não pode operar: profile_incomplete | account_suspended | account_blocked_med'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationError:
      description: 'validation_error ou amount_above_limit'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Recurso não encontrado
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    IdempotencyConflict:
      description: external_id existe nas últimas 24h com amount diferente.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimit:
      description: '100 req/min por IP. Header Retry-After indica segundos.'
      headers:
        Retry-After:             { schema: { type: integer } }
        X-RateLimit-Limit:       { schema: { type: integer } }
        X-RateLimit-Remaining:   { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PspError:
      description: Erro no PSP
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
