BORADocs
llms.txt openapi.json
Referência da API

API BORA

A API BORA conecta o seu sistema ao BORA ACCESS, o aparelho que reconhece a palma da mão na catraca. Esta referência cobre as 16 rotas da prova de conceito: pessoas, consentimento, cadastro da palma, passagens, aparelhos e carteiro.

openapi.json

Para agentes de IA: use o llms.txt para ver o índice completo da documentação, ou o llms-full.txt para tudo em um arquivo.

Visão geral

O BORA ACCESS fica na catraca e reconhece a palma offline, na base local do aparelho. Ele não abre a catraca. Ele manda um veredito assinado pelo cabo USB ao validador da operadora, o DBD. O DBD decide (saldo, bloqueio, categoria) e aciona a catraca.

A API BORA é a parte na nuvem. O seu backend (o BDOS da operadora, um app ou um CRM) usa a API para cadastrar pessoas, registrar o consentimento, abrir o cadastro da palma, conciliar as passagens e levar registros entre a BORA e os aparelhos.

Parceiro e API BORA por HTTPS; BORA ACCESS e DBD pelo cabo USB; carteiro da API BORA ao aparelhoAPI BORAnuvem: cadastros,base e passagensPARCEIRO / BDOSbackend da operadora(BDOS, app ou CRM)DBDvalidador da operadoradecide e abre a catracaBORA ACCESSna catraca, reconhecea palma offlineHTTPSAPI keyrede daoperadoracabo USBBORA-LINK/1carteiro: a BORA sela registros para o aparelho; o BDOS e o DBD levam, mas não conseguem abrira volta (outbox) faz o caminho inversoMesmo fluxo, na verticalAPI BORAnuvem: cadastros,base e passagensPARCEIRO / BDOSbackend da operadora(BDOS, app ou CRM)DBDvalidador da operadoradecide e abre a catracaBORA ACCESSna catraca, reconhecea palma offlineHTTPS API keyrede da operadoracabo USB BORA-LINK/1carteiro: registros selados que o DBD não abre
O cabo USB usa o protocolo BORA-LINK/1 e não passa pela API. O carteiro leva registros selados: a BORA cifra para o aparelho, e nem o BDOS nem o DBD conseguem abrir.

Esta referência cobre só as rotas de parceiro da prova de conceito. As rotas internas do aparelho (registro, galeria, envio de vereditos e cadastro pelo totem) não fazem parte dela. O aparelho fala com a BORA com credenciais próprias, nunca com a API key do parceiro.

Fluxo básico

  1. Crie a pessoa com o external_ref da conta do cliente.
  2. Registre o consentimento da finalidade identification.
  3. Abra o cadastro e mostre o QR ou o código curto para o totem.
  4. Acompanhe o cadastro até completed.
  5. Consulte as passagens para conciliar.
  6. Faça o carteiro: busque a inbox de cada aparelho, entregue ao DBD e devolva a outbox.
  7. Apague a pessoa quando ela pedir (LGPD) e guarde o recibo.

Base URL

Todas as rotas começam com /v1. Corpo e respostas em JSON (UTF-8). Datas em RFC 3339, em UTC, com milissegundos.

ambienteURLestado
Localhttp://127.0.0.1:8710servidor de referência
Sandboxhttps://sandbox.api.bora.idem breve
Produçãohttps://api.bora.idem breve

Autenticação

Toda rota de parceiro exige o header Authorization com a sua API key:

HTTP
Authorization: Bearer bora_test_<key_id>_<secret>

A key tem o formato bora_<ambiente>_<key_id>_<secret>. Keys bora_test_ valem só no sandbox, com dados separados. Keys bora_live_ valem em produção. Hoje a key de teste é emitida pela equipe BORA, a pedido.

Cada key tem escopos, e cada rota diz o escopo que precisa: subjects:read, subjects:write, devices:read, devices:write e verdicts:read. As rotas GET /v1/health e GET /v1/keys são públicas.

Guarde a key numa variável de ambiente (BORA_API_KEY), nunca no código. Toda falha de credencial devolve o mesmo 401 unauthenticated, sem dizer o motivo.

curl "https://sandbox.api.bora.id/v1/subjects?limit=1" \
  -H "Authorization: Bearer $BORA_API_KEY"

