{
  "openapi": "3.1.0",
  "info": {
    "title": "UNIFOKAL API",
    "version": "v1",
    "summary": "Verificação de identidade: sessão criada no servidor, jornada no widget, resultado no webhook.",
    "description": "A versão vive no caminho (/v1); não existe header de versão. Adição compatível (campo, módulo ou chave nova) NÃO sobe a versão: ignore o que não conhecer. Todo erro tem a forma { error, message }: o código é estável, a mensagem não. Ids são prefixo + ULID. Webhooks são assinados (HMAC SHA-256 no header X-IDSAAS-Signature, janela de 300s; o segredo é SEU, mínimo 32 caracteres) e o header x-idsaas-event carrega o tipo do evento.",
    "termsOfService": "https://unifokal.com/termos",
    "contact": {
      "name": "UNIFOKAL",
      "url": "https://unifokal.com/docs"
    }
  },
  "externalDocs": {
    "description": "Documentação completa em https://unifokal.com/docs. Briefing para agentes de IA em https://unifokal.com/llms.txt.",
    "url": "https://unifokal.com/docs"
  },
  "servers": [
    {
      "url": "https://api.unifokal.com/v1"
    }
  ],
  "paths": {
    "/verification-sessions": {
      "post": {
        "operationId": "createVerificationSession",
        "summary": "Cria uma sessão de verificação (a única chamada obrigatória do integrador)",
        "description": "O 201 nunca traz decisão: o resultado chega no seu webhook (a fonte da verdade). IDEMPOTÊNCIA: esta rota não usa header nenhum. O reference_id (obrigatório) É a chave: a mesma organização, no mesmo ambiente, com o mesmo reference_id, recebe de volta a MESMA sessão ENQUANTO ELA VIVER (expires_in), em vez de uma segunda (o replay volta com Idempotent-Replay: true). Expirada a sessão, o mesmo reference_id abre uma sessão nova, que é o que a pessoa precisa para tentar de novo. Repetir o reference_id com um corpo DIFERENTE é 422 idempotency_key_reuse; com a primeira chamada ainda em voo, 409 idempotency_conflict.",
        "security": [
          {
            "secretKey": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSessionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sessão criada (monte o widget com o id vs_; nenhum segredo vai ao browser) — OU, quando o corpo traz transaction/transactions, a resposta de INGESTÃO (id ing_ + contadores; nenhum vs_).",
            "headers": {
              "Idempotent-Replay": {
                "schema": {
                  "type": "string"
                },
                "description": "Presente (true) quando a resposta é o replay selado de uma tentativa anterior."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/VerificationSession"
                    },
                    {
                      "$ref": "#/components/schemas/IngestResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Códigos estáveis: validation_error. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "validation_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Códigos estáveis: invalid_api_key. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_api_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "402": {
            "description": "Códigos estáveis: insufficient_credit. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "insufficient_credit"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Códigos estáveis: email_not_verified, credential_type_not_allowed, organization_suspended, test_key_used_in_production. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "email_not_verified",
                            "credential_type_not_allowed",
                            "organization_suspended",
                            "test_key_used_in_production"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Códigos estáveis: flow_not_found. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "flow_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "Códigos estáveis: idempotency_conflict, already_settled. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "idempotency_conflict",
                            "already_settled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Códigos estáveis: flow_not_live, email_not_accepted, phone_not_accepted, idempotency_key_reuse, policy_module_not_in_flow, policy_ubo_cap_above_flow, transaction_conflict, transaction_required, transaction_not_supported, batch_too_large, batch_not_supported_for_gate, external_id_required, amount_too_large, currency_not_supported, event_too_old, reference_id_charset, pii_shaped_value, account_event_required, account_event_not_supported, account_event_ip_not_public, assinatura_document_required, assinatura_not_supported, account_event_document_not_hashed, reference_id_required, unknown_settles_reference, pending_lifecycle_not_enabled, enrollment_not_found, enrollment_locked, document_blocklisted, session_monitoring_not_supported, window_too_long, pix_device_required, pix_device_module_not_in_flow, pix_device_reference_required. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "flow_not_live",
                            "email_not_accepted",
                            "phone_not_accepted",
                            "idempotency_key_reuse",
                            "policy_module_not_in_flow",
                            "policy_ubo_cap_above_flow",
                            "transaction_conflict",
                            "transaction_required",
                            "transaction_not_supported",
                            "batch_too_large",
                            "batch_not_supported_for_gate",
                            "external_id_required",
                            "amount_too_large",
                            "currency_not_supported",
                            "event_too_old",
                            "reference_id_charset",
                            "pii_shaped_value",
                            "account_event_required",
                            "account_event_not_supported",
                            "account_event_ip_not_public",
                            "assinatura_document_required",
                            "assinatura_not_supported",
                            "account_event_document_not_hashed",
                            "reference_id_required",
                            "unknown_settles_reference",
                            "pending_lifecycle_not_enabled",
                            "enrollment_not_found",
                            "enrollment_locked",
                            "document_blocklisted",
                            "session_monitoring_not_supported",
                            "window_too_long",
                            "pix_device_required",
                            "pix_device_module_not_in_flow",
                            "pix_device_reference_required"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Códigos estáveis: reauth_rate_limited, sandbox_limit_reached, rate_limited. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "reauth_rate_limited",
                            "sandbox_limit_reached",
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/webhook-events": {
      "get": {
        "operationId": "listUndeliveredWebhookEvents",
        "summary": "Lista os webhooks NÃO confirmados (falha definitiva de entrega), para redisparo",
        "security": [
          {
            "secretKey": []
          }
        ],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Valores acima de 20 são reduzidos a 20 por página."
          }
        ],
        "responses": {
          "200": {
            "description": "Entregas com falha DEFINITIVA no ambiente da chave (sandbox e produção nunca se cruzam).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEventList"
                }
              }
            }
          },
          "400": {
            "description": "Códigos estáveis: validation_error. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "validation_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Códigos estáveis: invalid_api_key. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_api_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Códigos estáveis: credential_type_not_allowed, organization_suspended, test_key_used_in_production. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "credential_type_not_allowed",
                            "organization_suspended",
                            "test_key_used_in_production"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/webhook-events/replay": {
      "post": {
        "operationId": "replayWebhookEvents",
        "summary": "Redispara os webhooks de até 20 verificações",
        "description": "Teto de 30 chamadas por minuto. Reenvia o corpo salvo byte a byte ao destino atual.",
        "security": [
          {
            "secretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReplayRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado por verificação; itens podem falhar individualmente (campo error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayResponse"
                }
              }
            }
          },
          "400": {
            "description": "Códigos estáveis: validation_error. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "validation_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Códigos estáveis: invalid_api_key. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_api_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Códigos estáveis: credential_type_not_allowed, organization_suspended, test_key_used_in_production. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "credential_type_not_allowed",
                            "organization_suspended",
                            "test_key_used_in_production"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Códigos estáveis: rate_limited. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/verification-links": {
      "post": {
        "operationId": "createVerificationLink",
        "summary": "Cria um link hospedado de verificação (o titular abre a jornada sem você montar o widget)",
        "description": "Para quem NÃO vai montar o widget: a UNIFOKAL hospeda a página e você entrega a url ao titular (e-mail, WhatsApp, QR). O link vive HORAS (a sessão vive 900s e só nasce no resgate), o token vlt_ volta em claro uma única vez e o link não cobra nada: quem cobra é a verificação que nascer do resgate. Sem Idempotency-Key de propósito: selar o corpo guardaria o segredo no banco; repetir a chamada só cria outro link, que expira sozinho.",
        "security": [
          {
            "secretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateLinkRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Link criado. Entregue a url ao titular; o token não é reexibido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerificationLink"
                }
              }
            }
          },
          "400": {
            "description": "Códigos estáveis: validation_error. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "validation_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Códigos estáveis: invalid_api_key. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_api_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Códigos estáveis: email_not_verified, credential_type_not_allowed, organization_suspended, test_key_used_in_production. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "email_not_verified",
                            "credential_type_not_allowed",
                            "organization_suspended",
                            "test_key_used_in_production"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Códigos estáveis: flow_not_found. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "flow_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "Códigos estáveis: flow_not_live, email_not_accepted, phone_not_accepted, expires_in_too_long, environment_mismatch, policy_module_not_in_flow, policy_ubo_cap_above_flow. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "flow_not_live",
                            "email_not_accepted",
                            "phone_not_accepted",
                            "expires_in_too_long",
                            "environment_mismatch",
                            "policy_module_not_in_flow",
                            "policy_ubo_cap_above_flow"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Códigos estáveis: rate_limited. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/capabilities": {
      "get": {
        "operationId": "getCapabilities",
        "summary": "Catálogo vivo de módulos e preços; com sk_, o contrato efetivo da sua organização",
        "description": "Credencial OPCIONAL. Sem credencial: catálogo geral com o preço-base público (cacheável). Com Bearer sk_: a MESMA URL devolve o contrato da sua organização (preço efetivo com override de contrato, ambiente e livemode da chave; sem cache HTTP). O estado available/coming_soon vem do banco, a mesma fonte da vitrine.",
        "security": [
          {},
          {
            "secretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo de capacidades. context.authenticated diz qual dos dois modos respondeu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capabilities"
                }
              }
            }
          },
          "401": {
            "description": "Códigos estáveis: invalid_api_key. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_api_key"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "Códigos estáveis: credential_type_not_allowed, organization_suspended, test_key_used_in_production. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "credential_type_not_allowed",
                            "organization_suspended",
                            "test_key_used_in_production"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Códigos estáveis: rate_limited. Ramifique pelo campo error, nunca pela mensagem.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ErrorEnvelope"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "rate_limited"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "verification.completed": {
      "post": {
        "operationId": "receiveVerificationWebhook",
        "summary": "A entrega assinada com o resultado da verificação (a fonte da verdade)",
        "description": "Verifique a assinatura HMAC SHA-256 do header X-IDSAAS-Signature (formato t=<ts>,v1=<hex>, janela de 300s) antes de confiar no corpo. Responda 2xx rápido e deduplique pelo id do evento; sem 2xx, a entrega re-tenta com backoff exponencial, 3 tentativas no total, ao longo de cerca de 15 minutos; passada a janela a entrega para e o resgate é o replay (operação replayWebhookEvents). Crédito e aprovação no seu sistema só a partir do webhook VERIFICADO, nunca da resposta síncrona.",
        "parameters": [
          {
            "name": "X-IDSAAS-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Assinatura HMAC (pode trazer mais de um v1 durante rotação de segredo)."
          },
          {
            "name": "x-idsaas-event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Tipo do evento (verification.<tipo>)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmação de recebimento. Qualquer 2xx conta; processar depois, responder já."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "secretKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Chave secreta sk_ no Authorization: Bearer. Só no seu servidor: nunca no browser ou app (o widget monta apenas com o id vs_ da sessão). Não existe chave publishable."
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "description": "Todo erro da API tem exatamente esta forma. O campo error é um código estável (contrato); message é texto livre e pode mudar sem aviso.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Código estável do erro. Ramifique por ele."
          },
          "message": {
            "type": "string",
            "description": "Texto explicativo. NÃO é contrato."
          }
        },
        "additionalProperties": false
      },
      "VerificationSessionId": {
        "type": "string",
        "pattern": "^vs_[0-9A-HJKMNP-TV-Z]{26}$",
        "description": "Id da sessão de verificação (vs_ + ULID). É o único valor que vai ao browser: o widget monta só com ele."
      },
      "FlowId": {
        "type": "string",
        "pattern": "^flow_[0-9A-HJKMNP-TV-Z]{26}$",
        "description": "Id de flow (flow_ + ULID). Criado no painel, nunca pela API sk_."
      },
      "VerificationId": {
        "type": "string",
        "pattern": "^ver_[0-9A-HJKMNP-TV-Z]{26}$",
        "description": "Id da verificação (ver_ + ULID), o identificador que o webhook carrega."
      },
      "CreateSessionRequest": {
        "type": "object",
        "properties": {
          "flow_id": {
            "type": "string",
            "minLength": 1,
            "description": "Id do flow que define os módulos da verificação (prefixo flow_)."
          },
          "reference_id": {
            "type": "string",
            "maxLength": 255,
            "description": "OBRIGATÓRIO, e ele É A CHAVE DE IDEMPOTÊNCIA desta rota (não existe header): a mesma organização, no mesmo ambiente, com o mesmo reference_id, recebe de volta a MESMA sessão enquanto ela viver, em vez de uma segunda. Volta EM CLARO no 201 e no webhook: nunca coloque dado pessoal aqui."
          },
          "email": {
            "description": "RECUSADO com 422 email_not_accepted: o e-mail é sempre digitado pelo titular no widget, que também dispara o código em seguida. Nunca envie o endereço na criação."
          },
          "phone": {
            "description": "RECUSADO com 422 phone_not_accepted: o telefone é sempre digitado pelo titular no widget, nunca enviado na criação."
          },
          "policy": {
            "type": "object",
            "properties": {
              "allow_pep": {
                "type": "boolean"
              },
              "allow_betting_ban": {
                "type": "boolean"
              },
              "ubo_max_paid_nodes": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10
              }
            },
            "minProperties": 1,
            "description": "Override de política por sessão (whitelist fechada; chave desconhecida vira 400, nunca é ignorada em silêncio)."
          },
          "return_url": {
            "type": "string",
            "maxLength": 2048,
            "description": "Para onde o WIDGET manda o titular quando a verificação termina. Existe para o caso do APLICATIVO NATIVO: no iOS o caminho recomendado é abrir a verificação no navegador do sistema, e sem destino de volta o titular acaba e fica preso lá. Aceita https ou o esquema PRÓPRIO do seu aplicativo (meubanco://kyc/pronto); os esquemas que o navegador interpreta sozinho (javascript:, data:, blob:, file:, intent: e afins) são recusados com 400, e http também (a volta depois de uma verificação de identidade não desce para texto claro). A volta NÃO carrega desfecho, score nem nada da verificação, nem em query nem em fragmento: um redirecionamento no browser do titular é forjável por quem controla o aparelho, então quem conta o que aconteceu é o WEBHOOK, e o seu aplicativo pergunta ao próprio backend. Ausente = ninguém é redirecionado."
          },
          "monitoring": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              }
            },
            "required": [
              "enabled"
            ]
          },
          "transaction": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "deposit",
                  "withdraw",
                  "bet",
                  "bet_profit",
                  "bet_loss",
                  "transfer",
                  "payment",
                  "settlement",
                  "reversal"
                ]
              },
              "amount_cents": {
                "type": "integer"
              },
              "currency": {
                "type": "string"
              },
              "external_id": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              "occurred_at": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "confirmed",
                  "failed"
                ]
              },
              "settles": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              "method": {
                "type": "string",
                "enum": [
                  "pix",
                  "ted",
                  "boleto",
                  "card",
                  "internal"
                ]
              },
              "counterparty_ref": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              "device": {
                "type": "object",
                "properties": {
                  "ip": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "fingerprint": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  }
                }
              }
            },
            "required": [
              "type",
              "amount_cents"
            ],
            "description": "Evento de transação (ingestão). Só é aceito em flow que contenha um módulo consumidor de transação; hoje esse módulo é o Gate Transacional (`transacao`). Flow sem consumidor responde 422 transaction_not_supported. É exclusivo com `transactions` (os dois juntos = 422 transaction_conflict), external_id é obrigatório (idempotência durável) e, sem id natural, derive sha256(reference_id|type|amount_cents|occurred_at|posição); a resposta 201 traz a forma de ingestão (id ing_ + contadores) e, em flow com gate síncrono, o veredito do gate."
          },
          "transactions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "deposit",
                    "withdraw",
                    "bet",
                    "bet_profit",
                    "bet_loss",
                    "transfer",
                    "payment",
                    "settlement",
                    "reversal"
                  ]
                },
                "amount_cents": {
                  "type": "integer"
                },
                "currency": {
                  "type": "string"
                },
                "external_id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 512
                },
                "occurred_at": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "confirmed",
                    "failed"
                  ]
                },
                "settles": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 512
                },
                "method": {
                  "type": "string",
                  "enum": [
                    "pix",
                    "ted",
                    "boleto",
                    "card",
                    "internal"
                  ]
                },
                "counterparty_ref": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 512
                },
                "device": {
                  "type": "object",
                  "properties": {
                    "ip": {
                      "type": "string",
                      "maxLength": 64
                    },
                    "fingerprint": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255
                    }
                  }
                }
              },
              "required": [
                "type",
                "amount_cents"
              ]
            },
            "minItems": 1,
            "description": "Lote de eventos (ingestão). Mesma condição do campo `transaction`: flow sem módulo consumidor responde 422 transaction_not_supported. Aceita 1..teto INGEST_MAX_BATCH (acima = 422 batch_too_large com o teto no corpo) e reenviar o mesmo lote não duplica nem cobra de novo (dedupe por external_id). ATENÇÃO: o lote é PROIBIDO quando o flow contém um gate síncrono, e o Gate Transacional (`transacao`) é um, então nesse flow use `transaction` (singular): o lote responde 422 batch_not_supported_for_gate, porque um gate decide UM pagamento e um lote não teria veredito."
          },
          "account_event": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "login",
                  "login_failed",
                  "signup",
                  "password_change",
                  "recovery",
                  "email_change",
                  "sensitive_action"
                ]
              },
              "action": {
                "type": "string",
                "maxLength": 64
              },
              "occurred_at": {
                "type": "string"
              },
              "device": {
                "type": "object",
                "properties": {
                  "ip": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 45
                  },
                  "fingerprint": {
                    "type": "string",
                    "maxLength": 512
                  },
                  "user_agent": {
                    "type": "string",
                    "maxLength": 512
                  }
                },
                "required": [
                  "ip"
                ]
              },
              "document_hash": {
                "type": "string"
              }
            },
            "required": [
              "type",
              "device"
            ]
          },
          "assinatura": {
            "type": "object",
            "properties": {
              "document_sha256": {
                "type": "string"
              }
            },
            "required": [
              "document_sha256"
            ],
            "description": "Bloco do módulo `assinatura` (assinatura eletrônica avançada): document_sha256 é o SHA-256, em hex, do documento que o titular vai assinar. O documento em si NUNCA é enviado: você guarda os bytes, a UNIFOKAL amarra o hash à verificação aprovada e devolve o dossiê assinado (Ed25519) no webhook. OBRIGATÓRIO quando o flow contém o módulo (422 assinatura_document_required) e RECUSADO quando não contém (422 assinatura_not_supported); chave desconhecida dentro do bloco vira 400, nunca é ignorada em silêncio. O módulo está pausado no catálogo (\"Em breve\")."
          },
          "pix_device": {
            "type": "object",
            "properties": {
              "fingerprint": {
                "type": "string",
                "minLength": 8,
                "maxLength": 512
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              },
              "platform": {
                "type": "string",
                "enum": [
                  "ios",
                  "android",
                  "web"
                ]
              }
            },
            "required": [
              "fingerprint"
            ]
          },
          "session_monitoring": {
            "type": "object",
            "properties": {
              "window_hours": {
                "type": "integer",
                "minimum": 1
              },
              "require_geolocation": {}
            },
            "required": [
              "window_hours"
            ]
          }
        },
        "required": [
          "flow_id",
          "reference_id"
        ]
      },
      "CreateLinkRequest": {
        "type": "object",
        "properties": {
          "flow_id": {
            "type": "string",
            "minLength": 1,
            "description": "Id do flow que define os módulos da verificação (prefixo flow_). O flow precisa estar live no ambiente da credencial."
          },
          "reference_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Seu identificador do titular/da tentativa. Volta EM CLARO na resposta e no webhook da verificação: nunca coloque dado pessoal aqui."
          },
          "email": {
            "description": "RECUSADO com 422 email_not_accepted: a página hospedada PEDE o e-mail ao titular e dispara o código em seguida, igual ao telefone. Nunca envie o endereço na emissão."
          },
          "phone": {
            "description": "RECUSADO com 422 phone_not_accepted: o telefone é sempre digitado pelo titular no widget, nunca enviado na emissão."
          },
          "expires_in": {
            "type": "integer",
            "minimum": 60,
            "description": "Validade do LINK em segundos (mínimo 60; acima do teto da casa é 422 expires_in_too_long, nunca um clamp silencioso). Não confunda com a vida da sessão, que só nasce no resgate."
          },
          "policy": {
            "type": "object",
            "properties": {
              "allow_pep": {
                "type": "boolean"
              },
              "allow_betting_ban": {
                "type": "boolean"
              },
              "ubo_max_paid_nodes": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10
              }
            },
            "minProperties": 1,
            "description": "Override de política por sessão, validado JÁ NA EMISSÃO (a sessão do resgate herda). Política de módulo que o flow não tem é 422 policy_module_not_in_flow, e o teto ubo_max_paid_nodes só APERTA o do flow (acima dele é 422 policy_ubo_cap_above_flow)."
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ],
            "description": "SÓ para a superfície de painel. Com sk_ o ambiente É a chave: mandar um diferente é 422 environment_mismatch (aceitar e ignorar faria você acreditar que trocou de ambiente)."
          }
        },
        "required": [
          "flow_id",
          "reference_id"
        ]
      },
      "IngestBatchId": {
        "type": "string",
        "pattern": "^ing_[0-9A-HJKMNP-TV-Z]{26}$",
        "description": "Id do LOTE de ingestão (ing_ + ULID). NÃO é credencial de nada: usado como Bearer, dá 401."
      },
      "IngestResponse": {
        "type": "object",
        "description": "Resposta 201 quando o corpo traz transaction/transactions (ingestão). Nunca traz score, regra ou limiar; o bloco ingest é SEMPRE o mesmo para qualquer módulo consumidor.",
        "required": [
          "id",
          "status",
          "ingest"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/IngestBatchId"
          },
          "status": {
            "type": "string",
            "const": "consumed"
          },
          "ingest": {
            "type": "object",
            "required": [
              "batch_id",
              "accepted",
              "duplicated",
              "batch_size",
              "first_seen_at"
            ],
            "properties": {
              "batch_id": {
                "$ref": "#/components/schemas/IngestBatchId"
              },
              "accepted": {
                "type": "integer",
                "description": "Eventos gravados nesta chamada."
              },
              "duplicated": {
                "type": "integer",
                "description": "Eventos já vistos (dedupe durável por external_id): retry não duplica nem cobra de novo."
              },
              "batch_size": {
                "type": "integer"
              },
              "first_seen_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "VerificationSession": {
        "type": "object",
        "description": "Resposta 201 da criação. NUNCA traz decisão: o resultado chega no seu webhook.",
        "required": [
          "id",
          "flow_id",
          "environment",
          "reference_id",
          "status",
          "expires_at",
          "created_at",
          "modules",
          "livemode",
          "expires_in"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/VerificationSessionId"
          },
          "flow_id": {
            "$ref": "#/components/schemas/FlowId"
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ]
          },
          "reference_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "requires_input",
              "processing",
              "completed",
              "expired"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "modules": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Módulos do flow. Valor novo pode surgir sem subir a versão (adição compatível): ignore o que não conhecer."
          },
          "livemode": {
            "type": "boolean"
          },
          "expires_in": {
            "type": "integer",
            "description": "Segundos até a sessão expirar. Crie a sessão com a pessoa presente; nunca em lote."
          }
        }
      },
      "VerificationLinkId": {
        "type": "string",
        "pattern": "^vl_[0-9A-HJKMNP-TV-Z]{26}$",
        "description": "Id do link hospedado (vl_ + ULID). NÃO é o segredo: o segredo é o token vlt_."
      },
      "VerificationLink": {
        "type": "object",
        "description": "Resposta 201 da criação do link hospedado. O campo token (vlt_) é devolvido EM CLARO uma única vez e nunca é reexibido: guarde-o ou entregue a url ao titular na mesma resposta.",
        "required": [
          "id",
          "environment",
          "flow_id",
          "reference_id",
          "contact_masked",
          "created_via",
          "created_by_member_id",
          "status",
          "expires_at",
          "opened_at",
          "consumed_at",
          "session_id",
          "revoked_at",
          "created_at",
          "url",
          "token",
          "expires_in",
          "livemode"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/VerificationLinkId"
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ]
          },
          "flow_id": {
            "$ref": "#/components/schemas/FlowId"
          },
          "reference_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_masked": {
            "type": [
              "string",
              "null"
            ],
            "description": "E-mail MASCARADO, e só quando o flow verifica e-mail: o endereço em claro fica cifrado no repouso e nunca volta por aqui."
          },
          "created_via": {
            "type": "string",
            "enum": [
              "api",
              "dashboard"
            ]
          },
          "created_by_member_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Membro que emitiu pelo painel. Emissão por sk_ não tem membro: null."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "opened",
              "consumed",
              "expired",
              "revoked"
            ],
            "description": "Estado do link. `consumed` significa que alguém resgatou e a sessão nasceu (veja session_id)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "opened_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "consumed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sessão criada no resgate do link. Null até alguém abrir e resgatar."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A página hospedada, com o token embutido. É o que você entrega ao titular."
          },
          "token": {
            "type": "string",
            "description": "Segredo do link (vlt_), em claro UMA vez. Guardamos só o hash: não há como reexibir."
          },
          "expires_in": {
            "type": "integer",
            "description": "Segundos de validade do link (não da sessão)."
          },
          "livemode": {
            "type": "boolean"
          }
        }
      },
      "UndeliveredWebhookEvent": {
        "type": "object",
        "description": "Um webhook cujo ciclo de entrega terminou SEM confirmação 2xx do seu servidor.",
        "required": [
          "event_type",
          "verification_id",
          "reference_id",
          "target_url",
          "status",
          "status_code",
          "attempts",
          "failed_at"
        ],
        "properties": {
          "event_type": {
            "type": "string",
            "enum": [
              "completed",
              "blocked",
              "failed",
              "pending",
              "monitoring",
              "test",
              "pix_device_revoked",
              "subject_erased"
            ]
          },
          "verification_id": {
            "$ref": "#/components/schemas/VerificationId"
          },
          "reference_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "target_url": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "const": "error"
          },
          "status_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "failed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "WebhookEventList": {
        "type": "object",
        "required": [
          "data",
          "page",
          "totalPages",
          "totalItems"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UndeliveredWebhookEvent"
            }
          },
          "page": {
            "type": "integer"
          },
          "totalPages": {
            "type": "integer"
          },
          "totalItems": {
            "type": "integer"
          }
        }
      },
      "ReplayRequest": {
        "type": "object",
        "required": [
          "verification_ids"
        ],
        "properties": {
          "verification_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            },
            "minItems": 1,
            "maxItems": 20,
            "uniqueItems": true,
            "description": "Até 20 ids de verificação por chamada."
          }
        }
      },
      "ReplayResult": {
        "type": "object",
        "required": [
          "verification_id",
          "resent",
          "http_status"
        ],
        "properties": {
          "verification_id": {
            "type": "string"
          },
          "resent": {
            "type": "boolean"
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ]
          },
          "error": {
            "type": "string",
            "description": "Presente só quando este item falhou (ex.: nothing_to_replay)."
          }
        }
      },
      "ReplayResponse": {
        "type": "object",
        "required": [
          "results",
          "resent",
          "failed"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReplayResult"
            }
          },
          "resent": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          }
        }
      },
      "CapabilityModule": {
        "type": "object",
        "required": [
          "module",
          "group",
          "status",
          "unit_cents",
          "is_addon",
          "requires"
        ],
        "properties": {
          "module": {
            "type": "string",
            "description": "Nome do módulo (o mesmo usado nos flows)."
          },
          "group": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "coming_soon"
            ],
            "description": "available = vendável hoje; coming_soon = preço publicado, venda pausada."
          },
          "unit_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "Preço unitário em centavos de BRL. Sem credencial, o preço-base de vitrine; com sk_, o preço efetivo do contrato da sua organização."
          },
          "is_addon": {
            "type": "boolean"
          },
          "requires": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Módulos que este exige no mesmo flow (o mesmo grafo que a API enforça com 422)."
          },
          "decision": {
            "type": "string",
            "enum": [
              "decisive",
              "informational"
            ],
            "description": "O que você leva por este preço. decisive = o resultado participa do veredito da verificação (portão que reprova ou segura, ou peso no score). informational = o módulo ENTREGA DADO e não julga: é resolvido depois da decisão e não altera aprovação, reprovação nem score. Ausente nas linhas que não são módulo de flow (unidades de metering)."
          }
        }
      },
      "CapabilitiesContext": {
        "type": "object",
        "required": [
          "authenticated",
          "environment",
          "livemode",
          "pricing"
        ],
        "properties": {
          "authenticated": {
            "type": "boolean"
          },
          "environment": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "sandbox",
              "production",
              null
            ]
          },
          "livemode": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "pricing": {
            "type": "string",
            "enum": [
              "list",
              "contract"
            ],
            "description": "list = preço-base público; contract = preço efetivo da organização da chave."
          }
        }
      },
      "Capabilities": {
        "type": "object",
        "required": [
          "api_version",
          "spec_url",
          "docs_url",
          "llms_url",
          "webhook_schema_version",
          "context",
          "modules"
        ],
        "properties": {
          "api_version": {
            "type": "string",
            "const": "v1"
          },
          "spec_url": {
            "type": "string",
            "format": "uri"
          },
          "docs_url": {
            "type": "string",
            "format": "uri"
          },
          "llms_url": {
            "type": "string",
            "format": "uri"
          },
          "webhook_schema_version": {
            "type": "integer",
            "const": 1
          },
          "context": {
            "$ref": "#/components/schemas/CapabilitiesContext"
          },
          "modules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CapabilityModule"
            }
          }
        }
      },
      "WebhookData": {
        "type": "object",
        "description": "Conteúdo da verificação decidida. Campo novo pode surgir sem subir schema_version (adição compatível): o parser tem que ignorar chave desconhecida. check_details é omitido quando o endpoint está em modo minimal; truncated marca corpo podado acima do teto (a decisão nunca é podada).",
        "required": [
          "object",
          "id",
          "verification_id",
          "flow_id",
          "reference_id",
          "status",
          "score",
          "risk_level",
          "recommendation",
          "decision_reason",
          "reason_code",
          "environment",
          "completed_at",
          "saldoUsado",
          "saldoRestante",
          "checks"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "verification"
          },
          "id": {
            "$ref": "#/components/schemas/VerificationId"
          },
          "verification_id": {
            "$ref": "#/components/schemas/VerificationId"
          },
          "flow_id": {
            "$ref": "#/components/schemas/FlowId"
          },
          "reference_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "denied",
              "blocked",
              "failed",
              "review"
            ]
          },
          "score": {
            "type": [
              "number",
              "null"
            ]
          },
          "risk_level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "low",
              "medium",
              "high",
              null
            ]
          },
          "recommendation": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "approve",
              "review",
              "decline",
              null
            ]
          },
          "decision_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason_code": {
            "type": [
              "object",
              "null"
            ],
            "description": "Motivo da decisão em forma legível. Carrega code (categoria), module, secondary, subject_code, catalog_version e subject_message. Desde 11 de setembro de 2026 carrega também reasons, uma lista em que cada item separa a evidência (code, o mesmo vocabulário de decision_reason) do efeito dela no desfecho (outcome_effect: blocked, review ou info), com o módulo de origem, o grupo (aspect: document, biometrics, data_validation, fraud_signals ou channel) e dois textos em português, display_pt e action_pt; e aspects, o índice desses códigos pelos cinco grupos, sempre com as cinco chaves."
          },
          "environment": {
            "type": "string",
            "enum": [
              "sandbox",
              "production"
            ]
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "saldoUsado": {
            "type": "integer",
            "description": "Centavos debitados por ESTA verificação (sandbox: valor fixo)."
          },
          "saldoRestante": {
            "type": "integer",
            "description": "Saldo após o débito, estável entre reentregas."
          },
          "checks": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Resumo semântico por área (sem PII). Presente também no modo minimal."
          },
          "check_details": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "module",
                "passed",
                "outcome",
                "score",
                "reason"
              ],
              "properties": {
                "module": {
                  "type": "string"
                },
                "passed": {
                  "type": [
                    "boolean",
                    "null"
                  ]
                },
                "outcome": {
                  "type": "string"
                },
                "score": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "additionalProperties": true
            }
          },
          "blocklist_face_match": {
            "type": [
              "object",
              "null"
            ],
            "description": "Evidência do portão de rosto da blocklist, só quando ele moveu a decisão."
          },
          "truncated": {
            "type": "boolean"
          },
          "truncated_fields": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Envelope entregue ao seu endpoint. schema_version só sobe em mudança INCOMPATÍVEL (que também exigiria /v2); adição de campo mantém a versão. Corpo acima de 262144 bytes chega podado e marcado com truncated.",
        "required": [
          "id",
          "schema_version",
          "event",
          "livemode",
          "created",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id estável do evento (deduplique por ele): evt_<verification_id>_<tipo>, com sufixo _rN em reemissão manual."
          },
          "schema_version": {
            "type": "integer",
            "const": 1
          },
          "event": {
            "type": "string",
            "enum": [
              "verification.completed",
              "verification.blocked",
              "verification.failed",
              "verification.pending",
              "verification.monitoring",
              "verification.test",
              "verification.pix_device_revoked",
              "verification.subject_erased"
            ]
          },
          "livemode": {
            "type": "boolean"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "$ref": "#/components/schemas/WebhookData"
          }
        }
      }
    }
  }
}
