Verificação Dupla
A Verificação Dupla (TSSV — Two-Step Security Verification) é o processo de custódia segura de alunos, onde um responsável é verificado antes de retirar um aluno da escola. A TSSV suporta dois canais de check-in: o totem biométrico instalado na escola (reconhecimento facial físico) e o app móvel (verificação criptográfica vinculada à biometria do dispositivo).
Como funciona
A TSSV funciona por dois canais independentes. O resultado final é o mesmo: o responsável é verificado e os alunos disponíveis para custódia são retornados.
Via totem biométrico (reconhecimento facial)
-
1
O responsável se aproxima do totem biométrico instalado na escola e realiza o reconhecimento facial.
-
2
O totem envia o CPF do responsável e o CNPJ da escola via
POST /api/v1/tssv/responsible/check/inusando o token de parceiro. -
3
A API retorna a lista de alunos vinculados ao responsável presentes na escola naquele dia.
Via app móvel (assinatura criptográfica)
-
1
O responsável abre o app e inicia o check-in. O app solicita um desafio (nonce) ao servidor.
-
2
O app assina o nonce com a chave privada do dispositivo — o SO exibe Face ID ou digital ao responsável. Só com biometria aprovada a assinatura é gerada.
-
3
O app envia o check-in com
device_id,challengeesignature. O backend verifica a assinatura e libera os alunos.
students (array)
Sucesso — retorna lista de alunos disponíveis para custódia.
ALREADY_WAITING_FOR_CUSTODY
O responsável já está aguardando liberação.
RESPONSIBLE_NOT_FOUND
O responsável não foi encontrado no sistema.
NO_STUDENTS_FOR_CUSTODY
O responsável não possui alunos para custódia hoje.
/v1/tssv/responsible/check/in
Check-in do responsável com facial
Usado exclusivamente pelo totem biométrico instalado na escola. Após o reconhecimento facial do responsável, o totem chama este endpoint informando o CPF do responsável e o CNPJ da escola. A API registra a chegada e retorna os alunos disponíveis para custódia.
Este endpoint usa autenticação por token de parceiro (X-Authorization,
X-Partner, X-Client),
Para check-in via app móvel, veja Check-in com app móvel.
Atributos obrigatórios (body JSON)
school_cnpj
string
obrigatório
CNPJ da escola (deve existir no sistema).
responsible_cpf
string
obrigatório
CPF do responsável (deve estar cadastrado no sistema).
checked_in_at
datetime
obrigatório
Data e hora da chegada no formato Y-m-d H:i:s.
Códigos de resposta
200
Sucesso.
400
Dados inválidos.
401
Autenticação inválida.
404
Recurso de referência não encontrado.
-H "Content-Type: application/json" \
-H "X-Authorization: {api_token}" \
-H "X-Partner: {partner_token}" \
-H "X-Client: {client_slug}" \
-d '{"school_cnpj": "26019466000122", "responsible_cpf": "123.456.789-00", "checked_in_at": "2026-04-29 17:10:00"}'
use GuzzleHttp\Client; $client = new Client(); $response = $client->post('https://toakiescola.com.br/api/v1/tssv/responsible/check/in', [ 'headers' => [ 'X-Authorization' => '{api_token}', 'X-Partner' => '{partner_token}', 'X-Client' => '{client_slug}', ], 'json' => [ 'school_cnpj' => '26019466000122', 'responsible_cpf' => '123.456.789-00', 'checked_in_at' => '2026-04-29 17:10:00', ], ]); $data = json_decode($response->getBody(), true);
const response = await fetch('https://toakiescola.com.br/api/v1/tssv/responsible/check/in', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Authorization': '{api_token}', 'X-Partner': '{partner_token}', 'X-Client': '{client_slug}', }, body: JSON.stringify({ school_cnpj: '26019466000122', responsible_cpf: '123.456.789-00', checked_in_at: '2026-04-29 17:10:00', }), }); const data = await response.json();
{ "success": true, "students": [ { "enrollment_number": "7.553", "name": "João da Silva", "photo": "iVBORw0KGgoAAAANS..." } ] }
{ "message": "Autenticação inválida." }
{ "message": "Recurso não encontrado." }
{ "success": true, "message": "O responsável não possui alunos para custódia no dia de hoje." }
{ "message": "Autenticação inválida." }
{ "message": "Recurso não encontrado." }
{ "success": false, "errors": [ "O responsável informado não foi encontrado." ] }
Como funciona a verificação criptográfica
O check-in do responsável via app móvel usa criptografia assimétrica vinculada à biometria do dispositivo (Face ID / digital). Em vez de transmitir dados biométricos — o que é arquiteturalmente impossível e ilegal — o dispositivo assina um desafio emitido pelo servidor. O backend verifica a assinatura com a chave pública previamente registrada.
-
1
Geração do par de chaves no dispositivo
O app gera um par de chaves EC (P-256) dentro do Secure Enclave (iOS) ou Android Keystore com a flag
userPresence = true. Isso garante que a chave privada só pode ser utilizada após biometria bem-sucedida (Face ID ou digital). A chave privada jamais sai do chip. -
2
Registro da chave pública no servidor
A chave pública (PEM) é enviada uma única vez para
POST .../device/registere armazenada vinculada ao responsável e aodevice_id. -
3
Obtenção do desafio (nonce) — antes de cada check-in
O app solicita um nonce em
GET .../device/challenge. O servidor gera um valor aleatório de 32 bytes (base64), armazena em cache com TTL de 5 minutos e o retorna. Esse valor é de uso único. -
4
Assinatura do desafio (biometria exigida pelo SO)
O app tenta assinar o nonce com a chave privada. O SO apresenta Face ID / digital ao usuário. Só com biometria aprovada a assinatura é gerada. O app envia
device_id,challengeesignatureno corpo do check-in. -
5
Verificação no servidor
O backend busca a chave pública registrada para o par
responsible + device_id, confirma que o nonce bate com o que foi emitido e chamaopenssl_verify(). Se a verificação falhar, o check-in é recusado com403.
Retrocompatibilidade: enquanto nenhum dispositivo for registrado para um responsável, o check-in funciona sem assinatura. Assim que o primeiro dispositivo for registrado, o servidor passa a exigir assinatura válida em todos os check-ins desse responsável.
let access = SecAccessControlCreateWithFlags( nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, [.privateKeyUsage, .userPresence], nil )! let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeySizeInBits as String: 256, kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true, kSecAttrApplicationTag as String: "br.com.toakiescola.tssv", kSecAttrAccessControl as String: access, ], ] var error: Unmanaged<CFError>? let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error)! let publicKey = SecKeyCopyPublicKey(privateKey)!
val keyPairGenerator = KeyPairGenerator .getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") keyPairGenerator.initialize( KeyGenParameterSpec.Builder( "tssv_key", KeyProperties.PURPOSE_SIGN ) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(true) .setInvalidatedByBiometricEnrollment(true) .build() ) val keyPair = keyPairGenerator.generateKeyPair() // keyPair.public → enviar como PEM para o servidor
// expo install expo-crypto expo-secure-store expo-local-authentication import * as Crypto from 'expo-crypto'; import * as SecureStore from 'expo-secure-store'; import * as LocalAuthentication from 'expo-local-authentication'; // 1. Gerar par de chaves EC P-256 e persistir a chave privada const keyPair = await Crypto.generateKeyPairAsync( 'EC', { namedCurve: 'P-256', extractable: false }, ); // 2. Exportar chave pública como PEM para enviar ao servidor const publicKeyPem = await Crypto.exportKeyAsync('spki', keyPair.publicKey); // 3. Persistir a chave privada de forma segura await SecureStore.setItemAsync('tssv_private_key', keyPair.privateKey, { requireAuthentication: true, }); // 4. Na hora do check-in: solicitar autenticação biométrica e assinar o challenge const biometricResult = await LocalAuthentication.authenticateAsync({ promptMessage: 'Confirme sua identidade para o check-in', }); if (!biometricResult.success) throw new Error('Autenticação biométrica cancelada'); const privateKey = await SecureStore.getItemAsync('tssv_private_key', { requireAuthentication: true, }); const signature = await Crypto.signAsync( 'SHA-256', challenge, // string base64 obtida de /device/challenge privateKey, ); // signature → enviar como base64 no campo "signature"
/api/v1/pedagogic/attendance/responsible/device/register
Registrar chave pública do dispositivo
Registra (ou atualiza) a chave pública EC de um dispositivo móvel para o responsável autenticado. Após o registro, o check-in desse responsável passará a exigir assinatura biométrica. Requer bearer token do responsável.
Atributos obrigatórios (body JSON)
device_id
string (UUID)
obrigatório
UUID gerado pelo app na primeira instalação. Identifica o dispositivo de forma única por responsável.
public_key
string (PEM)
obrigatório
Chave pública EC P-256 exportada no formato PEM (-----BEGIN PUBLIC KEY-----).
platform
string
obrigatório
Plataforma do dispositivo: ios ou android.
device_name
string
obrigatório
Nome legível do dispositivo (ex: "iPhone 15 de Maria"). Exibido para o usuário na tela de gerenciamento de dispositivos.
Códigos de resposta
200
Sucesso.
400
Dados inválidos.
401
Autenticação inválida.
404
Recurso de referência não encontrado.
curl .../api/v1/pedagogic/attendance/responsible/device/register \ -H "Authorization: Bearer {bearer_token}" \ -H "Content-Type: application/json" \ -d '{ "device_id": "550e8400-e29b-41d4-a716-446655440000", "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYH...\n-----END PUBLIC KEY-----", "platform": "ios", "device_name": "iPhone 15 de Maria" }'
{ "message": "Dispositivo registrado com sucesso." }
{ "message": "Dados inválidos." }
{ "message": "Autenticação inválida." }
{ "message": "Recurso de referência não encontrado." }
/api/v1/pedagogic/attendance/responsible/device/challenge
Obter desafio para assinatura
Retorna um nonce aleatório (32 bytes, base64) vinculado ao responsável e ao dispositivo. O nonce expira em 5 minutos e é consumido na primeira utilização — não pode ser reutilizado. Chame este endpoint imediatamente antes de solicitar a assinatura biométrica ao usuário.
Parâmetros de query
device_id
string (UUID)
obrigatório
O mesmo device_id usado no cadastro do dispositivo.
Códigos de resposta
200
Sucesso.
401
Autenticação inválida.
404
Recurso não encontrado.
curl .../api/v1/pedagogic/attendance/responsible/device/challenge?device_id=550e8400-... \ -H "Authorization: Bearer {bearer_token}"
{ "challenge": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ==", "expires_at": "2026-05-01T17:15:00+00:00" }
{ "message": "Autenticação inválida." }
{ "message": "Recurso não encontrado." }
/api/v1/pedagogic/attendance/responsible/check/in
Check-in do responsável com app móvel
Endpoint de check-in do responsável para o app móvel. Requer autenticação por Bearer Token.
Quando o responsável possui um dispositivo registrado, os campos device_id,
challenge e
signature são obrigatórios.
O servidor verifica a assinatura ECDSA via
openssl_verify()
com SHA-256 antes de processar o check-in.
Este endpoint usa autenticação por Bearer Token. Para check-in via totem biométrico, veja Check-in com facial.
Campos adicionais no body JSON
Aviso de descontinuação — obrigatoriedade a partir de 1º de julho de 2026
Os campos device_id, challenge e signature são condicionais até 30 de junho de 2026.
A partir de 1º de julho de 2026, o envio desses campos passará a ser obrigatório para todos os responsáveis, independentemente de possuírem dispositivo registrado.
Utilize este período de transição para implementar o fluxo de registro biométrico e assinatura de challenge no seu aplicativo.
device_id
string
condicional
Obrigatório se o responsável tiver dispositivo registrado.
challenge
string (base64)
condicional
O nonce obtido em /device/challenge imediatamente antes.
signature
string (base64)
condicional
Assinatura SHA-256 do challenge, gerada pela chave privada biométrica do dispositivo, codificada em base64.
Dica para o app móvel: assine a string do challenge diretamente
(não em UTF-8 nem re-encode). No iOS, use
SecKeyCreateSignature com
ecdsaSignatureMessageX962SHA256.
No Android, use Signature.getInstance("SHA256withECDSA")
e converta o resultado para base64.
Códigos de resposta
200
Sucesso.
400
Dados inválidos.
401
Autenticação inválida.
404
Recurso de referência não encontrado.
curl .../api/v1/pedagogic/attendance/responsible/check/in \ -H "Authorization: Bearer {bearer_token}" \ -H "Content-Type: application/json" \ -d '{ "device_id": "550e8400-e29b-41d4-a716-446655440000", "challenge": "dGhpcyBpcyBhIHJhbmRvbSBub25jZQ==", "signature": "MEUCIQD3...base64-encoded-ECDSA-signature..." }'
{ "success": true, "students": [ { "enrollment_number": "7.553", "name": "João da Silva", "photo": "iVBORw0KGgoAAAANS..." } ] }
{ "message": "Dados inválidos." }
{ "message": "Autenticação inválida." }
{ "message": "Assinatura inválida. Autenticação biométrica recusada." }
{ "message": "Recurso de referência não encontrado." }