{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/signdocs-com-br/json-schema/signdocs-com-br-create-signing-session-request-schema.json",
  "title": "CreateSigningSessionRequest",
  "description": "Requisição para criar uma sessão de assinatura expressa.",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/signdocs-com-br-openapi.yaml#/components/schemas/CreateSigningSessionRequest",
  "type": "object",
  "required": [
    "purpose",
    "policy",
    "signer"
  ],
  "properties": {
    "purpose": {
      "$ref": "#/$defs/TransactionPurpose"
    },
    "policy": {
      "$ref": "#/$defs/Policy"
    },
    "signer": {
      "$ref": "#/$defs/Signer"
    },
    "document": {
      "type": "object",
      "description": "Documento inline (base64). Máximo 10 MB. Suporta PDF e outros formatos.",
      "properties": {
        "content": {
          "type": "string",
          "format": "byte",
          "description": "Conteúdo do documento em base64."
        },
        "filename": {
          "type": "string",
          "description": "Nome do arquivo."
        }
      }
    },
    "referenceImage": {
      "type": "object",
      "description": "Imagem de referência biométrica para esta sessão, em base64 (JPEG). Máximo 5 MB.\n\nDefine contra qual rosto a etapa `BIOMETRIC_MATCH` compara o liveness capturado:\n\n- **Com `referenceImage`**: a imagem enviada aqui é usada e tem precedência sobre\n  qualquer enrollment existente. Vale apenas para esta transação — não cria nem\n  atualiza o enrollment de `signer.userExternalId`.\n- **Sem `referenceImage`** (caso comum): a comparação usa o enrollment cadastrado\n  para `signer.userExternalId` via `PUT /v1/users/{userExternalId}/enrollment`.\n- **Sem nenhum dos dois**: a etapa de comparação falha com 422.\n\nA imagem é retida por 90 dias, conforme a política de retenção de dados biométricos.\nA presença de um rosto **não** é verificada no momento da criação: uma imagem sem\nrosto detectável só falha mais tarde, na etapa de comparação.\n\nNão é aceito em `POST /v1/trust-sessions` (retorna 400).\n\nNa evidência, a comparação resultante registra\n`enrollmentRef.source: TRANSACTION_PROVIDED` e\n`enrollmentRef.capturedFrom: SESSION_CREATE`.\n",
      "required": [
        "content"
      ],
      "properties": {
        "content": {
          "type": "string",
          "format": "byte",
          "description": "Imagem JPEG codificada em base64."
        }
      }
    },
    "action": {
      "$ref": "#/$defs/ActionMetadata"
    },
    "returnUrl": {
      "type": "string",
      "format": "uri",
      "description": "URL de retorno após conclusão. O `session_id` será adicionado como query parameter."
    },
    "cancelUrl": {
      "type": "string",
      "format": "uri",
      "description": "URL de retorno em caso de cancelamento pelo signatário."
    },
    "metadata": {
      "type": "object",
      "description": "Metadados livres (chave-valor). Chaves até 256 chars, valores até 1024 chars.\n\nQuatro chaves são interpretadas pela API, todas como string e todas\napenas em sandbox — é por elas que se testa a reprovação **pela página\nhospedada**, onde quem emite a chamada é a própria página e o\nintegrador não tem outro ponto de injeção:\n\n| Chave | Efeito | Padrão |\n|---|---|---|\n| `sandboxSimilarity` | Similaridade simulada de `BIOMETRIC_MATCH` / `DOCUMENT_PHOTO_MATCH` | 99.5 |\n| `sandboxLivenessConfidence` | Confiança simulada de `BIOMETRIC_LIVENESS` | 99.5 |\n| `sandboxBrightness` | Brilho simulado da captura (dispara `DOCUMENT_QUALITY_LOW`) | 80 |\n| `sandboxSharpness` | Nitidez simulada da captura | 80 |\n\nFora do sandbox são ignoradas. Valores inválidos também são ignorados,\nmantendo o padrão: elas são fixadas na criação e não podem quebrar o\nsignatário no meio do fluxo.\n",
      "additionalProperties": {
        "type": "string"
      }
    },
    "locale": {
      "type": "string",
      "description": "Idioma da página de assinatura.",
      "enum": [
        "pt-BR",
        "en",
        "es"
      ],
      "default": "pt-BR"
    },
    "expiresInMinutes": {
      "type": "integer",
      "description": "Tempo de expiração em minutos (5 a 1440). Padrão 60.",
      "minimum": 5,
      "maximum": 1440,
      "default": 60
    },
    "appearance": {
      "$ref": "#/$defs/SigningSessionAppearance"
    },
    "owner": {
      "$ref": "#/$defs/Owner"
    },
    "deliverVia": {
      "type": "array",
      "description": "Canais pelos quais a SignDocs entrega o link de assinatura a este\nsignatário. Omita para manter o comportamento anterior: apenas o e-mail de\nconvite, sujeito à regra de `owner`/`signer.email`. Os canais escolhidos\nficam gravados: um `resend-invite` sem corpo reenvia por eles.\n\nWhatsApp e Telegram são habilitados **sob demanda**: o tenant pede ao time\ncomercial, e não é algo que se liga pelo painel. Pedir um canal que o tenant\nnão tem retorna **403** antes de qualquer gravação.\n\n`whatsapp` exige `signer.phone` em E.164. `whatsappInviteSent: true`\nsignifica que a Meta **aceitou** a mensagem, não que ela foi entregue ou\nlida; em qualquer outro caso o campo é omitido da resposta. Se a Meta\nreportar falha depois e o signatário tiver e-mail, a SignDocs reenvia o\nconvite por e-mail automaticamente, com um link novo, diferente do que veio\nnesta resposta; nenhum webhook, campo ou endpoint informa esse reenvio. Um\nnúmero que respondeu \"parar\", \"sair\" ou \"stop\" deixa de receber mensagens da\nSignDocs por WhatsApp.\n\n`telegram` exige `signer.cpf` e só alcança signatários que já registraram o\npróprio CPF no bot da SignDocs. `telegramInviteSent` vem na resposta sempre\nque o convite por Telegram é tentado nesta chamada, `false` quando não chegou.\n\nCada envio por WhatsApp ou Telegram consome a cota de mensagens do tenant.\nEsgotada a cota, a requisição é recusada com **429** antes de qualquer\ngravação; com o canal suspenso por kill switch, com **503**. Credenciais de\nsandbox também enviam mensagens reais por WhatsApp e consomem a cota de\nmensagens de sandbox.\n",
      "items": {
        "type": "string",
        "enum": [
          "email",
          "whatsapp",
          "telegram"
        ]
      }
    }
  },
  "$defs": {
    "ActionMetadata": {
      "type": "object",
      "description": "Metadados da ação autenticada (para `ACTION_AUTHENTICATION`).",
      "required": [
        "type",
        "description"
      ],
      "properties": {
        "type": {
          "type": "string",
          "description": "Tipo da ação."
        },
        "description": {
          "type": "string",
          "description": "Descrição da ação."
        },
        "reference": {
          "type": "string",
          "description": "Referência externa da ação."
        }
      }
    },
    "OtpChannel": {
      "type": "string",
      "description": "Canal de entrega do OTP, escolhido por signatário. Independente do canal\nusado para entregar o link de assinatura: um convite por WhatsApp pode ter\nOTP por e-mail, e um convite por e-mail pode ter OTP por WhatsApp.\n\n`sms` e `whatsapp` exigem `signer.phone` e o canal habilitado para o tenant\n(`sms.enabled` / `whatsapp.enabled`); caso contrário o passo de OTP falha com\n**422**. No WhatsApp, um número que pediu para não receber mensagens também\nfalha com 422, com uma mensagem que orienta a escolher outro canal.\n\n`telegram` exige `telegram.enabled` no tenant. O código chega pelo bot da\nSignDocs quando o signatário já registrou o próprio CPF nele; sem registro, é\nenviado por telefone (Telegram Gateway) se o tenant tiver essa opção\nhabilitada, e caso contrário o passo falha com **422** de imediato. Ao criar a\nsessão, `otpChannel: telegram` ainda exige `signer.email` quando a política\ntem OTP.\n\nUm OTP por WhatsApp ou pelo Telegram Gateway consome a cota de mensagens do\ntenant; esgotada, o envio é recusado com **429**.\n",
      "enum": [
        "email",
        "sms",
        "whatsapp",
        "telegram"
      ],
      "default": "email"
    },
    "Owner": {
      "type": "object",
      "description": "Identidade do solicitante da assinatura (quem cria a sessão/envelope), distinta\ndo signatário. Quando informado, a SignDocs:\n\n1. **Envia automaticamente um e-mail de convite** para `signer.email` no momento\n   da criação da sessão, se `signer.email` for diferente de `owner.email`\n   (case-insensitive). Em envelopes, cada signatário recebe seu próprio convite.\n2. **Notifica o solicitante por e-mail** quando cada signatário concluir a\n   assinatura, com o protocolo de evidência e link para o verificador.\n\nOmitir `owner` mantém o comportamento padrão: o integrador entrega a URL de\nassinatura por seus próprios canais e usa webhooks para acompanhar o status.\n",
      "properties": {
        "email": {
          "type": "string",
          "format": "email",
          "description": "E-mail do solicitante. Recebe notificações de conclusão."
        },
        "name": {
          "type": "string",
          "description": "Nome do solicitante (usado na saudação dos e-mails).",
          "maxLength": 1024
        }
      }
    },
    "Policy": {
      "type": "object",
      "description": "Política de verificação de identidade da transação.",
      "required": [
        "profile"
      ],
      "properties": {
        "profile": {
          "$ref": "#/$defs/PolicyProfile"
        },
        "customSteps": {
          "type": "array",
          "description": "Etapas customizadas (apenas quando `profile` = `CUSTOM`).",
          "items": {
            "$ref": "#/$defs/StepType"
          }
        },
        "minSimilarity": {
          "type": "number",
          "format": "double",
          "minimum": 0,
          "maximum": 100,
          "description": "Limiar mínimo de similaridade facial exigido desta transação, para as\netapas `BIOMETRIC_MATCH` e `DOCUMENT_PHOTO_MATCH`.\n\n**Só aperta, nunca afrouxa.** O valor precisa ser maior ou igual ao\nlimiar configurado para o tenant; um valor abaixo dele retorna 400\ninformando o mínimo vigente, em vez de ser ignorado em silêncio —\nafrouxar a verificação de identidade é decisão do tenant, não da\nrequisição. Documentos de maior risco podem assim exigir mais\nconfiança que o padrão da conta, sem reconfigurar o tenant.\n\nAceita tanto percentual (`95`) quanto fração (`0.95`).\n"
        },
        "minLivenessConfidence": {
          "type": "number",
          "format": "double",
          "minimum": 0,
          "maximum": 100,
          "description": "Limiar mínimo de confiança de vivacidade (`BIOMETRIC_LIVENESS`) exigido\ndesta transação. Mesmo enquadramento do `minSimilarity`: só aperta, e\num valor abaixo do mínimo do tenant retorna 400.\n\nQuando o tenant não define limiar de vivacidade próprio, qualquer valor\nválido é aceito.\n"
        }
      }
    },
    "PolicyProfile": {
      "type": "string",
      "description": "Perfis de política de verificação pré-definidos. Cada perfil resolve para uma\nlista ordenada de etapas (`StepType`):\n\n- `CLICK_ONLY`: CLICK_ACCEPT\n- `CLICK_PLUS_OTP`: CLICK_ACCEPT, OTP_CHALLENGE, OTP_VERIFY\n- `BIOMETRIC`: BIOMETRIC_LIVENESS, BIOMETRIC_MATCH\n- `BIOMETRIC_PLUS_OTP`: BIOMETRIC_LIVENESS, BIOMETRIC_MATCH, OTP_CHALLENGE, OTP_VERIFY\n- `DIGITAL_CERTIFICATE`: CLICK_ACCEPT, DIGITAL_SIGN_A1\n- `BIOMETRIC_SERPRO`: BIOMETRIC_LIVENESS, BIOMETRIC_MATCH, SERPRO_IDENTITY_CHECK\n- `BIOMETRIC_SERPRO_AUTO_FALLBACK`: as mesmas etapas de `BIOMETRIC_SERPRO`, mais uma\n  etapa DOCUMENT_PHOTO_MATCH criada em estado `DORMANT`. Se a base governamental\n  estiver indisponível, a etapa SERPRO passa a `SKIPPED` e a dormente é ativada,\n  sem derrubar a assinatura em andamento.\n- `BIOMETRIC_DOCUMENT_FALLBACK`: BIOMETRIC_LIVENESS, DOCUMENT_PHOTO_MATCH — para\n  signatários sem cadastro biométrico prévio, comparando a prova de vida com a foto\n  de um documento oficial.\n- `CUSTOM`: etapas definidas em `policy.customSteps`.\n\n`BIOMETRIC`, `BIOMETRIC_PLUS_OTP`, `BIOMETRIC_SERPRO`, `BIOMETRIC_SERPRO_AUTO_FALLBACK`\ne `BIOMETRIC_DOCUMENT_FALLBACK` exigem cota biométrica provisionada para o tenant.\n",
      "enum": [
        "CLICK_ONLY",
        "CLICK_PLUS_OTP",
        "BIOMETRIC",
        "BIOMETRIC_PLUS_OTP",
        "DIGITAL_CERTIFICATE",
        "BIOMETRIC_SERPRO",
        "BIOMETRIC_SERPRO_AUTO_FALLBACK",
        "BIOMETRIC_DOCUMENT_FALLBACK",
        "CUSTOM"
      ]
    },
    "Signer": {
      "type": "object",
      "description": "Dados do signatário.\nÉ obrigatório informar pelo menos um entre `cpf` e `cnpj`.\n",
      "required": [
        "name",
        "userExternalId"
      ],
      "anyOf": [
        {
          "required": [
            "cpf"
          ]
        },
        {
          "required": [
            "cnpj"
          ]
        }
      ],
      "properties": {
        "name": {
          "type": "string",
          "description": "Nome completo do signatário."
        },
        "email": {
          "type": "string",
          "format": "email",
          "description": "E-mail do signatário (obrigatório para etapas OTP)."
        },
        "userExternalId": {
          "type": "string",
          "description": "Identificador externo do usuário no sistema do integrador."
        },
        "displayName": {
          "type": "string",
          "description": "Nome de exibição do signatário."
        },
        "cpf": {
          "type": "string",
          "description": "CPF do signatário (11 dígitos, sem pontuação). Obrigatório se `cnpj` não for informado.",
          "pattern": "^\\d{11}$"
        },
        "cnpj": {
          "type": "string",
          "description": "CNPJ do signatário (14 dígitos, sem pontuação). Obrigatório se `cpf` não for informado.",
          "pattern": "^\\d{14}$"
        },
        "phone": {
          "type": "string",
          "description": "Telefone do signatário em formato E.164. Obrigatório quando `otpChannel` é `sms` ou `whatsapp`, ou quando `deliverVia` inclui `whatsapp`.",
          "pattern": "^\\+[1-9]\\d{7,14}$"
        },
        "otpChannel": {
          "$ref": "#/$defs/OtpChannel"
        },
        "otpChannelSelectable": {
          "type": "boolean",
          "description": "Quando `true`, a tela de assinatura hospedada permite que o signatário escolha o canal (entre os de `availableOtpChannels`) que recebe o OTP no momento da assinatura e troque de canal durante o fluxo. Padrão `false` — integrações existentes mantêm um único canal fixo e o backend recusa um canal escolhido pelo signatário se este campo não estiver definido.",
          "default": false
        }
      }
    },
    "SigningSessionAppearance": {
      "type": "object",
      "description": "Configuração visual da página de assinatura hospedada.",
      "properties": {
        "brandColor": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Cor primária da marca (hex)."
        },
        "logoUrl": {
          "type": "string",
          "format": "uri",
          "description": "URL HTTPS do logotipo da empresa."
        },
        "companyName": {
          "type": "string",
          "description": "Nome da empresa exibido na página."
        },
        "backgroundColor": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Cor de fundo (hex)."
        },
        "textColor": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Cor do texto (hex)."
        },
        "buttonTextColor": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Cor do texto dos botões (hex)."
        },
        "borderRadius": {
          "type": "string",
          "pattern": "^\\d+px$",
          "description": "Raio de borda dos elementos (formato \"Npx\")."
        },
        "headerStyle": {
          "type": "string",
          "description": "Estilo do cabeçalho da página.",
          "enum": [
            "full",
            "minimal",
            "none"
          ],
          "default": "full"
        },
        "fontFamily": {
          "type": "string",
          "description": "Família tipográfica.",
          "enum": [
            "system-ui",
            "Inter",
            "Roboto",
            "Open Sans",
            "Lato",
            "Montserrat",
            "Source Sans Pro",
            "Nunito",
            "Poppins",
            "Raleway",
            "Ubuntu"
          ],
          "default": "system-ui"
        }
      }
    },
    "StepType": {
      "type": "string",
      "description": "Tipos de etapa de verificação de identidade. Em `policy.customSteps` a ordem é\nvalidada na criação: BIOMETRIC_MATCH, SERPRO_IDENTITY_CHECK e DOCUMENT_PHOTO_MATCH\nexigem BIOMETRIC_LIVENESS antes de si, OTP_CHALLENGE e OTP_VERIFY vêm sempre em par\ne nessa ordem, e DIGITAL_SIGN_A1 tem de ser a última etapa.\n",
      "enum": [
        "CLICK_ACCEPT",
        "OTP_CHALLENGE",
        "OTP_VERIFY",
        "BIOMETRIC_LIVENESS",
        "BIOMETRIC_MATCH",
        "DOCUMENT_PHOTO_MATCH",
        "SERPRO_IDENTITY_CHECK",
        "DIGITAL_SIGN_A1",
        "PURPOSE_DISCLOSURE"
      ]
    },
    "TransactionPurpose": {
      "type": "string",
      "description": "Finalidade da transação.",
      "enum": [
        "DOCUMENT_SIGNATURE",
        "ACTION_AUTHENTICATION"
      ]
    }
  }
}