# BORA Docs: documentação completa > Fonte: https://docs.bora.id. Gerado do contrato OpenAPI 1.0.0-draft.2, só com as operações da prova de conceito. As rotas internas do aparelho não fazem parte desta referência. # Referência da API 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. Para agentes de IA: use o [llms.txt](https://docs.bora.id/llms.txt) para ver o índice completo da documentação, ou o [llms-full.txt](https://docs.bora.id/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. Como as peças se ligam: - Parceiro (BDOS, app ou CRM) e API BORA: HTTPS com API key. - BORA ACCESS e DBD: cabo USB com o protocolo BORA-LINK/1. O cabo não passa pela API. - Carteiro: API BORA, depois BDOS, depois DBD, depois BORA ACCESS. Os registros vão selados: a BORA cifra para o aparelho, e nem o BDOS nem o DBD conseguem abrir. A volta (outbox) faz o caminho inverso. - [Pessoas](https://docs.bora.id/api/#pessoas): Crie, busque e apague pessoas pelo ID da conta no seu sistema. - [Consentimento](https://docs.bora.id/api/#consentimento): Registre e revogue o consentimento por finalidade (LGPD). - [Cadastro](https://docs.bora.id/api/#cadastro): Abra o cadastro da palma no totem e acompanhe o resultado. - [Passagens](https://docs.bora.id/api/#passagens): Consulte as passagens com prova assinada para conciliar. - [Aparelhos](https://docs.bora.id/api/#aparelhos): Liste os aparelhos e revogue um aparelho perdido ou roubado. - [Carteiro](https://docs.bora.id/api/#carteiro): Leve registros selados entre a BORA e o aparelho, sem abrir. - [Sistema](https://docs.bora.id/api/#sistema): Estado do serviço e chaves públicas da BORA. 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](https://docs.bora.id/api/#createSubject) com o `external_ref` da conta do cliente. 2. [Registre o consentimento](https://docs.bora.id/api/#grantConsent) da finalidade `identification`. 3. [Abra o cadastro](https://docs.bora.id/api/#createEnrollmentSession) e mostre o QR ou o código curto para o totem. 4. [Acompanhe o cadastro](https://docs.bora.id/api/#getEnrollmentSession) até `completed`. 5. [Consulte as passagens](https://docs.bora.id/api/#listVerdicts) para conciliar. 6. Faça o carteiro: [busque a inbox](https://docs.bora.id/api/#listDeviceInbox) de cada aparelho, entregue ao DBD e [devolva a outbox](https://docs.bora.id/api/#submitDeviceOutbox). 7. [Apague a pessoa](https://docs.bora.id/api/#eraseSubject) 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. | ambiente | URL | estado | |---|---|---| | Local | `http://127.0.0.1:8710` | servidor de referência | | Sandbox | `https://sandbox.api.bora.id` | em breve | | Produção | `https://api.bora.id` | em breve | ## Autenticação Toda rota de parceiro exige o header `Authorization` com a sua API key: ```http Authorization: Bearer bora_test__ ``` A key tem o formato `bora___`. 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. ```bash 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. | status | significado | |---|---| | `200` | Deu certo. | | `201` | Recurso criado. | | `400` | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | `401` | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | `403` | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | `404` | Recurso inexistente ou de outro tenant (mesma resposta). | | `409` | Conflito com o estado atual. | | `410` | Recurso ou histórico fora da retenção. | | `415` | Content-Type diferente de application/json. | | `422` | JSON válido, mas fora do contrato. | | `428` | POST sem Idempotency-Key. | | `429` | Limite de taxa estourado. | | `500` | Erro interno. Pode repetir com a mesma Idempotency-Key. | | `503` | Serviç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 | quem | limite | |---|---| | API key, leitura | 100 req/s, rajada de 200 | | API key, escrita | 20 req/s, rajada de 40 | | `POST /v1/enrollment-sessions` | 60 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 | code | status | quando | repetir? | |---|---|---|---| | `malformed_json` | 400 | corpo não é JSON válido | não | | `invalid_request` | 400 | parâmetro de query ou header com formato errado | não | | `invalid_cursor` | 400 | cursor adulterado ou de outra lista | não | | `unauthenticated` | 401 | qualquer falha de credencial (key, assinatura, mTLS, relógio) | não | | `forbidden` | 403 | credencial válida sem acesso ao recurso (ex.: totem em rota de galeria) | não | | `insufficient_scope` | 403 | API key sem o escopo da rota | não | | `livemode_mismatch` | 403 | key `test` em recurso `live` ou o contrário | não | | `enrollment_session_invalid` | 403 | token de cadastro errado, usado, vencido ou de outro aparelho | não | | `subject_not_found` | 404 | pessoa inexistente, apagada ou de outro tenant | não | | `device_not_found` | 404 | aparelho inexistente ou de outro tenant | não | | `verdict_not_found` | 404 | veredito inexistente ou de outro tenant | não | | `enrollment_session_not_found` | 404 | sessão inexistente ou de outro tenant | não | | `erasure_receipt_not_found` | 404 | recibo inexistente ou de outro tenant | não | | `external_ref_conflict` | 409 | external_ref já usado no tenant | não | | `cpf_conflict` | 409 | CPF já ligado a outra pessoa ativa do tenant (sem dizer qual) | não | | `subject_state_conflict` | 409 | operação inválida no estado atual (ex.: unblock de quem não está bloqueado) | não | | `enrollment_session_state_conflict` | 409 | sessão não está aberta | não | | `device_state_conflict` | 409 | device_id ativo com outras chaves, ou chave já de outro aparelho | não | | `idempotency_in_progress` | 409 | a primeira requisição com a mesma chave ainda roda | sim | | `history_expired` | 410 | delta pedido fora da retenção; corpo traz `snapshot_seq` | não, refazer por snapshot | | `cursor_expired` | 410 | cursor fora da retenção | não | | `payload_too_large` | 413 | corpo acima do limite | não | | `unsupported_media_type` | 415 | Content-Type errado | não | | `validation_failed` | 422 | JSON fora do contrato; detalhes em `errors[]` | não | | `external_ref_looks_like_cpf` | 422 | external_ref carrega um CPF válido (11 dígitos seguidos, com ou sem separador) | não | | `onboard_ref_looks_like_cpf` | 422 | onboard_ref carrega um CPF válido (ele viaja no cabo e na base dos aparelhos) | não | | `cpf_invalid` | 422 | CPF com dígito verificador errado | não | | `consent_required` | 422 | operação exige consentimento vigente da finalidade | não | | `model_unsupported` | 422 | vetor de `model_id` que a galeria do tenant não usa | não | | `idempotency_key_mismatch` | 422 | mesma Idempotency-Key com corpo diferente | não | | `idempotency_key_required` | 428 | POST sem Idempotency-Key | não | | `rate_limited` | 429 | limite de taxa | sim, depois do Retry-After | | `internal_error` | 500 | erro interno | sim | | `not_implemented` | 501 | recurso planejado (webhooks no v1.0) | não | | `service_unavailable` | 503 | manutenção ou sobrecarga | sim, 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `Idempotency-Key` | header | string | sim | 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-Id` | header | string | não | 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 (`application/json`): | campo | tipo | obrigatório | descrição | |---|---|---|---| | `external_ref` | string | sim | 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}$`. | | `cpf` | string | não | Opcional, só para KYC e dedup. Só dígitos. Nunca devolvido. Formato: `^[0-9]{11}$`. | | `onboard_ref` | string | não | 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: | status | significado | |---|---| | 201 | Pessoa criada. | | 400 | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 409 | Conflito com o estado atual. | | 415 | Content-Type diferente de application/json. | | 422 | JSON válido, mas fora do contrato. | | 428 | POST sem Idempotency-Key. | | 429 | Limite de taxa estourado. | | 500 | Erro interno. Pode repetir com a mesma Idempotency-Key. | Exemplo (cURL): ```bash curl -X POST "https://sandbox.api.bora.id/v1/subjects" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "external_ref": "MBH-000482913" }' ``` Resposta 201: ```json { "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "external_ref": "MBH-000482913", "subject_ref": "2a16ce69bc964deda2b740bfe43edf4b", "onboard_ref": "obr-7Q2m9X", "status": "pending_enrollment", "block": null, "enrollment": null, "consents": [], "kyc": { "cpf_on_file": true }, "gallery_seq": null, "created_at": "2026-10-01T09:55:00.000Z", "updated_at": "2026-10-01T09:55:00.000Z", "livemode": true } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `external_ref` | query | string | não | 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}$`. | | `status` | query | string | não | Valores: `pending_enrollment`, `active`, `blocked`. | | `limit` | query | integer | não | De 1 a 200. Padrão: `50`. | | `cursor` | query | string | não | Cursor opaco (next_cursor da página anterior). Até 512 caracteres. Formato: `^[A-Za-z0-9_=-]+$`. | Respostas: | status | significado | |---|---| | 200 | Página de pessoas. | | 400 | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 410 | Recurso ou histórico fora da retenção. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/subjects?external_ref=MBH-000482913" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "data": [ { "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "external_ref": "MBH-000482913", "subject_ref": "2a16ce69bc964deda2b740bfe43edf4b", "onboard_ref": "obr-7Q2m9X", "status": "active", "block": null, "enrollment": { "hands": [ "L", "R" ], "model_id": "deptrum-onnx-2026.09", "enrolled_at": "2026-10-01T10:02:41.120Z" }, "consents": [ { "purpose": "enrollment", "status": "granted" }, { "purpose": "identification", "status": "granted" } ], "kyc": { "cpf_on_file": true }, "gallery_seq": 15873, "created_at": "2026-10-01T09:55:00.000Z", "updated_at": "2026-10-01T10:02:41.120Z", "livemode": true } ], "has_more": false, "next_cursor": null } ``` ### Consulta uma pessoa `GET /v1/subjects/{subject_id}` Autenticação: API key com o escopo `subjects:read`. Parâmetros: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `subject_id` | path | string (uuid) | sim | | Respostas: | status | significado | |---|---| | 200 | A pessoa. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/subjects/3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "external_ref": "MBH-000482913", "subject_ref": "2a16ce69bc964deda2b740bfe43edf4b", "onboard_ref": "obr-7Q2m9X", "status": "active", "block": null, "enrollment": { "hands": [ "L", "R" ], "model_id": "deptrum-onnx-2026.09", "enrolled_at": "2026-10-01T10:02:41.120Z" }, "consents": [ { "purpose": "enrollment", "status": "granted" }, { "purpose": "identification", "status": "granted" } ], "kyc": { "cpf_on_file": true }, "gallery_seq": 15873, "created_at": "2026-10-01T09:55:00.000Z", "updated_at": "2026-10-01T10:02:41.120Z", "livemode": true } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `subject_id` | path | string (uuid) | sim | | | `Idempotency-Key` | header | string | não | De 1 a 255 caracteres. Formato: `^[\x21-\x7E]+$`. | Respostas: | status | significado | |---|---| | 200 | Apagado na nuvem; recibo assinado e estado da propagação. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 409 | Conflito com o estado atual. | | 429 | Limite de taxa estourado. | | 500 | Erro interno. Pode repetir com a mesma Idempotency-Key. | Exemplo (cURL): ```bash curl -X DELETE "https://sandbox.api.bora.id/v1/subjects/3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Resposta 200: ```json { "receipt_id": "c0a8f2d4-1b3e-4f5a-8c7d-9e0f1a2b3c4d", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "signed_receipt": { "receipt": { "v": 1, "receipt_id": "c0a8f2d4-1b3e-4f5a-8c7d-9e0f1a2b3c4d", "tenant_id": "tnt-metro-bh", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "requested_at_ms": 1790853600000, "erased_at_ms": 1790853600120, "erased": [ "templates", "cpf_hmac", "cpf_ciphertext", "external_ref", "onboard_ref" ], "gallery": { "stream": "main", "epoch": 4, "seq": 15901 } }, "alg": "ed25519", "signer_kid": "abb9ff726ba8ffb31638d59e7fcfb820", "sig_b64": "qr+/q4EU9D99x0ne4JPlwxemRgrnSSAh1WQcQidnsIfXjl6VlJJIhR5KvbYJGmc3tAqy6Y14GmgmSM7bVae1CQ==" }, "propagation": { "status": "pending", "devices_total": 12, "devices_acknowledged": 9, "fail_closed_at": "2026-10-01T17:20:00.120Z" } } ``` ## 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `subject_id` | path | string (uuid) | sim | | | `Idempotency-Key` | header | string | sim | 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 (`application/json`): | campo | tipo | obrigatório | descrição | |---|---|---|---| | `purpose` | string | sim | 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`. | | `evidence` | object | sim | | | `evidence.method` | string | sim | Valores: `device_screen`, `partner_app`, `paper`. | | `evidence.terms_version` | string | sim | Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `evidence.accepted_at` | string (date-time) | sim | | | `evidence.evidence_sha256` | string | sim | 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: | status | significado | |---|---| | 200 | Já havia consentimento vigente para a finalidade; devolve o registro. | | 201 | Consentimento registrado. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 422 | JSON válido, mas fora do contrato. | | 428 | POST sem Idempotency-Key. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl -X POST "https://sandbox.api.bora.id/v1/subjects/3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64/consents" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "purpose": "identification", "evidence": { "method": "partner_app", "terms_version": "termos-bora-transport-2026-10", "accepted_at": "2026-10-01T09:58:12.000Z", "evidence_sha256": "5d41402abc4b2a76b9719d911017c592ae2f1c8e9b1f7a5b3c2d1e0f9a8b7c6d" } }' ``` Resposta 201: ```json { "consent_id": "6e1d2c3b-4a59-4f68-8a7b-9c0d1e2f3a4b", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "purpose": "identification", "status": "granted", "terms_version": "termos-bora-transport-2026-10", "method": "partner_app", "granted_at": "2026-10-01T09:58:12.000Z", "revoked_at": null } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `subject_id` | path | string (uuid) | sim | | | `purpose` | path | string | sim | 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-Key` | header | string | não | De 1 a 255 caracteres. Formato: `^[\x21-\x7E]+$`. | Respostas: | status | significado | |---|---| | 200 | Consentimento revogado. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl -X DELETE "https://sandbox.api.bora.id/v1/subjects/3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64/consents/identification" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Resposta 200: ```json { "consent_id": "6e1d2c3b-4a59-4f68-8a7b-9c0d1e2f3a4b", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "purpose": "identification", "status": "revoked", "terms_version": "termos-bora-transport-2026-10", "method": "partner_app", "granted_at": "2026-10-01T09:58:12.000Z", "revoked_at": "2026-10-02T08:00:00.000Z" } ``` ## 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `Idempotency-Key` | header | string | sim | 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 (`application/json`): | campo | tipo | obrigatório | descrição | |---|---|---|---| | `subject_id` | string (uuid) | um dos dois | | | `external_ref` | string | 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_required` | array de string | não | Itens: `L`, `R`. De 1 a 2 itens. Padrão: `["L", "R"]`. | | `device_id` | string | não | 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_s` | integer | não | De 60 a 900. Padrão: `300`. | | `dataset_collection` | boolean | não | Pede também o consentimento separado de coleta para treino (opt-in na tela do totem). Padrão: `false`. | Respostas: | status | significado | |---|---| | 201 | Sessão aberta. | | 400 | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 409 | Conflito com o estado atual. | | 422 | JSON válido, mas fora do contrato. | | 428 | POST sem Idempotency-Key. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl -X POST "https://sandbox.api.bora.id/v1/enrollment-sessions" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "hands_required": [ "L", "R" ], "expires_in_s": 300 }' ``` Resposta 201: ```json { "session_id": "51d3e9a2-7c4b-4f1e-9a8d-2b3c4d5e6f70", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "status": "open", "failure_code": null, "hands_required": [ "L", "R" ], "device_id": null, "completed_by_device_id": null, "enrollment_id": null, "token": "kJ3n9Qx2Lm8Vb7Rt5Yp1Zc4Wd6Fh0Gs2Aa9Ee3Ii7Oo", "qr_payload": "BORA1:ENR:51d3e9a2-7c4b-4f1e-9a8d-2b3c4d5e6f70:kJ3n9Qx2Lm8Vb7Rt5Yp1Zc4Wd6Fh0Gs2Aa9Ee3Ii7Oo", "short_code": "K7M2Q9XA", "expires_at": "2026-10-01T10:05:00.000Z", "created_at": "2026-10-01T10:00:00.000Z", "completed_at": null } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `session_id` | path | string (uuid) | sim | | Respostas: | status | significado | |---|---| | 200 | Estado da sessão. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/enrollment-sessions/51d3e9a2-7c4b-4f1e-9a8d-2b3c4d5e6f70" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "session_id": "51d3e9a2-7c4b-4f1e-9a8d-2b3c4d5e6f70", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "status": "completed", "failure_code": null, "hands_required": [ "L", "R" ], "device_id": null, "completed_by_device_id": "totem-bh-central-01", "enrollment_id": "9b2f4c1e-7a3d-4e8b-a1c2-3d4e5f6a7b8c", "expires_at": "2026-10-01T10:05:00.000Z", "created_at": "2026-10-01T10:00:00.000Z", "completed_at": "2026-10-01T10:02:41.120Z" } ``` ## 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `device_id` | query | string | não | Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `gate_id` | query | string | não | Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `subject_id` | query | string (uuid) | não | | | `decision` | query | string | não | Decisão de identidade do BORA ACCESS. Valores: `accept`, `reject`, `abstain`. | | `reconciliation` | query | string | não | 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`. | | `from` | query | string (date-time) | não | Início inclusivo (decided_at). | | `to` | query | string (date-time) | não | Fim exclusivo (decided_at). | | `order` | query | string | não | Valores: `desc`, `asc`. Padrão: `"desc"`. | | `limit` | query | integer | não | De 1 a 200. Padrão: `50`. | | `cursor` | query | string | não | Cursor opaco (next_cursor da página anterior). Até 512 caracteres. Formato: `^[A-Za-z0-9_=-]+$`. | Respostas: | status | significado | |---|---| | 200 | Página de vereditos. | | 400 | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 410 | Recurso ou histórico fora da retenção. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/verdicts?gate_id=bh-eldorado-cat-03&limit=50" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "data": [ { "verdict_id": "bae244c9d453584320e97cf6b06fb432", "txn_id": "7f3c9a1e5b2d4c6f8a0b1c2d3e4f5a6b", "device_id": "access-bh-eldorado-03", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "verdict_seq": 1042, "decision": "accept", "result": "MATCH", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "external_ref": "MBH-000482913", "hand": "R", "pad_res": "pass", "quality_q": 9120, "model_id": "deptrum-onnx-2026.09", "decided_at": "2026-10-01T10:20:00.840Z", "received_at": "2026-10-01T10:20:03.112Z", "dbd": { "reason": "ok", "outcome": "LIBERAR", "actuated": true, "decision_seq": 88123, "decided_at": "2026-10-01T10:20:00.861Z" }, "passage": { "passed": true, "cause": "giro", "at": "2026-10-01T10:20:02.400Z" }, "reconciliation": "complete" } ], "has_more": true, "next_cursor": "eyJrIjoiZGVjaWRlZF9hdCIsInYiOjE3OTA4NTAwMDA4NDB9" } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `verdict_id` | path | string | sim | O verdict_ref do BORA-LINK/1. Formato: `^[0-9a-f]{32}$`. | Respostas: | status | significado | |---|---| | 200 | Prova. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/verdicts/bae244c9d453584320e97cf6b06fb432" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "record": { "verdict_id": "bae244c9d453584320e97cf6b06fb432", "txn_id": "7f3c9a1e5b2d4c6f8a0b1c2d3e4f5a6b", "device_id": "access-bh-eldorado-03", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "verdict_seq": 1042, "decision": "accept", "result": "MATCH", "subject_id": "3f6c2a9e-8b1d-4e7a-9c3f-5d2e1a0b7c64", "external_ref": "MBH-000482913", "hand": "R", "pad_res": "pass", "quality_q": 9120, "model_id": "deptrum-onnx-2026.09", "decided_at": "2026-10-01T10:20:00.840Z", "received_at": "2026-10-01T10:20:03.112Z", "dbd": { "reason": "ok", "outcome": "LIBERAR", "actuated": true, "decision_seq": 88123, "decided_at": "2026-10-01T10:20:00.861Z" }, "passage": { "passed": true, "cause": "giro", "at": "2026-10-01T10:20:02.400Z" }, "reconciliation": "complete" }, "signed_verdict": { "verdict": { "v": 1, "txn_id": "7f3c9a1e5b2d4c6f8a0b1c2d3e4f5a6b", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "nonce_b64": "nj8VYyTULw6ktvT86B1W+w==", "issued_at_ms": 1790850000000, "ttl_ms": 5000, "decided_at_ms": 1790850000840, "device_id": "access-bh-eldorado-03", "decision": "accept", "result": "MATCH", "verdict_seq": 1042, "prev_ref": "be1ccf680c02a5ca3847db9540093045", "subject_ref": "2a16ce69bc964deda2b740bfe43edf4b", "onboard_ref": "obr-7Q2m9X", "hand": "R", "score_q": 8712, "margin_q": 2104, "quality_q": 9120, "pad": { "res": "pass", "mech": "ir-depth-v1", "score_q": 9650 }, "attempts": 1, "model_id": "deptrum-onnx-2026.09", "thr_id": "thr-2026-09-a", "gallery": { "stream": "main", "epoch": 3, "seq": 15872, "head16": "9f2e6d33a3717ee826353a404ba4618d" } }, "alg": "ed25519", "signer_kid": "300290aeb182952c1337b227be5fe728", "sig_b64": "U7SHmogLJ+JjGP4QcyMxSqscfiDUMB08y0potQvGdp/0HjnGdtl8JvmZA9nafAdhp0PtTv/6nkTNW0y4iF/dBg==" }, "signed_decision": { "decision_obj": { "v": 1, "txn_id": "7f3c9a1e5b2d4c6f8a0b1c2d3e4f5a6b", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "nonce_b64": "nj8VYyTULw6ktvT86B1W+w==", "verdict_ref": "bae244c9d453584320e97cf6b06fb432", "reason": "ok", "outcome": "LIBERAR", "actuated": true, "decision_seq": 88123, "decided_at_ms": 1790850000861 }, "alg": "ed25519", "signer_kid": "4678808aef484c079515f89dbe4e1ef3", "sig_b64": "JgbRq9m69X6O36Ys/Xn8amTrXYl6SJHK2PO8ERYeMVVOrwW1qWNwJMHIGT9832/4kax6+nxogBFSUQMMAJgXAg==" }, "signed_passage": { "passage": { "v": 1, "txn_id": "7f3c9a1e5b2d4c6f8a0b1c2d3e4f5a6b", "dbd_id": "dbd-bh-eldorado-03", "gate_id": "bh-eldorado-cat-03", "verdict_ref": "bae244c9d453584320e97cf6b06fb432", "passed": true, "cause": "giro", "at_ms": 1790850002400, "dbd_event_seq": 401220 }, "alg": "ed25519", "signer_kid": "4678808aef484c079515f89dbe4e1ef3", "sig_b64": "0mOVxqp5SWykiWKAvKOoUKKH7juWTSwQVoY40oRoZY4UqVT13S3iecxPNcFHjPV5m2D/ceCUP+j6+tdeZj20Cg==" }, "verification": { "verdict_ref": "bae244c9d453584320e97cf6b06fb432", "device_verdict_key": { "kid": "300290aeb182952c1337b227be5fe728", "alg": "ed25519", "pub_b64": "0mZcYF+l54oAZ/8dAsbliiXUbumVlpIjXGc8EDb6qO0=" }, "dbd_sign_key": { "kid": "4678808aef484c079515f89dbe4e1ef3", "alg": "ed25519", "pub_b64": "/EorC2SHripynSbtWpO3y0kvagTLyjyo+9rKeg/+YGs=" }, "prefixes": { "verdict": "bora.gate.verdict.v1", "decision": "bora.gate.decision.v1", "passage": "bora.gate.passage.v1" } } } ``` ## 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `status` | query | string | não | Valores: `active`, `revoked`, `wiped`. | | `gate_id` | query | string | não | Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `kind` | query | string | não | Valores: `access`, `totem`. | | `limit` | query | integer | não | De 1 a 200. Padrão: `50`. | | `cursor` | query | string | não | Cursor opaco (next_cursor da página anterior). Até 512 caracteres. Formato: `^[A-Za-z0-9_=-]+$`. | Respostas: | status | significado | |---|---| | 200 | Página de aparelhos. | | 400 | Requisição malformada (JSON inválido, cursor inválido, parâmetro com formato errado). | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/devices?kind=access" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "data": [ { "device_id": "access-bh-eldorado-03", "kind": "access", "status": "active", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "app": { "pkg": "studio.unflat.boradeptrum.dbd", "version": "2.1.4" }, "model_id": "deptrum-onnx-2026.09", "gallery": { "stream": "main", "epoch": 3, "seq": 15873, "synced_at": "2026-10-01T10:19:58.000Z" }, "keys": [ { "purpose": "verdict", "alg": "ed25519", "kid": "300290aeb182952c1337b227be5fe728" }, { "purpose": "http", "alg": "ed25519", "kid": "d1ab5521bfd29c9326bcc0912595d2d2" } ], "trust": { "attest_level": "none", "verified": false }, "health": [ "KEY_UNATTESTED" ], "last_seen_at": "2026-10-01T10:20:01.000Z", "registered_at": "2026-09-30T18:00:00.000Z", "revoked_at": null, "livemode": true } ], "has_more": false, "next_cursor": null } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `device_id` | path | string | sim | Id do aparelho. `self`, `register` e `attest` são reservados. Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `Idempotency-Key` | header | string | sim | 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 (`application/json`): | campo | tipo | obrigatório | descrição | |---|---|---|---| | `reason` | string | sim | Valores: `lost`, `stolen`, `decommissioned`, `compromised`, `other`. | Respostas: | status | significado | |---|---| | 200 | Aparelho revogado. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 422 | JSON válido, mas fora do contrato. | | 428 | POST sem Idempotency-Key. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl -X POST "https://sandbox.api.bora.id/v1/devices/access-bh-eldorado-03/revoke" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reason": "stolen" }' ``` Resposta 200: ```json { "device_id": "access-bh-eldorado-03", "kind": "access", "status": "revoked", "gate_id": "bh-eldorado-cat-03", "dbd_id": "dbd-bh-eldorado-03", "app": { "pkg": "studio.unflat.boradeptrum.dbd", "version": "2.1.4" }, "model_id": "deptrum-onnx-2026.09", "gallery": { "stream": "main", "epoch": 3, "seq": 15873, "synced_at": "2026-10-01T10:19:58.000Z" }, "keys": [ { "purpose": "verdict", "alg": "ed25519", "kid": "300290aeb182952c1337b227be5fe728" }, { "purpose": "http", "alg": "ed25519", "kid": "d1ab5521bfd29c9326bcc0912595d2d2" } ], "trust": { "attest_level": "none", "verified": false }, "health": [ "KEY_UNATTESTED" ], "last_seen_at": "2026-10-01T10:20:01.000Z", "registered_at": "2026-09-30T18:00:00.000Z", "revoked_at": "2026-10-01T11:00:00.000Z", "livemode": true } ``` ## 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `device_id` | path | string | sim | Id do aparelho. `self`, `register` e `attest` são reservados. Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `after_seq` | query | integer | não | Devolve só registros com `seq` maior que este. Padrão 0 (desde o início). Mínimo 0. Padrão: `0`. | | `limit` | query | integer | não | De 1 a 200. Padrão: `50`. | Respostas: | status | significado | |---|---| | 200 | Registros pendentes para o aparelho, em ordem de `seq`. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 422 | JSON válido, mas fora do contrato. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/devices/access-bh-eldorado-03/inbox?after_seq=87" \ -H "Authorization: Bearer $BORA_API_KEY" ``` Resposta 200: ```json { "device_id": "access-bh-eldorado-03", "data": [ { "post": { "v": 1, "rid": "c41f2a9be07d4d3f9a6e5b2c1d0e8f7a", "box": "in", "src": "bora", "dst": "access-bh-eldorado-03", "seq": 88, "created_ms": 1790849000000, "suite": "hpke-x25519-sha256-chacha20poly1305", "ct_len": 144, "ct_sha256": "7d8bbc540bbbfc01881f4becb42f2ff14c72d372a889bf04e3e0a07019e365fc" }, "ct_b64": "TAc0sssnInErGpzmvTRKQb1f5wNinoQEfZvKHfG8DrjbuN1sZ3oAIPAOl1u/UG3lbMMpi9l756LtnMyruTrLn9PYmK5g1C7F3rI3xTWKOR5sbbVg+L2Z6V7MNLDnKKqxiPclJu5KxrYKxErFd1yleZcuCnC4aWnex4JcyE+D3UupqQN8hyyjLD4mJWsuIWBo", "alg": "ed25519", "signer_kid": "d9bf8bfe11bc109cb5687c77a5bf6581", "sig_b64": "3rt1LWthsu8S4Qr4eGPNGTmfqMda3mm1/YzeaSjKdbsDLNsUOhxj0jkDcCpKJIZYiZUOTKhl6drZF61PH1ZzDg==", "signer_cert_b64": "kvCBJCkO7mgC+XHfZDDByN6Nkz2+zpLxXeQBnT3vemEAAAGixmd6QBQuCyvnDKjwbNqTrEAThAEsCj79FV04k648hrD4XcsbBS9kg8rvOZ6GUMWK38vKc0BIqjHSvQ8MeJ9UXLkoeAw=" } ], "has_more": false, "next_after_seq": 88 } ``` ### 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: | nome | em | tipo | obrigatório | descrição | |---|---|---|---|---| | `device_id` | path | string | sim | Id do aparelho. `self`, `register` e `attest` são reservados. Formato: `^[A-Za-z0-9._:-]{1,64}$`. | | `Idempotency-Key` | header | string | sim | 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 (`application/json`): | campo | tipo | obrigatório | descrição | |---|---|---|---| | `records` | array de SealedRecord | sim | De 1 a 32 itens. | | `records[].post` | object | sim | | | `records[].ct_b64` | string | sim | Base64 padrão com padding. Formato: `^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==\|[A-Za-z0-9+/]{3}=)?$`. | | `records[].alg` | string | sim | Valor fixo: `"ed25519"`. | | `records[].signer_kid` | string | sim | 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_b64` | string | sim | 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_b64` | string | não | 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: | status | significado | |---|---| | 200 | Resultado por registro. | | 401 | Credencial ausente ou inválida. Sempre a mesma resposta, sem dizer o motivo (sem oráculo). | | 403 | Autenticado, mas sem permissão (escopo, ambiente test/live, token de cadastro inválido). | | 404 | Recurso inexistente ou de outro tenant (mesma resposta). | | 422 | JSON válido, mas fora do contrato. | | 428 | POST sem Idempotency-Key. | | 429 | Limite de taxa estourado. | Exemplo (cURL): ```bash curl -X POST "https://sandbox.api.bora.id/v1/devices/access-bh-eldorado-03/outbox" \ -H "Authorization: Bearer $BORA_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "post": { "v": 1, "rid": "43f942747a684f61764a6b589e5a0557", "box": "out", "src": "access-bh-eldorado-03", "dst": "bora", "seq": 41, "created_ms": 1790849100000, "suite": "hpke-x25519-sha256-chacha20poly1305", "ct_len": 96, "ct_sha256": "86f4584fac1a57bdd67f82b98d8da05a97c715571097f7c70167b4deeefe0629" }, "ct_b64": "NL6ht3TgTTzJjPh8cwUgSiJ+0fEHSRs0zGT8ItLRnJE0vqG3dOBNPMmM+HxzBSBKIn7R8QdJGzTMZPwi0tGckTS+obd04E08yYz4fHMFIEoiftHxB0kbNMxk/CLS0ZyR", "alg": "ed25519", "signer_kid": "300290aeb182952c1337b227be5fe728", "sig_b64": "vEyNjy1+G698fP8FuJz/lItjTQyV7SI6wcGy9B9YHrSUCfYNjs1WzpvydW2bz0bY9Crd05IIC05b1KctU0peCg==" } ] }' ``` Resposta 200: ```json { "device_id": "access-bh-eldorado-03", "accepted": [ 41 ], "duplicates": [], "rejected": [], "acked_upto_seq": 41 } ``` ## 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: | status | significado | |---|---| | 200 | Serviço no ar. | | 503 | Serviço indisponível. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/health" ``` Resposta 200: ```json { "status": "ok", "api_version": "v1", "contract_version": "1.0.0-draft.1", "server_time": "2026-10-01T10:20:00.000Z" } ``` ### 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: | status | significado | |---|---| | 200 | Lista de chaves. | Exemplo (cURL): ```bash curl "https://sandbox.api.bora.id/v1/keys" ``` Resposta 200: ```json { "keys": [ { "kid": "abb9ff726ba8ffb31638d59e7fcfb820", "alg": "ed25519", "use": [ "erasure_receipt", "gallery", "device_status", "webhook" ], "pub_b64": "4SXx18WKcMv5f/KQIYBA/EbMcJ06DFPAXSTV6JaDcak=", "not_before": "2026-09-01T00:00:00.000Z", "not_after": null } ] } ``` # Build with AI ## Construa com IA Integrar a BORA pode ser tão simples quanto colar um prompt no seu agente de código. O prompt explica o produto, aponta a documentação e descreve o fluxo da prova de conceito, passo a passo. Para agentes de IA: use o [llms.txt](https://docs.bora.id/llms.txt) para ver o índice completo da documentação, ou o [llms-full.txt](https://docs.bora.id/llms-full.txt) para tudo em um arquivo. ## O prompt Cole este prompt no seu agente de código (Claude Code, Cursor, Codex ou outro que leia a web). O mesmo texto está em [prompt.md](https://docs.bora.id/prompt.md). ```markdown # Integrar a API BORA Você vai integrar a API BORA no meu sistema. Siga estas instruções. ## O que é a BORA A BORA faz autenticação pela palma da mão em catracas de transporte. O aparelho BORA ACCESS fica na catraca e reconhece a palma offline. Ele manda um veredito assinado pelo cabo USB ao validador da operadora, o DBD. O DBD decide e abre a catraca. A API BORA é a parte na nuvem: cadastra pessoas, abre o cadastro da palma, guarda as passagens para conciliação e serve de carteiro entre a BORA e os aparelhos. ## Leia antes de escrever código 1. https://docs.bora.id/llms-full.txt: a referência completa em Markdown. 2. https://docs.bora.id/openapi.json: o contrato OpenAPI 3.1 com as 16 operações da prova de conceito. Siga o contrato. Não invente rota, campo nem código de erro. ## Conexão - Base URL do sandbox: https://sandbox.api.bora.id (em breve). Até lá, use a URL que a equipe BORA passar. - Produção: https://api.bora.id (em breve). - Toda chamada de parceiro leva o header `Authorization: Bearer `. - Leia a key da variável de ambiente `BORA_API_KEY`. Nunca escreva a key no código. - No sandbox, use a key de teste (`bora_test_...`). ## Fluxo para implementar 1. Criar a pessoa com o `external_ref` da conta do cliente: `POST /v1/subjects`. O `external_ref` é o ID da conta no meu sistema, nunca o CPF. O CPF é opcional e vai só no corpo. 2. Registrar o consentimento da finalidade de acesso: `POST /v1/subjects/{subject_id}/consents` com `purpose` igual a `identification` e a evidência do aceite (`method`, `terms_version`, `accepted_at`, `evidence_sha256`). 3. Abrir a sessão de cadastro: `POST /v1/enrollment-sessions`. Mostrar o `qr_payload` como QR code para o totem ler, ou o `short_code` para digitar no totem. O `token` só aparece nessa resposta. Não guarde o token em log. 4. Acompanhar o estado do cadastro: `GET /v1/enrollment-sessions/{session_id}` até `completed`. Trate também `expired`, `cancelled`, `failed` e `review`. 5. Consultar as passagens para conciliação: `GET /v1/verdicts`, com filtros (`gate_id`, `device_id`, `from`, `to`, `reconciliation`) e paginação por cursor. A prova assinada de uma passagem está em `GET /v1/verdicts/{verdict_id}`. 6. Carteiro: para cada aparelho (`GET /v1/devices`), buscar `GET /v1/devices/{device_id}/inbox?after_seq=<último seq entregue>` e entregar os registros ao DBD do jeito que vieram. O que o DBD puxar do aparelho, devolver em `POST /v1/devices/{device_id}/outbox`, sem abrir nem alterar. Guarde o último `seq` entregue de cada aparelho. 7. Apagar a pessoa a pedido (LGPD): `DELETE /v1/subjects/{subject_id}`. Guarde o recibo assinado que volta na resposta. ## Regras - Nunca ponha CPF em URL, query string ou log. - Mande `Idempotency-Key` (um UUID v4 novo) em todo POST. Ao repetir a mesma operação, repita a mesma chave. - Se vier `429`, espere os segundos do header `Retry-After` antes de tentar de novo. Em `5xx`, tente de novo com espera crescente e a mesma `Idempotency-Key`. - Os erros vêm em `application/problem+json` com um `code` estável. Trate pelo `code`, nunca pelo texto do `detail`. - No sandbox, use só a key de teste. Nunca use uma key `bora_live_` em teste. - Ignore campos desconhecidos nas respostas. A API só cresce com campos novos. - Não chame rotas internas do aparelho (registro, galeria, envio de vereditos). Elas não são do parceiro. ## Entrega - Um cliente da API com as operações do fluxo, com timeout e tratamento de erro pelo `code`. - Testes do fluxo de ponta a ponta com respostas simuladas a partir dos exemplos do `openapi.json`. - Um resumo curto do que foi feito e de como rodar. ``` ## Docs para agentes O agente pode aprender a API sozinho, lendo estes arquivos. Nada precisa ser configurado. - [llms.txt](https://docs.bora.id/llms.txt): Índice curto da documentação, com um link por página e por rota. - [llms-full.txt](https://docs.bora.id/llms-full.txt): A documentação inteira em um arquivo Markdown. É o melhor contexto para o agente. - [openapi.json](https://docs.bora.id/openapi.json): Contrato OpenAPI 3.1 com as 16 operações. Serve para gerar cliente e validar exemplos. - [skill.md](https://docs.bora.id/skill.md): Agent Skill bora-api para agentes de código, com o fluxo e as regras. Cada página desta documentação tem o botão Copiar página como Markdown, para colar no chat do agente. ### Instalar a skill A skill `bora-api` ensina o fluxo e as regras da API ao seu agente de código. No Claude Code, instale com um comando: ```bash mkdir -p ~/.claude/skills/bora-api && curl -fsSL https://docs.bora.id/skill.md -o ~/.claude/skills/bora-api/SKILL.md ``` Depois é só pedir, por exemplo: integre a API BORA no meu backend. A skill entra sozinha quando o pedido combina com ela. ## Credenciais Hoje a API key de teste (`bora_test_...`) é emitida pela equipe BORA, a pedido. Ela vale só no sandbox, com dados separados de produção. Em breve: um painel para você gerar a sua própria key, escolher os escopos e revogar quando quiser. Guarde a key numa variável de ambiente (`BORA_API_KEY`). Nunca cole a key no prompt nem no código. ## MCP (em breve) Em breve. Um servidor MCP da BORA vai deixar o seu agente consultar esta documentação e chamar o sandbox direto da conversa. Até lá, use o prompt, o `llms-full.txt` e a skill.