{"openapi":"3.1.0","info":{"title":"Facial Access API","version":"1.0.0","description":"API para gestao de cadastro facial e controle de acessos","contact":{"name":"Preddita","email":"contato@preddita.com.br"}},"servers":[{"url":"http://localhost:3000","description":"Development"},{"url":"https://acesso.preddita.com.br","description":"Production"}],"tags":[{"name":"Health","description":"Endpoints de saude da API"},{"name":"Auth","description":"Autenticacao e autorizacao"},{"name":"Clients","description":"Gestao de clientes"},{"name":"Devices","description":"Gestao de dispositivos Control iD"},{"name":"Controllers","description":"Gestao de controladores MQTT"},{"name":"Access","description":"Gestao de acessos, links de cadastro e convites de visitante"},{"name":"Analytics","description":"Coleta e visualizacao de dados"},{"name":"Partner","description":"Integracao com sistemas parceiros (ex: BeePay)"},{"name":"Integracao","description":"Integracao com sistemas de RH/HCM: cadastro de pessoas, links de visitante e envio de eventos de acesso. Contrato neutro de fornecedor — identificadores proprietarios de cada sistema trafegam no objeto livre `attributes`. Endpoints marcados [ESPEC] estao documentados mas ainda nao implementados."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Chave de API do espaco, enviada junto com o header X-Tenant-Slug. Gerada no painel administrativo."},"SessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Cookie de sessao emitido no login. Usado pelas rotas do painel e do portal."}},"schemas":{"PersonIdentity":{"type":"object","properties":{"externalId":{"type":"string","minLength":1,"maxLength":100,"description":"Identificador da pessoa no sistema de origem. E a chave de correlacao entre os dois sistemas e deve ser estavel ao longo do tempo.","example":"EMP-4521"},"registration":{"type":"string","maxLength":50,"description":"Matricula funcional.","example":"4521"},"badgeCode":{"type":"string","maxLength":50,"description":"Codigo do cracha/credencial ativo. Necessario quando o sistema de origem correlaciona os eventos de acesso por cracha em vez de matricula.","example":"0000004521"},"companyCode":{"type":"string","maxLength":50,"description":"Codigo da empresa ou unidade a que a pessoa pertence no sistema de origem.","example":"1"},"departmentCode":{"type":"string","maxLength":50,"description":"Codigo do departamento."},"costCenter":{"type":"string","maxLength":50,"description":"Centro de custo."}},"required":["externalId"]},"AccessWindow":{"type":"object","properties":{"type":{"type":"string","enum":["permanent","temporary","single_use"],"default":"permanent","description":"Natureza do acesso concedido."},"validFrom":{"type":"string","format":"date-time","description":"Inicio da validade do acesso (ISO 8601).","example":"2026-03-01T00:00:00.000Z"},"validUntil":{"type":["string","null"],"format":"date-time","description":"Fim da validade do acesso (ISO 8601). `null` para acesso sem prazo."},"allowedHours":{"type":"object","properties":{"start":{"type":"string","example":"07:00"},"end":{"type":"string","example":"19:00"},"days":{"type":"array","items":{"type":"integer","minimum":0,"maximum":6},"description":"Dias da semana permitidos, 0 = domingo.","example":[1,2,3,4,5]}},"required":["start","end","days"],"description":"Janela de horario permitida."}}},"Person":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Identificador da pessoa no nosso sistema."},"identity":{"$ref":"#/components/schemas/PersonIdentity"},"personType":{"type":"string","enum":["employee","contractor","partner","visitor"],"description":"Vinculo da pessoa com o espaco. `employee` colaborador proprio, `contractor` terceiro, `partner` parceiro, `visitor` visitante.","example":"employee"},"name":{"type":"string"},"status":{"type":"string","enum":["active","leave","terminated"]},"blocked":{"type":"boolean","description":"Se o acesso esta bloqueado no momento."},"photoPending":{"type":"boolean","description":"Verdadeiro quando ainda nao ha foto facial — a pessoa nao passa no dispositivo."},"registrationUrl":{"type":["string","null"],"format":"uri","description":"URL para o titular concluir o cadastro. Preenchida somente quando `photoPending`."},"access":{"$ref":"#/components/schemas/AccessWindow"},"devices":{"type":"array","items":{"type":"object","properties":{"deviceName":{"type":"string","example":"Catraca Portaria"},"synced":{"type":"boolean","description":"Se a face ja foi gravada no dispositivo."},"lastSyncAt":{"type":["string","null"],"format":"date-time"},"error":{"type":["string","null"],"description":"Ultima falha de sincronizacao, se houver."}},"required":["deviceName","synced","lastSyncAt","error"]},"description":"Situacao da pessoa em cada dispositivo do espaco."},"lastAccessAt":{"type":["string","null"],"format":"date-time","description":"Data e hora do ultimo acesso registrado."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"attributes":{"type":"object","additionalProperties":{},"description":"Campos livres do sistema de origem. E o ponto de extensao do contrato: identificadores e codigos proprietarios de cada ERP/RH ficam aqui, sem alterar o restante do payload.","example":{"matriculaInterna":"4521","turno":"A"}}},"required":["id","identity","personType","name","status","blocked","photoPending","registrationUrl","access","devices","lastAccessAt","createdAt","updatedAt"]},"PersonEnvelope":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/Person"},"action":{"type":"string","enum":["created","updated","linked"],"description":"`created` pessoa nova, `updated` cadastro existente atualizado, `linked` a pessoa ja existia em outro espaco e passou a ter acesso a este, sem duplicar o registro."}},"required":["item","action"]},"ValidationErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"Validation Error"},"message":{"type":"string","example":"Dados invalidos"},"details":{"type":"object","additionalProperties":{},"description":"Erros por campo, no formato do validador"}},"required":["error","message"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"Not Found"},"message":{"type":"string","example":"Pessoa nao encontrada"}},"required":["error","message"]},"PersonDocument":{"type":"object","properties":{"type":{"type":"string","enum":["cpf","passport","foreign_id"],"default":"cpf","description":"Tipo do documento de identificacao."},"number":{"type":"string","minLength":1,"maxLength":30,"description":"Numero do documento, apenas digitos quando `cpf`.","example":"12345678900"}},"required":["number"],"description":"Documento usado para identificar a pessoa. E a chave de deduplicacao."},"PersonUpsertRequest":{"type":"object","properties":{"identity":{"$ref":"#/components/schemas/PersonIdentity"},"personType":{"type":"string","enum":["employee","contractor","partner","visitor"],"default":"employee","description":"Vinculo da pessoa com o espaco. `employee` colaborador proprio, `contractor` terceiro, `partner` parceiro, `visitor` visitante.","example":"employee"},"name":{"type":"string","minLength":2,"maxLength":200,"example":"Joao da Silva"},"document":{"$ref":"#/components/schemas/PersonDocument"},"email":{"type":"string","format":"email","example":"joao.silva@empresa.com.br"},"phone":{"type":"string","maxLength":20,"example":"47999998888"},"birthDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"1990-05-20"},"status":{"type":"string","enum":["active","leave","terminated"],"default":"active","description":"Situacao no sistema de origem. `leave` bloqueia o acesso preservando o cadastro e a face no dispositivo; `terminated` encerra o acesso."},"photoBase64":{"type":"string","description":"Foto facial em base64 (JPEG ou PNG), sem o prefixo `data:image/...`. Quando ausente, a pessoa e criada com cadastro incompleto e a resposta traz `photoPending: true` e uma URL para o proprio titular concluir o cadastro. **Sem foto a pessoa nao e reconhecida no dispositivo e portanto nao gera eventos de acesso.**"},"access":{"$ref":"#/components/schemas/AccessWindow"},"attributes":{"type":"object","additionalProperties":{},"description":"Campos livres do sistema de origem. E o ponto de extensao do contrato: identificadores e codigos proprietarios de cada ERP/RH ficam aqui, sem alterar o restante do payload.","example":{"matriculaInterna":"4521","turno":"A"}}},"required":["identity","name","document"]},"VisitorLink":{"type":"object","properties":{"token":{"type":"string","minLength":32,"maxLength":32,"description":"Token do link. Formato `^[A-Za-z0-9]{32}$`.","example":"aB3xY7kQ1mZ9pL2vN8cR4tS6wD0eF5gH"},"url":{"type":"string","format":"uri","description":"URL pronta para entregar ao visitante.","example":"https://acesso.preddita.com.br/register/matriz-sao-jose/aB3xY7kQ1mZ9pL2vN8cR4tS6wD0eF5gH"},"status":{"type":"string","enum":["active","used","expired","revoked"],"description":"Situacao atual do link."},"maxUses":{"type":"integer"},"currentUses":{"type":"integer"},"expiresAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"person":{"allOf":[{"$ref":"#/components/schemas/Person"},{"type":["object","null"],"description":"Pessoa criada a partir do link, quando ja utilizado."}]}},"required":["token","url","status","maxUses","currentUses","expiresAt","createdAt","person"]},"VisitorLinkRequest":{"type":"object","properties":{"expiresAt":{"type":"string","format":"date-time","description":"Momento em que o link deixa de ser valido. Sem valor, o link nao expira.","example":"2026-08-05T18:00:00.000Z"},"maxUses":{"type":"integer","exclusiveMinimum":0,"default":1,"description":"Quantidade maxima de cadastros que o link aceita."},"access":{"allOf":[{"$ref":"#/components/schemas/AccessWindow"},{"description":"Validade e janela de horario concedidas a quem se cadastrar pelo link."}]},"guest":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":20}},"description":"Dados conhecidos do visitante, usados para pre-preencher o formulario."},"attributes":{"type":"object","additionalProperties":{},"description":"Campos livres do sistema de origem. E o ponto de extensao do contrato: identificadores e codigos proprietarios de cada ERP/RH ficam aqui, sem alterar o restante do payload.","example":{"matriculaInterna":"4521","turno":"A"}}}},"SpaceBranding":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"logoUrl":{"type":["string","null"]},"primaryColor":{"type":["string","null"]},"buttonActiveColor":{"type":["string","null"]},"buttonInactiveColor":{"type":["string","null"]},"secondaryActiveColor":{"type":["string","null"]},"extraFieldsConfig":{"type":["array","null"],"items":{"type":"object","additionalProperties":{}},"description":"Campos extras que o formulario de cadastro deve exibir."},"serproEnabled":{"type":"boolean"},"blockMinorsEnabled":{"type":"boolean"},"spaceType":{"type":["string","null"]}},"required":["id","name","slug","logoUrl","primaryColor","buttonActiveColor","buttonInactiveColor","secondaryActiveColor","extraFieldsConfig","serproEnabled","blockMinorsEnabled","spaceType"]},"AccessLink":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenantId":{"type":"string","format":"uuid"},"token":{"type":"string","minLength":32,"maxLength":32},"linkType":{"type":"string","enum":["permanent","temporary","single_use","invitation"],"description":"Natureza do link. `invitation` e o link de visitante criado por um morador/colaborador para um convidado."},"accessType":{"type":"string","enum":["permanent","temporary","single_use"],"description":"Tipo de acesso concedido a quem se cadastrar pelo link."},"createdBy":{"type":["string","null"],"format":"uuid","description":"Cliente que criou o link. Preenchido apenas em convites de visitante; links criados por moderador ou administrador trazem `null`."},"createdByRole":{"type":"string","enum":["client","moderator","admin"]},"validFrom":{"type":["string","null"],"format":"date-time"},"validUntil":{"type":["string","null"],"format":"date-time"},"allowedHours":{"type":["object","null"],"properties":{"start":{"type":"string","example":"07:00"},"end":{"type":"string","example":"19:00"},"days":{"type":"array","items":{"type":"integer"},"example":[1,2,3,4,5]}},"required":["start","end","days"]},"maxUses":{"type":["integer","null"]},"currentUses":{"type":"integer"},"targetDevices":{"type":"array","items":{"type":"string"}},"prefillData":{"type":"object","additionalProperties":{}},"isActive":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"}},"required":["id","tenantId","token","linkType","accessType","createdBy","createdByRole","validFrom","validUntil","allowedHours","maxUses","currentUses","targetDevices","prefillData","isActive","createdAt","expiresAt"]},"Invitation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"guestName":{"type":["string","null"]},"guestEmail":{"type":["string","null"]},"guestPhone":{"type":["string","null"]},"status":{"type":"string","enum":["pending","used","expired","revoked"],"description":"Situacao do convite. Ver a observacao de comportamento atual na descricao do endpoint."},"token":{"type":["string","null"],"minLength":32,"maxLength":32,"description":"Token do link associado, usado para montar a URL do convite."},"createdAt":{"type":"string","format":"date-time"},"usedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","guestName","guestEmail","guestPhone","status","token","createdAt","usedAt"]}},"parameters":{}},"paths":{"/api/v1/health":{"get":{"tags":["Health"],"summary":"Verificar saude da API","description":"Retorna o status de saude da API e seus servicos dependentes","responses":{"200":{"description":"API saudavel","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"timestamp":{"type":"string","format":"date-time"},"version":{"type":"string"},"uptime":{"type":"number"},"services":{"type":"object","properties":{"database":{"type":"string","enum":["connected","disconnected","unknown"]},"redis":{"type":"string","enum":["connected","disconnected","unknown"]},"controlid":{"type":"string","enum":["connected","disconnected","unknown"]}},"required":["database","redis","controlid"]}},"required":["status","timestamp","version","uptime","services"]}}}},"503":{"description":"API com problemas","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"timestamp":{"type":"string","format":"date-time"},"version":{"type":"string"},"uptime":{"type":"number"},"services":{"type":"object","properties":{"database":{"type":"string","enum":["connected","disconnected","unknown"]},"redis":{"type":"string","enum":["connected","disconnected","unknown"]},"controlid":{"type":"string","enum":["connected","disconnected","unknown"]}},"required":["database","redis","controlid"]}},"required":["status","timestamp","version","uptime","services"]}}}}}}},"/api/v1/ready":{"get":{"tags":["Health"],"summary":"Verificar se API esta pronta","description":"Retorna se a API esta pronta para receber requisicoes","responses":{"200":{"description":"API pronta","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean"}},"required":["ready"]}}}},"503":{"description":"API nao esta pronta","content":{"application/json":{"schema":{"type":"object","properties":{"ready":{"type":"boolean"},"reason":{"type":"string"}},"required":["ready"]}}}}}}},"/api/v1/live":{"get":{"tags":["Health"],"summary":"Verificar se API esta viva","description":"Retorna simplesmente se a API esta respondendo","responses":{"200":{"description":"API viva","content":{"application/json":{"schema":{"type":"object","properties":{"alive":{"type":"boolean"}},"required":["alive"]}}}}}}},"/api/v1/partner/register":{"post":{"tags":["Partner"],"summary":"Cadastrar cliente via parceiro","description":"Registra um cliente enviado por um sistema parceiro (ex: BeePay). A senha é derivada automaticamente da data de nascimento no formato DDMMYYYY. Ignora restrição link_only — cadastros via parceiro são sempre autorizados. Autenticação: header X-API-Key com o valor configurado para o espaço no painel de administração.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","minLength":1,"description":"Slug do espaco/tenant","example":"meu-condominio"},"name":{"type":"string","minLength":2,"description":"Nome completo do cliente","example":"João Silva"},"email":{"type":"string","format":"email","description":"E-mail do cliente","example":"joao@email.com"},"phone":{"type":"string","minLength":1,"description":"Telefone (com DDD)","example":"11999990000"},"cpf":{"type":"string","description":"CPF (apenas dígitos, opcional)","example":"12345678901"},"dateOfBirth":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Data de nascimento (YYYY-MM-DD). Usada para derivar a senha (DDMMYYYY).","example":"1990-05-20"},"accessType":{"type":"string","enum":["permanent","temporary","single_use"],"default":"permanent","description":"Tipo de acesso","example":"permanent"},"metadata":{"type":"object","additionalProperties":{},"description":"Dados adicionais em formato livre"},"photoBase64":{"type":"string","minLength":1,"description":"Foto facial em base64 (JPEG ou PNG)"}},"required":["tenantSlug","name","email","phone","dateOfBirth","photoBase64"]}}}},"responses":{"201":{"description":"Cliente cadastrado com sucesso","content":{"application/json":{"schema":{"type":"object","properties":{"client":{"type":"object","additionalProperties":{},"description":"Dados do cliente cadastrado"}},"required":["client"]}}}},"400":{"description":"Dados inválidos","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{}}},"required":["error","message"]}}}},"401":{"description":"API key ausente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Integração de parceiro não habilitada para este espaço ou chave incorreta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Tenant não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"409":{"description":"Cliente já cadastrado (e-mail ou CPF duplicado)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/partner/devices":{"get":{"tags":["Partner"],"summary":"Listar dispositivos do tenant","description":"Retorna os dispositivos registrados para o tenant identificado pelo slug. Use os valores de `deviceId` ao registrar webhooks.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Slug do espaco/tenant"},"required":true,"name":"tenantSlug","in":"query"}],"responses":{"200":{"description":"Lista de dispositivos","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"UUID interno do dispositivo"},"deviceId":{"type":"string","description":"ID físico do dispositivo (use este campo ao registrar webhooks)"},"deviceName":{"type":["string","null"],"description":"Nome do dispositivo"},"partnerWebhookUrl":{"type":["string","null"],"description":"URL de webhook atualmente configurada, ou null se não houver"}},"required":["id","deviceId","deviceName","partnerWebhookUrl"]}}},"required":["devices"]}}}},"401":{"description":"API key ausente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Integração não habilitada ou chave incorreta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Tenant não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/partner/webhooks":{"post":{"tags":["Partner"],"summary":"Registrar webhook de abertura de porta","description":"Registra (ou atualiza) a URL que receberá notificações sempre que um acesso for liberado no dispositivo informado. Use `deviceId` retornado pelo endpoint `GET /partner/devices`.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","minLength":1,"description":"Slug do espaco/tenant","example":"meu-condominio"},"deviceId":{"type":"string","minLength":1,"description":"ID físico do dispositivo Control iD","example":"dev-001"},"webhookUrl":{"type":"string","format":"uri","description":"URL que receberá as notificações de abertura","example":"https://beepay.com.br/webhook/abc"}},"required":["tenantSlug","deviceId","webhookUrl"]}}}},"responses":{"200":{"description":"Webhook registrado com sucesso","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"401":{"description":"API key ausente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Integração não habilitada ou chave incorreta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Tenant ou dispositivo não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}},"delete":{"tags":["Partner"],"summary":"Remover webhook de abertura de porta","description":"Remove a URL de webhook cadastrada para o dispositivo.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tenantSlug":{"type":"string","minLength":1,"description":"Slug do espaco/tenant","example":"meu-condominio"},"deviceId":{"type":"string","minLength":1,"description":"ID físico do dispositivo Control iD","example":"dev-001"}},"required":["tenantSlug","deviceId"]}}}},"responses":{"200":{"description":"Webhook removido com sucesso","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"401":{"description":"API key ausente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Integração não habilitada ou chave incorreta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Tenant ou dispositivo não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/integration/persons":{"post":{"tags":["Integracao"],"summary":"[ESPEC] Cadastrar ou atualizar pessoa","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nCria a pessoa quando o documento ainda nao existe no espaco e atualiza quando ja existe — o sistema de origem pode reenviar o mesmo registro sem se preocupar em distinguir os casos.\n\n**Deduplicacao:** o `document.number` e a chave. Se a pessoa ja existir em outro espaco, o cadastro nao e duplicado; ela apenas passa a ter acesso a este espaco (`action: \"linked\"`).\n\n**Efeito nos dispositivos:** a face so e reenviada aos leitores quando o nome ou a foto mudam. Alteracoes de cracha, e-mail, telefone ou atributos nao geram trafego para o dispositivo.\n\n**Situacao:** `status: \"leave\"` remove a pessoa do grupo de acesso mas mantem a face gravada, de modo que o retorno e imediato e nao exige reenvio de foto. `status: \"terminated\"` encerra o acesso.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonUpsertRequest"}}}},"responses":{"200":{"description":"Pessoa atualizada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonEnvelope"}}}},"201":{"description":"Pessoa criada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonEnvelope"}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Documento ja vinculado a outra identidade neste espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Integracao"],"summary":"[ESPEC] Listar pessoas","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nListagem paginada com filtros. Use `updatedSince` para sincronizacao incremental e `status=unlinked` para encontrar quem se cadastrou no proprio espaco e ainda nao tem identidade no sistema de origem — essas pessoas acessam normalmente, mas os eventos delas ficam retidos ate receberem um `identity.externalId`.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Filtra por numero de documento."},"required":false,"name":"document","in":"query"},{"schema":{"type":"string","enum":["employee","contractor","partner","visitor"],"description":"Vinculo da pessoa com o espaco. `employee` colaborador proprio, `contractor` terceiro, `partner` parceiro, `visitor` visitante.","example":"employee"},"required":false,"name":"personType","in":"query"},{"schema":{"type":"string","enum":["active","leave","terminated","unlinked"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Retorna apenas registros alterados a partir desta data (ISO 8601)."},"required":false,"name":"updatedSince","in":"query"},{"schema":{"type":"integer","exclusiveMinimum":0,"default":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","exclusiveMinimum":0,"maximum":100,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"responses":{"200":{"description":"Lista de pessoas","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Person"}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"}},"required":["page","limit","total"]}},"required":["items","pagination"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/integration/persons/{externalId}":{"get":{"tags":["Integracao"],"summary":"[ESPEC] Consultar pessoa","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nRetorna o cadastro, a situacao do acesso e o estado de sincronizacao em cada dispositivo do espaco. Use para conferir se a pessoa esta apta a passar no leitor: `photoPending: true` ou `devices[].synced: false` indicam que ela ainda nao sera reconhecida.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Identificador da pessoa no sistema de origem.","example":"EMP-4521"},"required":true,"name":"externalId","in":"path"},{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"responses":{"200":{"description":"Pessoa encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/Person"}},"required":["item"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/integration/visitor-links":{"post":{"tags":["Integracao"],"summary":"[ESPEC] Gerar link de cadastro para visitante","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nGera um link de cadastro para entregar ao visitante — por e-mail, mensagem ou QR Code. O visitante abre a URL, informa os dados e captura a propria foto; ao concluir, o cadastro e sincronizado nos dispositivos do espaco e o webhook `person-registered` e disparado.\n\nE a alternativa ao envio de `photoBase64` em `POST /persons`: em vez de o sistema de origem obter a foto, o proprio visitante a fornece.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitorLinkRequest"}}}},"responses":{"201":{"description":"Link gerado","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/VisitorLink"}},"required":["item"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/integration/visitor-links/{token}":{"get":{"tags":["Integracao"],"summary":"[ESPEC] Consultar link de visitante","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nInforma se o link ainda esta valido e, quando ja utilizado, devolve a pessoa criada a partir dele.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":32,"maxLength":32,"description":"Token do link."},"required":true,"name":"token","in":"path"},{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"responses":{"200":{"description":"Situacao do link","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/VisitorLink"}},"required":["item"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Integracao"],"summary":"[ESPEC] Revogar link de visitante","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\nInvalida o link. Nao afeta quem ja se cadastrou por ele — para encerrar esse acesso, envie a pessoa com `status: \"terminated\"` em `POST /persons`.","security":[{"ApiKeyAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":32,"maxLength":32},"required":true,"name":"token","in":"path"},{"schema":{"type":"string","description":"Chave de API do espaco, gerada no painel administrativo.","example":"k_9fA3…"},"required":true,"name":"X-API-Key","in":"header"},{"schema":{"type":"string","description":"Identificador do espaco (slug).","example":"matriz-sao-jose"},"required":true,"name":"X-Tenant-Slug","in":"header"}],"responses":{"200":{"description":"Link revogado","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Link revogado"}},"required":["message"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"API key ausente ou invalida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Integracao nao habilitada para este espaco","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Espaco ou pessoa nao encontrada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Erro interno do servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/access-links/validate/{token}":{"get":{"tags":["Access"],"summary":"Validar link de cadastro","description":"Endpoint publico consumido pelo formulario de cadastro ao abrir um link de convite. Devolve a identidade visual e a configuracao do espaco, de modo que o formulario ja carrega personalizado.\n\nA URL entregue ao visitante tem o formato `https://<origem>/register/{slug-do-espaco}/{token}`. O token tem 32 caracteres no padrao `^[A-Za-z0-9]{32}$`.\n\nO espaco e derivado do proprio token — o slug presente na URL e apenas informativo.","parameters":[{"schema":{"type":"string","minLength":32,"maxLength":32},"required":true,"name":"token","in":"path"}],"responses":{"200":{"description":"Link valido","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true]},"linkType":{"type":"string","enum":["permanent","temporary","single_use","invitation"],"description":"Natureza do link. `invitation` e o link de visitante criado por um morador/colaborador para um convidado."},"accessType":{"type":"string","enum":["permanent","temporary","single_use"],"description":"Tipo de acesso concedido a quem se cadastrar pelo link."},"prefillData":{"type":"object","additionalProperties":{},"description":"Dados que serao mesclados ao cadastro criado a partir do link."},"tenant":{"$ref":"#/components/schemas/SpaceBranding"},"allowedHours":{"type":["object","null"],"properties":{"start":{"type":"string","example":"07:00"},"end":{"type":"string","example":"19:00"},"days":{"type":"array","items":{"type":"integer"},"example":[1,2,3,4,5]}},"required":["start","end","days"]},"validFrom":{"type":["string","null"],"format":"date-time"},"validUntil":{"type":["string","null"],"format":"date-time"}},"required":["valid","linkType","accessType","prefillData","tenant","allowedHours","validFrom","validUntil"]}}}},"404":{"description":"Link nao encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"410":{"description":"Link desativado, expirado ou com limite de uso atingido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/access-links/tenant/{slug}":{"get":{"tags":["Access"],"summary":"Obter dados de cadastro do espaco","description":"Endpoint publico usado quando o cadastro e aberto sem link, direto pelo slug do espaco (`/register/{slug}`). Devolve a mesma identidade visual de `GET /validate/{token}`, mais `tabletModeEnabled`.\n\nEspacos com `registrationMode = link_only` nao tem pagina publica de cadastro e respondem 403 com `reason: \"link_only\"`. O acesso por convite continua sendo feito via `GET /validate/{token}`.","parameters":[{"schema":{"type":"string","example":"matriz-sao-jose"},"required":true,"name":"slug","in":"path"}],"responses":{"200":{"description":"Espaco encontrado e com cadastro habilitado","content":{"application/json":{"schema":{"type":"object","properties":{"tenant":{"allOf":[{"$ref":"#/components/schemas/SpaceBranding"},{"type":"object","properties":{"tabletModeEnabled":{"type":"boolean"}},"required":["tabletModeEnabled"]}]}},"required":["tenant"]}}}},"403":{"description":"Cadastro desabilitado para o espaco, ou espaco `link_only` (corpo traz `reason: \"link_only\"`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Espaco nao encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/access-links":{"post":{"tags":["Access"],"summary":"Criar link de cadastro","description":"Cria um link de cadastro para o espaco. Use `linkType: \"invitation\"` para links de visitante.\n\nA URL entregue ao visitante tem o formato `https://<origem>/register/{slug-do-espaco}/{token}`. O token tem 32 caracteres no padrao `^[A-Za-z0-9]{32}$`.\n\nRequer sessao autenticada com papel `moderator` ou `admin`. Um moderador so pode criar links para espacos aos quais esta atribuido; um administrador pode informar qualquer `tenantId`.\n\n> **Comportamento atual:** `maxUses` e `single_use` **nao sao aplicados** no cadastro pelo formulario publico — `POST /clients/register` aceita o token sem revalidar e sem incrementar `currentUses`. O limite so e respeitado em `POST /access-links/enroll` e `POST /clients/enroll-by-cpf`. Da mesma forma, `targetDevices`, `allowedHours`, `validFrom` e `validUntil` sao aceitos, gravados e devolvidos, porem **nao restringem o acesso**.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid","description":"Espaco alvo. Sem valor, usa o espaco do usuario autenticado."},"linkType":{"type":"string","enum":["permanent","temporary","single_use","invitation"],"description":"Natureza do link. `invitation` e o link de visitante criado por um morador/colaborador para um convidado."},"accessType":{"type":"string","enum":["permanent","temporary","single_use"],"description":"Tipo de acesso concedido a quem se cadastrar pelo link."},"validFrom":{"type":"string"},"validUntil":{"type":"string"},"allowedHours":{"type":"object","properties":{"start":{"type":"string","example":"07:00"},"end":{"type":"string","example":"19:00"},"days":{"type":"array","items":{"type":"integer"},"example":[1,2,3,4,5]}},"required":["start","end","days"]},"maxUses":{"type":"integer","exclusiveMinimum":0,"description":"Padrao 1."},"targetDevices":{"type":"array","items":{"type":"string"}},"prefillData":{"type":"object","additionalProperties":{},"description":"Dados mesclados ao cadastro criado a partir do link."},"expiresAt":{"type":"string"}},"required":["linkType","accessType"]}}}},"responses":{"201":{"description":"Link criado","content":{"application/json":{"schema":{"type":"object","properties":{"link":{"$ref":"#/components/schemas/AccessLink"},"token":{"type":"string","minLength":32,"maxLength":32}},"required":["link","token"]}}}},"400":{"description":"Dados invalidos","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Sem permissao ou sem acesso ao espaco informado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}},"get":{"tags":["Access"],"summary":"Listar links de cadastro","description":"Lista os links do espaco. Moderadores veem os links do proprio espaco e dos espacos a que estao atribuidos; administradores veem todos.\n\n> **Comportamento atual:** a paginacao nao devolve `total`.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","exclusiveMinimum":0,"default":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","exclusiveMinimum":0,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Lista de links","content":{"application/json":{"schema":{"type":"object","properties":{"links":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"token":{"type":"string","minLength":32,"maxLength":32},"linkType":{"type":"string","enum":["permanent","temporary","single_use","invitation"],"description":"Natureza do link. `invitation` e o link de visitante criado por um morador/colaborador para um convidado."},"accessType":{"type":"string","enum":["permanent","temporary","single_use"],"description":"Tipo de acesso concedido a quem se cadastrar pelo link."},"isActive":{"type":"boolean"},"maxUses":{"type":["integer","null"]},"currentUses":{"type":"integer"},"expiresAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"tenantName":{"type":"string"},"tenantSlug":{"type":"string"}},"required":["id","token","linkType","accessType","isActive","maxUses","currentUses","expiresAt","createdAt","tenantName","tenantSlug"]}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"}},"required":["page","limit"]}},"required":["links","pagination"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/access-links/{id}":{"put":{"tags":["Access"],"summary":"Atualizar link de cadastro","description":"Altera validade, limite de uso ou situacao do link.\n\n> **Comportamento atual:** restrito ao espaco do usuario autenticado. Um administrador que criou um link para outro espaco nao consegue edita-lo por aqui — assimetria em relacao a `POST /` e `GET /`, que aceitam outros espacos.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isActive":{"type":"boolean"},"maxUses":{"type":"integer","exclusiveMinimum":0},"validUntil":{"type":"string"},"expiresAt":{"type":"string"}}}}}},"responses":{"200":{"description":"Link atualizado","content":{"application/json":{"schema":{"type":"object","properties":{"link":{"$ref":"#/components/schemas/AccessLink"}},"required":["link"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Link nao encontrado no espaco do usuario","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}},"delete":{"tags":["Access"],"summary":"Revogar link de cadastro","description":"Revoga o link marcando-o como inativo — o registro e preservado para auditoria. Quem ja se cadastrou pelo link mantem o acesso.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Link revogado","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Link revogado"}},"required":["message"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Link nao encontrado no espaco do usuario","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/access-links/enroll":{"post":{"tags":["Access"],"summary":"Adicionar espaco ao cadastro existente","description":"Usado quando a pessoa ja tem cadastro e abre um link de outro espaco: em vez de criar um novo registro, o cadastro existente passa a ter acesso tambem a esse espaco, reaproveitando nome, documento e foto.\n\nDiferente do cadastro pelo formulario, **este caminho revalida o token** (situacao, validade e limite de uso) e **incrementa `currentUses`**.\n\nQuando o espaco exige aprovacao de moderador, o acesso e criado como pendente e a face so e enviada aos dispositivos apos a aprovacao.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"accessLinkToken":{"type":"string","minLength":32,"maxLength":32}},"required":["accessLinkToken"]}}}},"responses":{"201":{"description":"Acesso concedido ao espaco","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Cadastrado com sucesso no espaco"},"client":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"tenantName":{"type":"string"}},"required":["id","externalId","tenantName"]}},"required":["message","client"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"409":{"description":"Ja cadastrado neste espaco","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"410":{"description":"Link desativado, expirado ou com limite de uso atingido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/clients/{id}/invitations":{"post":{"tags":["Access"],"summary":"Criar convite de visitante","description":"Cria um convite de visitante em nome de um cliente do espaco. Gera automaticamente um link de cadastro de uso unico (`linkType: \"invitation\"`, `accessType: \"temporary\"`) e devolve o token para montar a URL.\n\nA URL entregue ao visitante tem o formato `https://<origem>/register/{slug-do-espaco}/{token}`. O token tem 32 caracteres no padrao `^[A-Za-z0-9]{32}$`.\n\nAcessivel ao proprio cliente ou a um moderador/administrador do espaco.\n\n> **Comportamento atual:** o link e criado sem `expiresAt`, `validFrom` ou `validUntil`, apesar de `accessType: \"temporary\"` — na pratica ele nao expira por tempo. E, como o limite de uso nao e aplicado no cadastro pelo formulario publico, o convite pode ser reutilizado.\n\n> A flag `visitorLinksEnabled` do espaco **nao restringe** este endpoint: convites podem ser criados mesmo com ela desligada.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Id do cliente anfitriao."},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"guestName":{"type":"string","minLength":1,"example":"Maria Souza"},"guestEmail":{"type":"string","format":"email"},"guestPhone":{"type":"string"}},"required":["guestName"]}}}},"responses":{"201":{"description":"Convite criado","content":{"application/json":{"schema":{"type":"object","properties":{"invitation":{"$ref":"#/components/schemas/Invitation"},"token":{"type":"string","minLength":32,"maxLength":32}},"required":["invitation","token"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Sem permissao sobre este cliente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Cliente nao encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}},"get":{"tags":["Access"],"summary":"Listar convites de visitante","description":"Lista os convites criados por um cliente, do mais recente para o mais antigo.\n\n> **Comportamento atual:** o `status` nunca chega a `used` nem a `expired`, e `usedAt` permanece `null` — o cadastro do convidado nao marca o convite como utilizado. O ciclo real e `pending` → `revoked`. Para saber se o convidado se cadastrou, consulte a lista de pessoas do espaco em vez do status do convite.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","exclusiveMinimum":0,"default":1},"required":false,"name":"page","in":"query"},{"schema":{"type":"integer","exclusiveMinimum":0,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Lista de convites","content":{"application/json":{"schema":{"type":"object","properties":{"invitations":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}},"pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"}},"required":["page","limit"]}},"required":["invitations","pagination"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Sem permissao sobre este cliente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}},"/api/v1/clients/{id}/invitations/{invitationId}":{"delete":{"tags":["Access"],"summary":"Revogar convite de visitante","description":"Marca o convite como revogado e desativa o link associado, de modo que ele deixa de abrir o formulario de cadastro. Nao afeta um convidado que ja tenha se cadastrado.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Id do cliente anfitriao."},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","format":"uuid"},"required":true,"name":"invitationId","in":"path"}],"responses":{"200":{"description":"Convite revogado","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Convite revogado"}},"required":["message"]}}}},"401":{"description":"Autenticacao necessaria","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"403":{"description":"Sem permissao sobre este cliente","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}},"404":{"description":"Convite nao encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"]}}}}}}}},"webhooks":{"/access-event":{"post":{"tags":["Integracao"],"summary":"[ESPEC] Evento de acesso (enviado por nos)","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\n**Nos chamamos voce.** Cada passagem em um leitor do espaco gera uma requisicao para a URL cadastrada pelo sistema de origem. E o registro de auditoria de acesso: quem passou, onde, quando e em que sentido.\n\n### Garantias de entrega\n- **Ao menos uma vez.** Uma entrega pode se repetir apos falha de rede ou resposta perdida. Deduplique por `eventId`; `sequence` tambem e estavel entre tentativas.\n- **Ordem cronologica por leitor.** Os eventos de um mesmo dispositivo sao enviados em ordem crescente de `sequence`, sem lacunas.\n- **Durabilidade.** Eventos ficam persistidos ate a confirmacao. Uma indisponibilidade do destino nao descarta nada: ao restabelecer, o acumulo e entregue na ordem original.\n- **Retentativa.** Falha ou resposta fora da faixa 2xx agenda nova tentativa com espera progressiva. Apos o limite de tentativas o evento e retido para reenvio manual, nunca descartado.\n\n### O que o receptor deve fazer\n- Validar `X-Preddita-Signature` antes de processar.\n- Responder qualquer `2xx` em ate 10 segundos. Corpo da resposta e ignorado.\n- Responder rapido e processar de forma assincrona: a resposta lenta conta como falha e gera retentativa.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Identificador unico da entrega. Use como chave de idempotencia."},"required":true,"name":"X-Preddita-Event-Id","in":"header"},{"schema":{"type":"string","description":"HMAC-SHA256 do corpo bruto, no formato `sha256=<hex>`, calculado com o segredo do webhook. Valide antes de processar.","example":"sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"},"required":true,"name":"X-Preddita-Signature","in":"header"},{"schema":{"type":"string","description":"Numero da tentativa de entrega, comecando em 1.","example":"1"},"required":true,"name":"X-Preddita-Delivery-Attempt","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessEvent"}}}},"responses":{"200":{"description":"Evento recebido. Qualquer 2xx confirma a entrega."},"default":{"description":"Qualquer resposta fora da faixa 2xx, ou ausencia de resposta, agenda nova tentativa."}}}},"/person-registered":{"post":{"tags":["Integracao"],"summary":"[ESPEC] Pessoa cadastrada no espaco (enviado por nos)","description":"**Especificacao — ainda nao implementado.** Este endpoint esta publicado para acordo de contrato; chamadas retornam 404 ate a implementacao.\n\n**Nos chamamos voce.** Disparado quando alguem conclui um cadastro no espaco — tipicamente um visitante que usou um link gerado por `POST /visitor-links`, mas tambem cadastros feitos na recepcao ou pelo formulario publico.\n\nServe para o sistema de origem espelhar o registro e, quando for o caso, atribuir a essa pessoa uma identidade propria via `POST /persons`. Enquanto ela nao tiver `identity.externalId`, os eventos de acesso dela ficam retidos e nao sao entregues no webhook `access-event`.\n\nMesmas garantias de entrega, assinatura e resposta esperada do webhook `access-event`.","parameters":[{"schema":{"type":"string","format":"uuid","description":"Identificador unico da entrega. Use como chave de idempotencia."},"required":true,"name":"X-Preddita-Event-Id","in":"header"},{"schema":{"type":"string","description":"HMAC-SHA256 do corpo bruto, no formato `sha256=<hex>`, calculado com o segredo do webhook. Valide antes de processar.","example":"sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"},"required":true,"name":"X-Preddita-Signature","in":"header"},{"schema":{"type":"string","description":"Numero da tentativa de entrega, comecando em 1.","example":"1"},"required":true,"name":"X-Preddita-Delivery-Attempt","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonRegisteredEvent"}}}},"responses":{"200":{"description":"Evento recebido. Qualquer 2xx confirma a entrega."},"default":{"description":"Resposta fora da faixa 2xx agenda nova tentativa."}}}}}}