Erros

A API usa os códigos HTTP de sempre: 2xx é sucesso, 4xx é erro na requisição e 5xx é erro do servidor.

statussignificado
200Deu certo.
201Recurso criado.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
409Conflito com o estado atual.
410Recurso ou histórico fora da retenção.
415Content-Type diferente de application/json.
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.
500Erro interno. Pode repetir com a mesma Idempotency-Key.
503Serviço indisponível.

Todo erro vem em application/problem+json (RFC 9457) com um code estável. Trate o erro pelo code. O detail é texto para humanos e pode mudar. Nenhum erro ecoa o valor que você mandou.

JSON
{
  "type": "https://bora.id/transport/documentation#erro-external_ref_looks_like_cpf",
  "title": "external_ref parece um CPF",
  "status": 422,
  "code": "external_ref_looks_like_cpf",
  "detail": "Use o ID da conta no seu sistema; o CPF vai só no campo cpf.",
  "instance": "req_01J9Z8X7W6VB",
  "retryable": false,
  "errors": [
    {
      "pointer": "/external_ref",
      "code": "external_ref_looks_like_cpf"
    }
  ]
}

Limites de taxa

quemlimite
API key, leitura100 req/s, rajada de 200
API key, escrita20 req/s, rajada de 40
POST /v1/enrollment-sessions60 por minuto por tenant

Toda resposta traz RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset. O 429 traz Retry-After: espere esses segundos antes de tentar de novo. Os limites ainda são uma proposta e vão ser ajustados com carga real.

Códigos de erro

codestatusquandorepetir?
malformed_json400corpo não é JSON válidonão
invalid_request400parâmetro de query ou header com formato erradonão
invalid_cursor400cursor adulterado ou de outra listanão
unauthenticated401qualquer falha de credencial (key, assinatura, mTLS, relógio)não
forbidden403credencial válida sem acesso ao recurso (ex.: totem em rota de galeria)não
insufficient_scope403API key sem o escopo da rotanão
livemode_mismatch403key test em recurso live ou o contrárionão
enrollment_session_invalid403token de cadastro errado, usado, vencido ou de outro aparelhonão
subject_not_found404pessoa inexistente, apagada ou de outro tenantnão
device_not_found404aparelho inexistente ou de outro tenantnão
verdict_not_found404veredito inexistente ou de outro tenantnão
enrollment_session_not_found404sessão inexistente ou de outro tenantnão
erasure_receipt_not_found404recibo inexistente ou de outro tenantnão
external_ref_conflict409external_ref já usado no tenantnão
cpf_conflict409CPF já ligado a outra pessoa ativa do tenant (sem dizer qual)não
subject_state_conflict409operação inválida no estado atual (ex.: unblock de quem não está bloqueado)não
enrollment_session_state_conflict409sessão não está abertanão
device_state_conflict409device_id ativo com outras chaves, ou chave já de outro aparelhonão
idempotency_in_progress409a primeira requisição com a mesma chave ainda rodasim
history_expired410delta pedido fora da retenção; corpo traz snapshot_seqnão, refazer por snapshot
cursor_expired410cursor fora da retençãonão
payload_too_large413corpo acima do limitenão
unsupported_media_type415Content-Type erradonão
validation_failed422JSON fora do contrato; detalhes em errors[]não
external_ref_looks_like_cpf422external_ref carrega um CPF válido (11 dígitos seguidos, com ou sem separador)não
onboard_ref_looks_like_cpf422onboard_ref carrega um CPF válido (ele viaja no cabo e na base dos aparelhos)não
cpf_invalid422CPF com dígito verificador erradonão
model_unsupported422vetor de model_id que a galeria do tenant não usanão
idempotency_key_mismatch422mesma Idempotency-Key com corpo diferentenão
idempotency_key_required428POST sem Idempotency-Keynão
rate_limited429limite de taxasim, depois do Retry-After
internal_error500erro internosim
not_implemented501recurso planejado (webhooks no v1.0)não
service_unavailable503manutenção ou sobrecargasim, depois do Retry-After

Idempotência

Todo POST exige o header Idempotency-Key. Use um UUID v4 novo para cada operação. Repita a mesma chave só quando reenviar a mesma operação.

  • Mesma chave e mesmo corpo: a API devolve a resposta gravada, com Idempotent-Replayed: true.
  • Mesma chave com corpo diferente: 422 idempotency_key_mismatch.
  • A primeira requisição ainda em andamento: 409 idempotency_in_progress.
  • Sem a chave: 428 idempotency_key_required.
  • Respostas 5xx não são gravadas. Repita com a mesma chave.

A chave vale por 24 horas, por credencial, método e rota. Nos DELETE ela é opcional.

HTTP
Idempotency-Key: 0f8b2c7e-4a1d-4b9e-8c3f-2d1e0a9b8c7d

Paginação

As listas usam cursor opaco. Mande limit (de 1 a 200, padrão 50). Para a próxima página, mande cursor com o next_cursor da página anterior e repita os mesmos filtros.

JSON
{
  "data": [{ "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "status": "active" }],
  "has_more": true,
  "next_cursor": "eyJrIjoiZGVjaWRlZF9hdCIsInYiOjE3OTA4NTAwMDA4NDB9"
}

Quando has_more vier false, a lista acabou. Cursor inválido dá 400 invalid_cursor. Cursor fora da retenção dá 410 cursor_expired. Não existe paginação por offset.

A inbox do carteiro é diferente: ela pagina por after_seq, o último seq que você entregou ao aparelho.

Pessoas

Uma pessoa nasce pelo external_ref, o ID da conta dela no seu sistema. A palma vem depois, no cadastro. O CPF é opcional, entra só no corpo e nunca volta numa resposta.

Cria uma pessoa pelo external_ref

POST/v1/subjects

Cria a pessoa sem biometria (status pending_enrollment). O cadastro da palma vem depois por POST /v1/enrollment-sessions. cpf é opcional e só serve para KYC e deduplicação dentro do tenant: já existir outra pessoa ativa com o mesmo CPF dá 409 cpf_conflict (sem revelar qual).

Autenticação: API key com o escopo subjects:write.

Parâmetros

Idempotency-Keystringheaderobrigatório

Chave única da operação. Use um UUID v4 novo por operação (veja Idempotência).

De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.
X-Request-Idstringheader

Id de correlação do cliente (ecoado na resposta). Se ausente, a BORA gera um.

Até 64 caracteres. Formato: ^[A-Za-z0-9._:-]{1,64}$.

Corpo da requisição application/json

external_refstringobrigatório

ID da conta da pessoa no sistema do parceiro (matrícula, número da conta). Único por tenant. Nunca CPF.

Formato: ^[A-Za-z0-9._:@-]{1,128}$.
cpfstring

Opcional, só para KYC e dedup. Só dígitos. Nunca devolvido.

Formato: ^[0-9]{11}$.
onboard_refstring

Token opaco opcional do operador que viaja no veredito de accept (campo onboard_ref do BORA-LINK/1) e na base de todos os aparelhos. Não use CPF nem nome; um valor que carrega CPF válido dá 422 onboard_ref_looks_like_cpf.

Formato: ^[A-Za-z0-9._:+/=-]{1,128}$.

Respostas

statussignificado
201Pessoa criada.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
409Conflito com o estado atual.
415Content-Type diferente de application/json.
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.
500Erro interno. Pode repetir com a mesma Idempotency-Key.

Lista pessoas (ou busca pelo external_ref)

GET/v1/subjects

Com external_ref, devolve 0 ou 1 item. Não existe busca por CPF (CPF nunca vai em URL).

Autenticação: API key com o escopo subjects:read.

Parâmetros

external_refstringquery

ID da conta da pessoa no sistema do parceiro (matrícula, número da conta). Único por tenant. Nunca CPF.

Formato: ^[A-Za-z0-9._:@-]{1,128}$.
statusstringquery
Valores: pending_enrollment, active, blocked.
limitintegerquery
De 1 a 200. Padrão: 50.
cursorstringquery

Cursor opaco (next_cursor da página anterior).

Até 512 caracteres. Formato: ^[A-Za-z0-9_=-]+$.

Respostas

statussignificado
200Página de pessoas.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
410Recurso ou histórico fora da retenção.
429Limite de taxa estourado.

Consulta uma pessoa

GET/v1/subjects/{subject_id}

Autenticação: API key com o escopo subjects:read.

Parâmetros

subject_idstring (uuid)pathobrigatório

Respostas

statussignificado
200A pessoa.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
429Limite de taxa estourado.

Apaga a pessoa (LGPD) e devolve recibo assinado

DELETE/v1/subjects/{subject_id}

Apagamento definitivo (LGPD art. 18, VI). Na nuvem, na hora: crypto-shred da DEK do template, remoção do HMAC e da cifra do CPF, do external_ref e do onboard_ref, tombstone no próximo delta e compactação do log de deltas (o vetor some também do histórico servido). Nos aparelhos: o tombstone chega pelo delta; aparelho offline aplica no próximo sync e, se passar do TTL de 6 h da galeria, para de reconhecer todo mundo (fail-closed). O recibo é assinado pela BORA (bora.erasure.receipt.v1) e o andamento da propagação fica em GET /v1/erasure-receipts/{receipt_id}. Depois do apagamento, GET da pessoa dá 404.

Na mesma transação saem também: o external_ref dos eventos da pessoa (e o subject_id dos eventos verdict.recorded), o vínculo dos vereditos com o subject_id (o filtro ?subject_id= deixa de achar as passagens dela), o onboard_ref dos deltas guardados e as respostas gravadas de Idempotency-Key que falavam dela.

O que fica, sem biometria:

  • o recibo assinado e o pedido (auditoria do próprio apagamento);
  • a prova assinada das passagens já registradas (signed_verdict, com o pseudônimo subject_ref e, se havia, o onboard_ref que andaram no cabo), até o fim da retenção de vereditos (13 meses), porque a assinatura cobre esses bytes e reescrever quebraria a prova. Base legal proposta: conciliação tarifária e defesa em disputa (LGPD art. 16, I e art. 7º, VI), a validar com o jurídico. Quando há prova retida, o recibo diz isso em retained: [verdict_proofs].

Autenticação: API key com o escopo subjects:write.

Parâmetros

subject_idstring (uuid)pathobrigatório
Idempotency-Keystringheader
De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Respostas

statussignificado
200Apagado na nuvem; recibo assinado e estado da propagação.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
409Conflito com o estado atual.
429Limite de taxa estourado.
500Erro interno. Pode repetir com a mesma Idempotency-Key.

Consentimento

Consentimento por finalidade (LGPD art. 11). Revogar identification suspende o reconhecimento na catraca.

Registra consentimento para uma finalidade

POST/v1/subjects/{subject_id}/consents

Registra o consentimento coletado pelo parceiro (por exemplo, no app dele) com a evidência. O consentimento dado na tela do totem durante o cadastro é registrado pelo próprio POST /v1/enrollments. Repetir a mesma finalidade com consentimento vigente devolve o registro existente (200).

Autenticação: API key com o escopo subjects:write.

Parâmetros

subject_idstring (uuid)pathobrigatório
Idempotency-Keystringheaderobrigatório

Chave única da operação. Use um UUID v4 novo por operação (veja Idempotência).

De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Corpo da requisição application/json

purposestringobrigatório

Finalidade: enrollment (guardar a palma cadastrada), identification (reconhecer na catraca) ou dataset_collection (coleta opcional para treino). payment fica para o v2.

Valores: enrollment, identification, dataset_collection.
evidenceobjectobrigatório
evidence.methodstringobrigatório
Valores: device_screen, partner_app, paper.
evidence.terms_versionstringobrigatório
Formato: ^[A-Za-z0-9._:-]{1,64}$.
evidence.accepted_atstring (date-time)obrigatório
evidence.evidence_sha256stringobrigatório

SHA-256 do artefato de evidência guardado pelo parceiro (termo assinado, log do app). A BORA não recebe o artefato.

Formato: ^[0-9a-f]{64}$.

Respostas

statussignificado
200Já havia consentimento vigente para a finalidade; devolve o registro.
201Consentimento registrado.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.

Revoga o consentimento de uma finalidade

DELETE/v1/subjects/{subject_id}/consents/{purpose}

Efeitos: identification revogado suspende o reconhecimento (mesmo efeito do block); enrollment revogado apaga o template (crypto-shred) e a pessoa volta a pending_enrollment; dataset_collection revogado apaga as amostras coletadas para treino. Para apagar a pessoa inteira, use DELETE /v1/subjects/{subject_id}.

Autenticação: API key com o escopo subjects:write.

Parâmetros

subject_idstring (uuid)pathobrigatório
purposestringpathobrigatório

Finalidade: enrollment (guardar a palma cadastrada), identification (reconhecer na catraca) ou dataset_collection (coleta opcional para treino). payment fica para o v2.

Valores: enrollment, identification, dataset_collection.
Idempotency-Keystringheader
De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Respostas

statussignificado
200Consentimento revogado.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
429Limite de taxa estourado.

Cadastro

O cadastro da palma acontece no totem ou no BORA ACCESS. Você abre a sessão, mostra o QR ou o código curto para o aparelho e acompanha o resultado.

Abre um cadastro de palma

POST/v1/enrollment-sessions

Abre uma sessão de cadastro para uma pessoa já criada. Devolve um token de uso único e um qr_payload para o totem ou o BORA ACCESS ler. Aparelho sem leitor de QR: o operador digita o short_code e o aparelho troca o código pelo token em POST /v1/enrollment-sessions/redeem. O token aparece só nesta resposta, a BORA guarda só o hash dele. O aparelho conclui com POST /v1/enrollments. O consentimento é mostrado e aceito na tela do totem antes da captura. Limite: 60 sessões por minuto por tenant.

Autenticação: API key com o escopo subjects:write.

Parâmetros

Idempotency-Keystringheaderobrigatório

Chave única da operação. Use um UUID v4 novo por operação (veja Idempotência).

De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Corpo da requisição application/json

subject_idstring (uuid)obrigatório: um dos dois
external_refstringobrigatório: um dos dois

ID da conta da pessoa no sistema do parceiro (matrícula, número da conta). Único por tenant. Nunca CPF.

Formato: ^[A-Za-z0-9._:@-]{1,128}$.
hands_requiredarray de string
Itens: L, R. De 1 a 2 itens. Padrão: ["L", "R"].
device_idstring

Restringe a sessão a um aparelho. Sem ele, qualquer totem ou BORA ACCESS do tenant que ler o token conclui.

Formato: ^[A-Za-z0-9._:-]{1,64}$.
expires_in_sinteger
De 60 a 900. Padrão: 300.
dataset_collectionboolean

Pede também o consentimento separado de coleta para treino (opt-in na tela do totem).

Padrão: false.

Respostas

statussignificado
201Sessão aberta.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
409Conflito com o estado atual.
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.

Consulta o estado de um cadastro

GET/v1/enrollment-sessions/{session_id}

Nunca devolve o token. Falha de captura aparece só como capture_rejected (sem dizer se foi qualidade ou liveness, para não virar oráculo de ataque de apresentação).

Autenticação: API key com o escopo subjects:read.

Parâmetros

session_idstring (uuid)pathobrigatório

Respostas

statussignificado
200Estado da sessão.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
429Limite de taxa estourado.

Passagens

Cada passagem é um veredito assinado pelo aparelho, com a decisão do DBD quando ela chega. Use para conciliar com o BDOS e para contestar cobranças.

Log de passagens

GET/v1/verdicts

Vereditos que os aparelhos subiram (POST /v1/verdicts/batch), já resolvidos para subject_id e external_ref (só em accept), com a decisão e a passagem do DBD quando chegaram. Ordem padrão: mais recente primeiro por decided_at. Retenção: 13 meses (proposta).

Autenticação: API key com o escopo verdicts:read.

Parâmetros

device_idstringquery
Formato: ^[A-Za-z0-9._:-]{1,64}$.
gate_idstringquery
Formato: ^[A-Za-z0-9._:-]{1,64}$.
subject_idstring (uuid)query
decisionstringquery

Decisão de identidade do BORA ACCESS.

Valores: accept, reject, abstain.
reconciliationstringquery

complete = veredito, decisão e (se a catraca tem sensor de giro, passage_sensor do aparelho) passagem conciliados; awaiting_decision = veredito sem decisão do DBD ainda; awaiting_passage = acionou numa catraca com sensor e falta o giro (sem sensor, acionar já dá complete); outcome_unknown = o aparelho ficou em OUTCOME_UNKNOWN (decisão perdida); conflict = objetos assinados que não fecham entre si (ex.: decisão apontando para outro verdict_ref, LIBERAR sem actuated, lacuna de verdict_seq).

Valores: complete, awaiting_decision, awaiting_passage, outcome_unknown, conflict.
fromstring (date-time)query

Início inclusivo (decided_at).

tostring (date-time)query

Fim exclusivo (decided_at).

orderstringquery
Valores: desc, asc. Padrão: "desc".
limitintegerquery
De 1 a 200. Padrão: 50.
cursorstringquery

Cursor opaco (next_cursor da página anterior).

Até 512 caracteres. Formato: ^[A-Za-z0-9_=-]+$.

Respostas

statussignificado
200Página de vereditos.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
410Recurso ou histórico fora da retenção.
429Limite de taxa estourado.

Veredito com prova assinada

GET/v1/verdicts/{verdict_id}

Devolve o SignedVerdict exato que o aparelho mandou ao DBD, a SignedDecision e a SignedPassage do DBD (quando chegaram) e o material para verificar fora da BORA: chaves públicas e prefixos de domínio. Serve para contestação de cobrança e conciliação com o BDOS.

Autenticação: API key com o escopo verdicts:read.

Parâmetros

verdict_idstringpathobrigatório

O verdict_ref do BORA-LINK/1.

Formato: ^[0-9a-f]{32}$.

Respostas

statussignificado
200Prova.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
429Limite de taxa estourado.

Aparelhos

Os aparelhos do seu tenant: o BORA ACCESS na catraca e os totens de cadastro. Revogar mata as chaves do aparelho.

Lista os aparelhos do tenant

GET/v1/devices

Autenticação: API key com o escopo devices:read.

Parâmetros

statusstringquery
Valores: active, revoked, wiped.
gate_idstringquery
Formato: ^[A-Za-z0-9._:-]{1,64}$.
kindstringquery
Valores: access, totem.
limitintegerquery
De 1 a 200. Padrão: 50.
cursorstringquery

Cursor opaco (next_cursor da página anterior).

Até 512 caracteres. Formato: ^[A-Za-z0-9_=-]+$.

Respostas

statussignificado
200Página de aparelhos.
400Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado).
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
429Limite de taxa estourado.

Revoga um aparelho (kill remoto)

POST/v1/devices/{device_id}/revoke

Mata as chaves do aparelho (o kill é da chave, não do nome). Na próxima consulta a GET /v1/devices/self/status o aparelho recebe wipe assinado e apaga galeria, DEKs e seeds; depois disso toda rota de aparelho responde 401 para essas chaves. Com o DBD offline, a revogação chega pelo trust.bundle (v1.1). Irreversível para estas chaves: para pôr o aparelho de volta, o admin BORA emite um token de provisionamento novo para o MESMO device_id e o aparelho se registra com chaves novas (as antigas ficam arquivadas, só para verificar vereditos antigos).

Autenticação: API key com o escopo devices:write.

Parâmetros

device_idstringpathobrigatório

Id do aparelho. self, register e attest são reservados.

Formato: ^[A-Za-z0-9._:-]{1,64}$.
Idempotency-Keystringheaderobrigatório

Chave única da operação. Use um UUID v4 novo por operação (veja Idempotência).

De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Corpo da requisição application/json

reasonstringobrigatório
Valores: lost, stolen, decommissioned, compromised, other.

Respostas

statussignificado
200Aparelho revogado.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.

Carteiro

O carteiro leva registros entre a BORA e o aparelho pelo caminho que já existe: API BORA, BDOS, DBD e cabo USB. A BORA sela cada registro para o aparelho, e o aparelho sela para a BORA. Quem leva não consegue abrir, forjar nem escolher o que leva.

Registros selados para levar ao aparelho

GET/v1/devices/{device_id}/inbox

Carteiro, ida. Registros que a BORA selou para este aparelho: atualização da base, revogações, config assinada, trust.bundle e atualizações de app ou modelo. O BDOS busca aqui e entrega ao DBD, que oferece ao aparelho pelo cabo (file.offer kind post.in). Nem o BDOS nem o DBD conseguem abrir, forjar ou escolher o que levam: o conteúdo vai cifrado para a chave de selo do aparelho e assinado pela BORA. Peça sempre a partir do último seq entregue (after_seq); o aparelho aplica em ordem estrita.

Autenticação: API key com o escopo devices:read.

Parâmetros

device_idstringpathobrigatório

Id do aparelho. self, register e attest são reservados.

Formato: ^[A-Za-z0-9._:-]{1,64}$.
after_seqintegerquery

Devolve só registros com seq maior que este. Padrão 0 (desde o início).

Mínimo 0. Padrão: 0.
limitintegerquery
De 1 a 200. Padrão: 50.

Respostas

statussignificado
200Registros pendentes para o aparelho, em ordem de seq.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
422JSON válido, mas fora do contrato.
429Limite de taxa estourado.

Entrega registros selados que saíram do aparelho

POST/v1/devices/{device_id}/outbox

Carteiro, volta. O aparelho sela para a BORA os vereditos, recibos e eventos (post.out); o DBD puxa pelo cabo e o BDOS entrega aqui do jeito que recebeu, sem abrir. A BORA confere a assinatura do aparelho, o destino e o sha256 de cada registro e guarda. acked_upto_seq é o maior seq contíguo já recebido: o recibo assinado (bora.post.receipt) volta pela caixa de entrada e é ele que libera o aparelho para apagar a cópia local. Reenviar o mesmo registro é seguro (sai em duplicates).

Autenticação: API key com o escopo devices:write.

Parâmetros

device_idstringpathobrigatório

Id do aparelho. self, register e attest são reservados.

Formato: ^[A-Za-z0-9._:-]{1,64}$.
Idempotency-Keystringheaderobrigatório

Chave única da operação. Use um UUID v4 novo por operação (veja Idempotência).

De 1 a 255 caracteres. Formato: ^[\x21-\x7E]+$.

Corpo da requisição application/json

recordsarray de SealedRecordobrigatório
De 1 a 32 itens.
records[].postobjectobrigatório
records[].ct_b64stringobrigatório

Base64 padrão com padding.

Formato: ^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$.
records[].algstringobrigatório
Valor fixo: "ed25519".
records[].signer_kidstringobrigatório

Hash curto: 16 bytes em 32 hex minúsculos (txn_id, verdict_ref, prev_ref, subject_ref, kid, *16).

Formato: ^[0-9a-f]{32}$.
records[].sig_b64stringobrigatório

Assinatura Ed25519 de 64 bytes em base64 padrão (88 caracteres). Verificação estrita: S < L e ponto canônico.

Formato: ^[A-Za-z0-9+/]{86}==$.
records[].signer_cert_b64string

Certificado de 104 B de uma chave emissora BORA pela raiz, no formato do prelúdio do trust.bundle (7.6): signer_pub(32) | signer_not_after u64 BE | root_sig(64). O prefixo do root_sig separa a finalidade: "bora.bundle.signer.v1" para a chave de bundle (7.6) e "bora.post.signer.v1" para a chave do carteiro, que assina manifesto de file.* e registro selado box in (4.8.10); root_sig sobre prefixo || 0x00 || signer_pub || not_after. Em base64 (140 caracteres).

Formato: ^[A-Za-z0-9+/]{139}=$.

Respostas

statussignificado
200Resultado por registro.
401Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo).
403Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido).
404Recurso inexistente ou de outro tenant (mesma resposta).
422JSON válido, mas fora do contrato.
428POST sem Idempotency-Key.
429Limite de taxa estourado.

Sistema

Rotas públicas, sem API key: o estado do serviço e as chaves públicas que a BORA usa para assinar recibos e registros.

Estado do serviço

GET/v1/health

Rota pública, sem API key.

Respostas

statussignificado
200Serviço no ar.
503Serviço indisponível.

Chaves públicas de assinatura da BORA

GET/v1/keys

Chaves Ed25519 que a BORA usa para assinar recibos de apagamento, cabeças e deltas de galeria, status de aparelho e webhooks. O cliente deve fixar (pin) estas chaves por kid e aceitar rotação só quando a nova chave aparecer aqui antes do not_before.

Rota pública, sem API key.

Respostas

statussignificado
200Lista de chaves.