# RegistrarObra — documentação completa para desenvolvedores e assistentes de IA > Gerado no build a partir das mesmas fontes da página https://registrarobra.com.br/desenvolvedores. Duas partes: a documentação da API e a especificação OpenAPI 3.1. # API do RegistrarObra O RegistrarObra comprova a anterioridade de um arquivo: que ele já existia numa data e hora, com âncora na blockchain Bitcoin. A API de integração permite que o seu sistema registre obras em lote e acompanhe cada registro até o certificado. > **O arquivo nunca sai da máquina de quem registra.** O seu sistema envia à API a impressão digital do arquivo, mais o nome, o tamanho e os dados da obra e da autoria. Nunca o arquivo. Não existe endpoint de envio de arquivo. Esta documentação é pública; o acesso à API não é. O token de acesso é concedido pela equipe do RegistrarObra, sob contrato. Veja [Como obter acesso](https://registrarobra.com.br/desenvolvedores#acesso). Arquivos para ferramentas: [OpenAPI 3.1 (YAML)](https://registrarobra.com.br/openapi.yaml), [OpenAPI (JSON)](https://registrarobra.com.br/openapi.json), [esta página em markdown](https://registrarobra.com.br/desenvolvedores.md) e [tudo num arquivo, para assistentes de IA](https://registrarobra.com.br/llms-full.txt). ## Regras que importam {#regras} - **O arquivo nunca sai da máquina.** Só a impressão digital do arquivo, o nome, o tamanho e os dados da obra e da autoria chegam à API. - **Acesso por contrato.** A documentação é pública, o acesso não. Cada token pertence a uma conta com liberação ativa, concedida pela equipe. - **Não é registro público.** O RegistrarObra não substitui INPI, Biblioteca Nacional, ANCINE, cartório ou qualquer órgão de registro, e não concede direito autoral, marca ou patente. - **Prova anterioridade, não autoria.** O registro prova que o arquivo existia na data. A autoria que sai no certificado é declarada pelo titular; o RegistrarObra não a verifica. - **Prazos reais.** O certificado costuma sair em até 15 minutos depois que o registro entra em processamento. A confirmação da âncora na blockchain Bitcoin leva algumas horas. Não prometa ao seu usuário confirmação na blockchain em minutos. - **Crédito por obra.** Cada registro criado consome 1 crédito da conta titular. Crédito não se compra pela API. - **Um registro por arquivo.** A mesma impressão digital não é registrada duas vezes, em nenhuma conta. - **Nada se apaga pela API.** Não há endpoint de exclusão. ## Visão geral {#visao-geral} Base URL: `https://auth.registrarobra.com.br/functions/v1` Tudo é JSON em UTF-8, sobre HTTPS. As respostas de erro trazem `error` em português (ver [Erros](https://registrarobra.com.br/desenvolvedores#erros)). | Método | Caminho | Escopo | Para quê | | --- | --- | --- | --- | | GET | `/api-health-check` | `health:read` | Testar a conexão e ver o saldo de créditos | | POST | `/api-submit-bulk-registration-job` | `bulk:create` | Registrar de 1 a 25 obras (consome crédito) | | GET | `/api-get-bulk-job` | `bulk:read` | Situação de um lote e de cada item | | GET | `/api-get-registration` | `registration:read` | Situação de um registro | | GET | `/api-list-registrations` | `registration:read` | Registros mais recentes, com paginação | | GET | `/api-get-registration-artifact` | `artifact:read` | Link de download do certificado ou da ficha | | GET | `/api-verify-hash` | `registration:read` | A verificação pública, por impressão digital | ## Fluxo em três passos {#fluxo} 1. **Obtenha a impressão digital do arquivo no seu sistema.** O arquivo continua onde está; à API vai só a impressão digital, no formato do campo `file_hash` (ver a [especificação OpenAPI](https://registrarobra.com.br/openapi.yaml)). O passo a passo técnico é entregue no início do contrato. 2. **Crie o lote.** `POST /api-submit-bulk-registration-job` com de 1 a 25 obras. A resposta é `202` com o `job_id`; o processamento segue em segundo plano e cada item vira um registro, consumindo 1 crédito. 3. **Acompanhe e baixe.** Receba o [webhook](https://registrarobra.com.br/desenvolvedores#webhooks) do lote ou consulte `GET /api-get-bulk-job`. Com o `registration_id` de cada item, consulte `GET /api-get-registration` até `status` ser `concluido` e baixe o certificado por `GET /api-get-registration-artifact`. ## Autenticação {#autenticacao} Envie o token da integração em todas as chamadas: ```http Authorization: Bearer ro_live_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ``` Os cabeçalhos `x-api-key` e `x-api-token` também são aceitos, com o token puro. - O token (`ro_live_` + 64 caracteres) é mostrado **uma vez**, quando a equipe cria a integração. O RegistrarObra guarda só uma impressão dele; se você perder o token, a equipe gera outro e o anterior deixa de valer. - Guarde o token no servidor, nunca em código que roda no navegador ou no celular do seu cliente. - Para revogar ou trocar um token, fale com a equipe. A revogação vale na hora. - Uma integração pode ser da conta pessoal ou de um workspace. Em workspace, ela vê e cria registros do workspace, e o titular precisa continuar com permissão nele. Escopos: | Escopo | Permite | | --- | --- | | `health:read` | Testar a conexão e ler o saldo de créditos | | `bulk:create` | Criar lotes (consome crédito) | | `bulk:read` | Consultar lotes criados por esta integração | | `registration:read` | Consultar e listar registros; verificar impressão digital | | `artifact:read` | Baixar certificado e ficha | ## Limites {#limites} | Limite | Valor | | --- | --- | | Requisições por integração | Definido no contrato: 30 por minuto por padrão, até 600. Acima disso, `429` com `Retry-After` | | Obras por lote | 1 a 25 | | Corpo do lote | 256 KB | | Arquivo | até 500 GB; arquivo vazio e arquivo comprimido (.zip, .rar, .7z, .iso...) são recusados | | Título | 160 caracteres | | Descrição | 600 caracteres | | Nome do arquivo | 255 caracteres | | Autores por obra | 1 a 10 | | `external_job_id` | até 128 caracteres: letras, números e `. _ : -` | | Link de download | 120 segundos | | Listagem | até 50 registros por página | ## Idempotência {#idempotencia} Envie sempre um `external_job_id` no lote: o identificador do pedido no seu sistema. Se a mesma integração mandar o mesmo `external_job_id` de novo — por um timeout do seu lado, por exemplo —, a API devolve o lote que já existe (`200`, `"idempotent": true`) em vez de criar outro e gastar crédito de novo. Duas chamadas simultâneas com o mesmo valor também resultam num lote só. `order_id` e `job_id` são aceitos como apelidos de `external_job_id`. ## Endpoints {#endpoints} Nos exemplos, o token está na variável de ambiente `REGISTRAROBRA_TOKEN`. Os exemplos em TypeScript rodam em Node.js 18 ou mais novo e usam este auxiliar: ```ts const BASE = "https://auth.registrarobra.com.br/functions/v1"; export async function api(caminho: string, init: RequestInit = {}): Promise { const resposta = await fetch(`${BASE}/${caminho}`, { ...init, headers: { Authorization: `Bearer ${process.env.REGISTRAROBRA_TOKEN}`, "Content-Type": "application/json", ...init.headers, }, }); const corpo = await resposta.json(); if (!resposta.ok) throw Object.assign(new Error(corpo.error), { status: resposta.status, corpo }); return corpo as T; } ``` ### GET /api-health-check {#api-health-check} Testa token, escopo e webhook, sem efeito colateral, e mostra o saldo de créditos da conta titular. Escopo `health:read`. ```bash curl -s https://auth.registrarobra.com.br/functions/v1/api-health-check \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" ``` ```ts const saude = await api<{ credits: { available: number } }>("api-health-check"); console.log(`Créditos disponíveis: ${saude.credits.available}`); ``` Resposta `200`: ```json { "ok": true, "safe_test": true, "side_effects": false, "server_time": "2026-09-19T12:00:00.000Z", "integration": { "id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "name": "Editora Exemplo", "status": "active", "key_prefix": "ro_live_01234567", "scopes": ["health:read", "bulk:create", "bulk:read", "registration:read", "artifact:read"], "rate_limit_per_minute": 30, "webhook_configured": true, "last_used_at": "2026-09-19T11:58:00.000Z" }, "credits": { "available": 42 }, "checks": { "authentication": "ok", "scope": "health:read", "webhook": "configured" }, "next_recommended_calls": ["POST /api-submit-bulk-registration-job", "GET /api-get-bulk-job", "GET /api-get-registration"] } ``` ### POST /api-submit-bulk-registration-job {#api-submit-bulk-registration-job} Registra de 1 a 25 obras com a mesma categoria e a mesma autoria. Escopo `bulk:create`. Consome 1 crédito por obra registrada; a API confere o saldo antes de aceitar e recusa o lote inteiro (`409`) se faltar crédito ou se alguma impressão digital já tiver registro. Campos principais: - `work_type`: categoria — `documento`, `musica`, `roteiro`, `livro`, `foto`, `video`, `argumento`, `obra_artistica`, `filme`, `planta_arquitetura`, `marca` (identidade visual; não substitui o registro de marca no INPI) ou `patente` (invenção; não é patente). - `authors`: 1 a 10 autores, na ordem; o primeiro é o principal. Nome completo, CPF válido, nacionalidade, CEP, estado, cidade, bairro, rua e número são obrigatórios. Coautores precisam de e-mail: recebem convite para confirmar. - `items`: cada obra com `title`, `file_name`, `file_hash` (a impressão digital do arquivo, no formato da especificação), `file_size` em bytes e, se quiser, `description` própria. - `consent`: os quatro aceites do site, todos `true`. Colete-os do titular no seu sistema: aceite dos Termos de Uso, da Política de Privacidade, da responsabilidade por guardar o arquivo original e da execução imediata e irreversível. Eles ficam gravados em cada obra, com o id da sua integração. - `external_job_id`: seu identificador do pedido (ver [Idempotência](https://registrarobra.com.br/desenvolvedores#idempotencia)). ```bash curl -s -X POST https://auth.registrarobra.com.br/functions/v1/api-submit-bulk-registration-job \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "external_job_id": "pedido-2026-0042", "name": "Álbum Cerrado", "work_type": "musica", "description": "Faixas do álbum Cerrado.", "consent": { "accepted_terms": true, "accepted_privacy": true, "acknowledged_original_file_responsibility": true, "acknowledged_immediate_execution_and_irreversibility": true }, "authors": [{ "full_name": "Maria da Silva", "cpf": "529.982.247-25", "email": "maria@exemplo.com.br", "nationality": "Brasileira", "cep": "70610-420", "state": "DF", "city": "Brasília", "neighborhood": "Asa Sul", "street": "SQS 308 Bloco A", "number": "101" }], "items": [{ "title": "Chuva de Outubro", "file_name": "chuva-de-outubro.wav", "file_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "file_size": 48213996 }] }' ``` ```ts import { stat } from "node:fs/promises"; import { basename } from "node:path"; const caminho = "/obras/chuva-de-outubro.wav"; const impressaoDigitalDoArquivo: string = obterImpressaoDigital(caminho); // do seu sistema const lote = await api<{ job_id: string; idempotent?: boolean }>("api-submit-bulk-registration-job", { method: "POST", body: JSON.stringify({ external_job_id: "pedido-2026-0042", work_type: "musica", consent: { accepted_terms: true, accepted_privacy: true, acknowledged_original_file_responsibility: true, acknowledged_immediate_execution_and_irreversibility: true, }, authors: [{ full_name: "Maria da Silva", cpf: "529.982.247-25", nationality: "Brasileira", cep: "70610-420", state: "DF", city: "Brasília", neighborhood: "Asa Sul", street: "SQS 308 Bloco A", number: "101", }], items: [{ title: "Chuva de Outubro", file_name: basename(caminho), file_hash: impressaoDigitalDoArquivo, file_size: (await stat(caminho)).size, }], }), }); console.log(lote.job_id); ``` Resposta `202`: ```json { "success": true, "queued": true, "job_id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b", "external_job_id": "pedido-2026-0042", "total_items": 1, "status": "queued" } ``` Reenvio com o mesmo `external_job_id` responde `200` com `"idempotent": true`, o `job_id` existente e os contadores atuais. ### GET /api-get-bulk-job {#api-get-bulk-job} Situação de um lote criado por esta integração e de cada item, na ordem de envio. Informe `job_id` ou `external_job_id`. Escopo `bulk:read`. ```bash curl -s "https://auth.registrarobra.com.br/functions/v1/api-get-bulk-job?external_job_id=pedido-2026-0042" \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" ``` ```ts const { job, items } = await api<{ job: { status: string }; items: Array<{ title: string; status: string; registration_id: string | null }> }>( `api-get-bulk-job?job_id=${lote.job_id}`, ); for (const item of items) console.log(item.title, item.status, item.registration_id); ``` Resposta `200` (resumida): ```json { "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "job": { "id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b", "name": "Álbum Cerrado", "status": "completed", "total_items": 1, "completed_items": 1, "failed_items": 0, "created_registrations": 1, "external_job_id": "pedido-2026-0042", "callback_attempts": 1, "callback_last_status": 200 }, "items": [{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "position": 0, "title": "Chuva de Outubro", "file_name": "chuva-de-outubro.wav", "file_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "file_size": 48213996, "status": "completed", "error_message": null, "registration_id": "3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f" }] } ``` Lote `completed` quer dizer que os registros foram criados e entraram em emissão — não que os certificados já saíram. Veja [Situações](https://registrarobra.com.br/desenvolvedores#status). ### GET /api-get-registration {#api-get-registration} Um registro da conta (ou do workspace) da integração. Escopo `registration:read`. ```bash curl -s "https://auth.registrarobra.com.br/functions/v1/api-get-registration?registration_id=3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f" \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" ``` ```ts const { registration, artifacts } = await api<{ registration: { status: string; blockchain_tx_id: string | null }; artifacts: { certificate: { available: boolean } }; }>(`api-get-registration?registration_id=${registrationId}`); const bloco = registration.blockchain_tx_id?.match(/^bitcoin-h:(\d+)/)?.[1]; console.log(registration.status, artifacts.certificate.available, bloco ? `bloco ${bloco}` : "âncora aguardando"); ``` Resposta `200`: ```json { "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "registration": { "id": "3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f", "title": "Chuva de Outubro", "description": "Faixas do álbum Cerrado.", "authors": "Maria da Silva", "work_type": "musica", "status": "concluido", "payment_status": "paid", "product_type": "registro", "processing_step": "done", "file_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "blockchain_tx_id": "bitcoin-h:915000", "ficha_hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", "ficha_blockchain_tx_id": "bitcoin-h:915000", "created_at": "2026-09-19T12:00:05.000Z", "paid_at": "2026-09-19T12:00:06.000Z", "completed_at": "2026-09-19T12:07:41.000Z", "bulk_job_id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b" }, "artifacts": { "certificate": { "available": true, "endpoint": "https://…/functions/v1/api-get-registration-artifact?registration_id=…&artifact=certificate" }, "ficha": { "available": true, "endpoint": "https://…/functions/v1/api-get-registration-artifact?registration_id=…&artifact=ficha" } } } ``` - `blockchain_tx_id` é um identificador interno da âncora. Trate-o como opaco, com uma exceção: quando começa com `bitcoin-h:`, o número depois dos dois-pontos é a altura do bloco Bitcoin que confirma a âncora. Qualquer outro valor significa âncora ainda não confirmada. - `ficha_hash` é a impressão digital da ficha de cadastro, o documento com a autoria declarada, ancorado junto com o arquivo. - `processing_step` é informativo; não dependa dos valores. - `artifacts` pode trazer outras entradas além de `certificate` e `ficha`; ignore as que não conhecer. ### GET /api-list-registrations {#api-list-registrations} Registros da conta (ou do workspace) da integração, do mais novo para o mais antigo. Parâmetros: `limit` (1 a 50, padrão 20) e `before` (data ISO 8601). Quando há mais página, a resposta traz `next_before`: mande-o em `before` na chamada seguinte. Escopo `registration:read`. ```bash curl -s "https://auth.registrarobra.com.br/functions/v1/api-list-registrations?limit=20" \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" ``` ```ts let before: string | null = null; do { const pagina = await api<{ registrations: Array<{ id: string; title: string; status: string }>; next_before: string | null }>( `api-list-registrations?limit=50${before ? `&before=${encodeURIComponent(before)}` : ""}`, ); for (const registro of pagina.registrations) console.log(registro.id, registro.status, registro.title); before = pagina.next_before; } while (before); ``` Resposta `200`: ```json { "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "registrations": [{ "id": "3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f", "title": "Chuva de Outubro", "work_type": "musica", "status": "concluido", "payment_status": "paid", "product_type": "registro", "file_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "created_at": "2026-09-19T12:00:05.000Z", "paid_at": "2026-09-19T12:00:06.000Z", "completed_at": "2026-09-19T12:07:41.000Z", "bulk_job_id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b" }], "next_before": null } ``` ### GET /api-get-registration-artifact {#api-get-registration-artifact} Link de download de um documento de registro concluído: `artifact=certificate` (certificado em PDF) ou `artifact=ficha` (ficha de cadastro em PDF). A URL é assinada e vale 120 segundos: baixe logo. Com `return=redirect`, a API responde `302` direto para o arquivo. Antes da conclusão, responde `409`. Escopo `artifact:read`. ```bash curl -sL "https://auth.registrarobra.com.br/functions/v1/api-get-registration-artifact?registration_id=3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f&artifact=certificate&return=redirect" \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" \ -o certificado.pdf ``` ```ts import { writeFile } from "node:fs/promises"; const documento = await api<{ url: string; file_name: string }>( `api-get-registration-artifact?registration_id=${registrationId}&artifact=certificate`, ); const pdf = await fetch(documento.url); await writeFile(documento.file_name, Buffer.from(await pdf.arrayBuffer())); ``` Resposta `200`: ```json { "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "registration_id": "3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f", "artifact": "certificate", "file_name": "certificado-3C9D8E7F.pdf", "content_type": "application/pdf", "url": "https://…/storage/v1/object/sign/…", "expires_in_seconds": 120 } ``` ### GET /api-verify-hash {#api-verify-hash} A mesma verificação pública da página [registrarobra.com.br/verificar](https://registrarobra.com.br/verificar): diz se existe registro **concluído** com a impressão digital informada (do arquivo ou da ficha de cadastro) e, se existir, a data, a obra, a autoria declarada e a situação da âncora. Não olha a conta: o resultado é o que qualquer pessoa vê na página pública. Escopo `registration:read`. ```bash curl -s "https://auth.registrarobra.com.br/functions/v1/api-verify-hash?file_hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" \ -H "Authorization: Bearer $REGISTRAROBRA_TOKEN" ``` ```ts const verificacao = await api<{ found: boolean; registered_at?: string; anchor?: { confirmed: boolean } }>( `api-verify-hash?file_hash=${impressaoDigitalDoArquivo}`, ); console.log(verificacao.found ? `registrado em ${verificacao.registered_at}` : "sem registro concluído"); ``` Resposta `200`: ```json { "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "found": true, "matched": "file", "registered_at": "2026-09-19T12:00:05.000Z", "title": "Chuva de Outubro", "authors_declared": "Maria da Silva", "work_type": "musica", "anchor": { "confirmed": true, "bitcoin_block_height": 915000, "bitcoin_block_hash": "00000000000000000001a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7", "bitcoin_block_time": "2026-09-19T14:31:02.000Z" }, "verification_url": "https://registrarobra.com.br/verificar?hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" } ``` Sem registro concluído, a resposta é `{"found": false, "verification_url": "…"}`. ### Endpoints descontinuados {#descontinuados} `POST /api-create-upload-session` e `GET /api-get-upload-session` respondem `410 Gone`. Eram do tempo em que se enviava o arquivo; hoje o arquivo nunca é enviado. Use `POST /api-submit-bulk-registration-job`. ## Webhooks {#webhooks} Se a integração tiver uma URL de webhook (cadastrada pela equipe, `https` e com endereço público), o RegistrarObra avisa quando o processamento de um lote criado pela API termina. O evento é `bulk_job.` — normalmente `bulk_job.completed`, `bulk_job.partial` ou `bulk_job.failed`. O webhook anuncia o resultado do **lote** (registros criados e créditos consumidos), não a emissão do certificado. Depois dele, acompanhe cada registro por `GET /api-get-registration`. Cabeçalhos de cada entrega: | Cabeçalho | Conteúdo | | --- | --- | | `Content-Type` | `application/json` | | `User-Agent` | `RegistrarObra-Webhooks/1.0` | | `x-registrarobra-event` | O evento, como `bulk_job.completed` | | `x-registrarobra-delivery` | Id desta tentativa (muda a cada tentativa) | | `x-registrarobra-timestamp` | Momento do envio, em segundos Unix | | `x-registrarobra-signature` | `t=,v1=` | Corpo: ```json { "event": "bulk_job.completed", "integration_id": "5b0c2f4e-3a1d-4c7b-9e8f-1a2b3c4d5e6f", "external_job_id": "pedido-2026-0042", "job": { "id": "0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b", "name": "Álbum Cerrado", "status": "completed", "total_items": 1, "completed_items": 1, "failed_items": 0, "created_registrations": 1 }, "items": [{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "position": 0, "title": "Chuva de Outubro", "status": "completed", "error_message": null, "registration_id": "3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f", "file_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "completed_at": "2026-09-19T12:00:06.000Z" }], "delivered_at": "2026-09-19T12:00:07.000Z", "delivery_id": "c0ffee00-1234-4abc-9def-001122334455" } ``` **Verifique a assinatura antes de confiar no corpo.** A assinatura é o HMAC-SHA256, com o segredo `ro_whsec_...` da integração (mostrado uma vez, na criação), do texto `.`. Use o corpo exatamente como chegou, antes de interpretar o JSON, e recuse `t` a mais de 5 minutos do seu relógio. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function assinaturaValida(corpoCru: string, cabecalho: string | undefined, segredo: string, agora = Date.now()) { const partes = Object.fromEntries((cabecalho ?? "").split(",").map((parte) => { const [chave, ...resto] = parte.split("="); return [chave.trim(), resto.join("=").trim()]; })); const t = Number(partes.t); if (!Number.isSafeInteger(t) || !/^[0-9a-f]{64}$/.test(partes.v1 ?? "")) return false; if (Math.abs(agora / 1000 - t) > 300) return false; const esperado = createHmac("sha256", segredo).update(`${t}.${corpoCru}`).digest("hex"); return timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1)); } ``` Com Express, leia o corpo cru: ```ts import express from "express"; const app = express(); app.post("/webhooks/registrarobra", express.text({ type: "application/json" }), (req, res) => { if (!assinaturaValida(req.body, req.header("x-registrarobra-signature"), process.env.REGISTRAROBRA_WEBHOOK_SECRET!)) { return res.sendStatus(401); } const evento = JSON.parse(req.body); res.sendStatus(200); // responda rápido; processe depois void processarLote(evento.job.id, evento.items); }); ``` Entrega e novas tentativas: - Responda `2xx` em até 5 segundos. Qualquer outro status, tempo esgotado ou redirecionamento (`3xx`) conta como falha; o RegistrarObra não segue redirecionamentos. - São até 5 tentativas, com intervalos aproximados de 1 minuto, 5 minutos, 15 minutos e 1 hora. Depois disso a entrega para; a equipe pode reenviar. - A entrega é "pelo menos uma vez": trate repetição pelo par `job.id` + `event`, não pelo `delivery_id`. - Da sua resposta, só o status HTTP é guardado. O último status aparece em `callback_last_status` no `GET /api-get-bulk-job`. ## Erros {#erros} Todo erro vem como JSON com `error` (mensagem em português) e, quando houver, `details`. Não dependa do texto da mensagem: use o status HTTP e os campos de `details`. ```json { "error": "Lote invalido", "details": { "issues": [ { "field": "items[0].file_hash", "message": "SHA-256 em hexadecimal minúsculo (64 caracteres)" }, { "field": "authors[1].email", "message": "obrigatório para coautores" } ] } } ``` | Status | Quando | | --- | --- | | `400` | Parâmetro ausente ou malformado; corpo inválido, com `details.issues` (`field` e `message` para cada problema) | | `401` | Token ausente, inválido ou revogado | | `403` | Escopo insuficiente, acesso à API não liberado para o titular, ou titular sem permissão no workspace | | `404` | Lote ou registro não encontrado para esta integração | | `405` | Método HTTP errado | | `409` | Impressão digital já registrada (`details.file_hashes`); créditos insuficientes (`details.required_credits`, `details.available_credits`); documento pedido antes da conclusão | | `410` | Endpoint descontinuado | | `413` | Corpo maior que o limite do endpoint | | `429` | Limite por minuto atingido; espere os segundos de `Retry-After` | | `500` | Erro inesperado; a resposta traz `request_id` (e o cabeçalho `x-request-id`) para o suporte | ## Situações {#status} | Objeto | Valor | Significado | | --- | --- | --- | | Lote | `queued` | Na fila | | Lote | `processing` | Criando os registros | | Lote | `completed` | Todos os itens resolvidos (registrados ou já registrados antes) | | Lote | `partial` | Parte dos itens falhou | | Lote | `failed` | Nenhum item foi registrado | | Item | `completed` | Registro criado e 1 crédito consumido | | Item | `duplicate` | O arquivo já tinha registro; nenhum crédito consumido | | Item | `failed` | Não registrado; veja `error_message` | | Registro | `pendente` | Criado, aguardando confirmação (crédito ainda não consumido) | | Registro | `processando` | Crédito consumido; emissão em andamento | | Registro | `concluido` | Certificado disponível | A âncora na blockchain Bitcoin é confirmada algumas horas depois da conclusão. Enquanto isso, o certificado já pode ser baixado e mostra a âncora como aguardando; o `blockchain_tx_id` passa a começar com `bitcoin-h:` quando o bloco confirma. ## Créditos e cobrança {#creditos} - Cada obra registrada pela API consome 1 crédito da conta titular da integração — a conta pessoal do titular, também quando a integração é de um workspace. Item `duplicate` não consome. - O saldo aparece em `credits.available` no `GET /api-health-check`. - Crédito não se compra pela API. A conta titular compra créditos no painel do site ("Comprar créditos"), com preço por faixa de volume ([registrarobra.com.br/planos](https://registrarobra.com.br/planos)), ou recebe créditos conforme o contrato, pela equipe. ## Usando com IA {#usando-com-ia} Se você integra com a ajuda de um assistente de IA, aponte-o para [/llms-full.txt](https://registrarobra.com.br/llms-full.txt), que traz esta documentação e a especificação num arquivo só, e para a [especificação OpenAPI](https://registrarobra.com.br/openapi.yaml). Um começo de conversa: ```text Você vai me ajudar a integrar meu sistema com a API do RegistrarObra. Leia a documentação completa em https://registrarobra.com.br/llms-full.txt e a especificação OpenAPI em https://registrarobra.com.br/openapi.yaml. Regras que não podem ser quebradas: - O arquivo da obra nunca é enviado à API; siga a documentação para o formato de cada campo. - O token da API fica só no servidor, em variável de ambiente; nunca no código do navegador nem em logs. - Todo lote leva external_job_id, para não gastar crédito duas vezes num reenvio. - Verifique a assinatura de todo webhook antes de usar o corpo. - Não prometa ao usuário final confirmação na blockchain Bitcoin em minutos: o certificado costuma sair em até 15 minutos; a âncora confirma em algumas horas. - O RegistrarObra comprova anterioridade; não é registro público e a autoria é declarada, não verificada. - Não há endpoint que apague registro, e crédito não se compra pela API. ``` ## Como obter acesso {#acesso} O acesso à API é concedido pela equipe do RegistrarObra, sob contrato. Não existe cadastro de desenvolvedor pelo site. Para conversar, escreva para [contato@registrarobra.com.br](mailto:contato@registrarobra.com.br) ou use a página [Contato](https://registrarobra.com.br/contato). Conte: - quem é a empresa e qual o caso de uso; - o volume estimado de obras por mês; - se precisa de webhook (e a URL `https` que vai recebê-lo); - se a integração é da conta pessoal ou de um workspace. Com o contrato, a equipe libera o acesso na conta titular, cria a integração com os escopos e o limite combinados e entrega o token (e o segredo do webhook) uma única vez. ## Changelog {#changelog} - **2026-09-19** — Documentação pública e arquivos para assistentes de IA. Novos: `GET /api-list-registrations`, `GET /api-verify-hash` e o saldo de créditos (`credits.available`) no `GET /api-health-check`. - **2026-09-16** — API sob contrato: acesso liberado pela equipe; token e segredo do webhook mostrados uma vez; assinatura de webhook obrigatória no formato `t=...,v1=...`; os quatro aceites passaram a ser obrigatórios no lote; validação completa de autores e itens; idempotência também em chamadas simultâneas. - **2026-03-21** — O arquivo deixou de ser enviado: o registro passou a usar só a impressão digital. Os endpoints de sessão de envio foram descontinuados (`410`). --- # Especificação OpenAPI 3.1 da API de integração Também em https://registrarobra.com.br/openapi.yaml e https://registrarobra.com.br/openapi.json. ```yaml openapi: 3.1.0 info: title: API de integração do RegistrarObra version: "2026-09-19" summary: Registro de anterioridade de obras por integração, sob contrato. description: | O RegistrarObra comprova a anterioridade de um arquivo: que ele já existia numa data, com âncora na blockchain Bitcoin. **O arquivo nunca sai da máquina do cliente.** A API recebe a impressão digital SHA-256 do arquivo (64 caracteres hexadecimais minúsculos), calculada do lado do integrador, além do nome, do tamanho e dos dados da obra e da autoria declarada. Nunca o arquivo. O acesso é concedido pela equipe do RegistrarObra, sob contrato. A documentação é pública; o acesso não é. Contato comercial: contato@registrarobra.com.br. Limites do serviço: não é registro público e não substitui INPI, Biblioteca Nacional, ANCINE ou cartório; prova anterioridade, não autoria; a autoria é declarada pelo titular e não é verificada. O certificado costuma sair em até 15 minutos depois que o registro entra em processamento; a confirmação da âncora na blockchain Bitcoin leva algumas horas. Documentação completa: https://registrarobra.com.br/desenvolvedores contact: name: Comercial RegistrarObra email: contato@registrarobra.com.br url: https://registrarobra.com.br/desenvolvedores#acesso license: name: Proprietária — uso sob contrato url: https://registrarobra.com.br/termos servers: - url: https://auth.registrarobra.com.br/functions/v1 description: Produção security: - bearerAuth: [] tags: - name: Conexão description: "Teste de conexão e saldo de créditos." - name: Registro em lote description: "Registro de 1 a 25 obras por chamada; consome crédito." - name: Consulta description: "Situação de lotes e registros, documentos e verificação pública." - name: Descontinuados description: "Endpoints antigos que respondem 410." paths: /api-health-check: get: operationId: healthCheck tags: [Conexão] summary: Testa a conexão e mostra o saldo de créditos description: | Chamada sem efeito colateral: confere token, escopo e webhook, e devolve o saldo de créditos do titular da integração. Escopo `health:read`. x-required-scope: health:read responses: "200": description: Conexão válida. content: application/json: schema: { $ref: "#/components/schemas/HealthCheckResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-submit-bulk-registration-job: post: operationId: submitBulkRegistrationJob tags: [Registro em lote] summary: Cria um lote de 1 a 25 registros description: | Registra de 1 a 25 obras com a mesma categoria e a mesma autoria. Cada item leva a impressão digital SHA-256 do arquivo, o nome e o tamanho — nunca o arquivo. O lote é aceito (202) e processado em segundo plano: cada item vira um registro e consome 1 crédito da conta titular. Item cujo arquivo já tinha registro fica como `duplicate` e não consome crédito. Escopo `bulk:create`. Idempotência: envie `external_job_id`. O mesmo valor nesta integração devolve o lote já criado (200, `idempotent: true`) em vez de criar outro. x-required-scope: bulk:create requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BulkJobRequest" } responses: "202": description: Lote aceito e na fila. content: application/json: schema: { $ref: "#/components/schemas/BulkJobAccepted" } "200": description: Lote já existente com o mesmo `external_job_id` (resposta idempotente). content: application/json: schema: { $ref: "#/components/schemas/BulkJobIdempotent" } "400": { $ref: "#/components/responses/ValidationError" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": description: Perfil do titular da integração não encontrado. content: application/json: schema: { $ref: "#/components/schemas/Error" } "409": description: | Uma ou mais impressões digitais já têm registro (`details.file_hashes`), ou a conta não tem créditos para o lote inteiro (`details.required_credits`, `details.available_credits`). Nada é criado. content: application/json: schema: { $ref: "#/components/schemas/Error" } examples: duplicado: value: error: Uma ou mais hashes deste lote ja foram registradas anteriormente details: file_hashes: ["9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"] semCredito: value: error: Creditos insuficientes para este lote details: { required_credits: 3, available_credits: 1 } "413": { $ref: "#/components/responses/PayloadTooLarge" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-get-bulk-job: get: operationId: getBulkJob tags: [Consulta] summary: Situação de um lote e dos itens description: Informe `job_id` ou `external_job_id`. Só lotes criados por esta integração. Escopo `bulk:read`. x-required-scope: bulk:read parameters: - name: job_id in: query schema: { type: string, format: uuid } - name: external_job_id in: query schema: { $ref: "#/components/schemas/ExternalId" } responses: "200": description: Lote e itens, na ordem de envio. content: application/json: schema: { $ref: "#/components/schemas/BulkJobDetails" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-get-registration: get: operationId: getRegistration tags: [Consulta] summary: Um registro e os documentos disponíveis description: | Registro da conta (ou do workspace) da integração. Quando `status` é `concluido`, o certificado e a ficha podem ser baixados por `/api-get-registration-artifact`. Escopo `registration:read`. x-required-scope: registration:read parameters: - name: registration_id in: query required: true schema: { type: string, format: uuid } responses: "200": description: Registro encontrado. content: application/json: schema: { $ref: "#/components/schemas/RegistrationResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-list-registrations: get: operationId: listRegistrations tags: [Consulta] summary: Lista os registros mais recentes description: | Registros da conta (ou do workspace) da integração, do mais novo para o mais antigo. Paginação por cursor: quando houver mais, a resposta traz `next_before`; mande-o em `before` na chamada seguinte. Escopo `registration:read`. x-required-scope: registration:read parameters: - name: limit in: query schema: { type: integer, minimum: 1, maximum: 50, default: 20 } - name: before in: query description: Data ISO 8601; devolve registros criados antes dela. schema: { type: string, format: date-time } responses: "200": description: Página de registros. content: application/json: schema: { $ref: "#/components/schemas/RegistrationList" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-get-registration-artifact: get: operationId: getRegistrationArtifact tags: [Consulta] summary: Link de download do certificado ou da ficha description: | Devolve uma URL assinada, válida por 120 segundos, para baixar o documento de um registro concluído. Com `return=redirect`, responde 302 direto para a URL. Escopo `artifact:read`. x-required-scope: artifact:read parameters: - name: registration_id in: query required: true schema: { type: string, format: uuid } - name: artifact in: query required: true schema: type: string enum: [certificate, ficha] description: "`certificate`: certificado em PDF. `ficha`: ficha de cadastro em PDF." - name: return in: query schema: type: string enum: [redirect] responses: "200": description: URL assinada de curta duração. content: application/json: schema: { $ref: "#/components/schemas/ArtifactResponse" } "302": description: Redirecionamento para a URL assinada (com `return=redirect`). headers: Location: schema: { type: string, format: uri } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": description: O registro ainda não foi concluído; os documentos saem depois da emissão. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-verify-hash: get: operationId: verifyHash tags: [Consulta] summary: Verifica se uma impressão digital tem registro concluído description: | A mesma verificação pública da página https://registrarobra.com.br/verificar: diz se existe registro concluído com esta impressão digital (do arquivo ou da ficha de cadastro) e, se existir, data, obra, autoria declarada e situação da âncora. Não olha a conta: o resultado é o que qualquer pessoa vê na página pública. Escopo `registration:read`. x-required-scope: registration:read parameters: - name: file_hash in: query required: true schema: { $ref: "#/components/schemas/Sha256Hex" } responses: "200": description: Resultado da verificação. content: application/json: schema: { $ref: "#/components/schemas/VerifyResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/TooManyRequests" } "500": { $ref: "#/components/responses/InternalError" } /api-create-upload-session: post: operationId: createUploadSessionDeprecated tags: [Descontinuados] deprecated: true summary: Descontinuado — o arquivo nunca é enviado security: [] responses: "410": { $ref: "#/components/responses/Gone" } /api-get-upload-session: get: operationId: getUploadSessionDeprecated tags: [Descontinuados] deprecated: true summary: Descontinuado — o arquivo nunca é enviado security: [] responses: "410": { $ref: "#/components/responses/Gone" } webhooks: bulkJobFinished: post: operationId: bulkJobFinishedWebhook summary: Resultado do processamento de um lote description: | Enviado para a URL de webhook da integração (cadastrada pela equipe) quando o processamento de um lote criado pela API termina uma passagem. O evento é `bulk_job.`; normalmente `bulk_job.completed`, `bulk_job.partial` ou `bulk_job.failed`. O webhook anuncia o resultado do LOTE (registros criados e créditos consumidos), não a emissão do certificado: acompanhe cada registro por `/api-get-registration`. Assinatura obrigatória no cabeçalho `x-registrarobra-signature`: `t=,v1=.">`, com o segredo `ro_whsec_...` da integração. Recuse assinatura inválida ou com `t` a mais de 5 minutos do seu relógio. Responda 2xx em até 5 segundos; redirecionamento conta como falha. Até 5 tentativas (intervalos aproximados de 1 min, 5 min, 15 min e 1 h). parameters: - name: x-registrarobra-event in: header required: true schema: { type: string, example: bulk_job.completed } - name: x-registrarobra-delivery in: header required: true description: Id desta tentativa de entrega (muda a cada tentativa). schema: { type: string, format: uuid } - name: x-registrarobra-timestamp in: header required: true schema: { type: string, example: "1789999999" } - name: x-registrarobra-signature in: header required: true schema: { type: string, example: "t=1789999999,v1=5d41402abc4b2a76b9719d911017c592ae2f6b1c1f6b0c1a9b2e3f4a5b6c7d8e" } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/WebhookPayload" } responses: "200": description: Qualquer 2xx confirma o recebimento. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: ro_live_<64 hex> description: | Token da integração, entregue uma vez pela equipe quando a integração é criada. Também aceito nos cabeçalhos `x-api-key` ou `x-api-token`. responses: BadRequest: description: Parâmetro ausente ou malformado. content: application/json: schema: { $ref: "#/components/schemas/Error" } ValidationError: description: Corpo inválido; `details.issues` lista cada problema. content: application/json: schema: { $ref: "#/components/schemas/Error" } example: error: Lote invalido details: issues: - { field: "items[0].file_hash", message: "SHA-256 em hexadecimal minúsculo (64 caracteres)" } - { field: consent, message: "aceites obrigatórios ausentes: accepted_terms" } Unauthorized: description: Token ausente, inválido ou revogado. content: application/json: schema: { $ref: "#/components/schemas/Error" } Forbidden: description: Escopo insuficiente, acesso à API não liberado para o titular, ou titular sem permissão no workspace. content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: Não encontrado para esta integração. content: application/json: schema: { $ref: "#/components/schemas/Error" } PayloadTooLarge: description: Corpo maior que o limite do endpoint. content: application/json: schema: { $ref: "#/components/schemas/Error" } TooManyRequests: description: Limite de requisições por minuto da integração atingido. headers: Retry-After: description: Segundos até poder tentar de novo. schema: { type: integer } content: application/json: schema: { $ref: "#/components/schemas/Error" } InternalError: description: Erro inesperado. Informe o `request_id` ao suporte. headers: x-request-id: schema: { type: string, format: uuid } content: application/json: schema: { $ref: "#/components/schemas/Error" } Gone: description: Endpoint descontinuado. content: application/json: schema: type: object properties: error: { type: string } deprecated: { type: boolean, const: true } replacement: { type: string, example: POST /api-submit-bulk-registration-job } schemas: Error: type: object required: [error] properties: error: type: string description: Mensagem em português. Não dependa do texto; use o status HTTP e `details`. details: description: Detalhes estruturados, quando houver. type: object additionalProperties: true properties: issues: type: array items: type: object required: [field, message] properties: field: { type: string, example: "authors[0].cpf" } message: { type: string } file_hashes: type: array items: { $ref: "#/components/schemas/Sha256Hex" } required_credits: { type: integer } available_credits: { type: integer } request_id: type: string format: uuid description: Só em erro 500. Sha256Hex: type: string pattern: "^[a-f0-9]{64}$" description: | Impressão digital SHA-256 do arquivo, em hexadecimal minúsculo (64 caracteres), calculada sobre os bytes do arquivo, do lado do integrador. Maiúsculas são recusadas. O SHA-256 do arquivo vazio é recusado. example: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 ExternalId: type: string maxLength: 128 pattern: "^[A-Za-z0-9._:-]+$" WorkType: type: string description: | Categoria da obra. `marca` (Identidade visual) não substitui o registro de marca no INPI; `patente` (Invenção) não é patente e não substitui o depósito no INPI. enum: [documento, musica, roteiro, livro, foto, video, argumento, obra_artistica, filme, planta_arquitetura, marca, patente] Author: type: object description: Autoria declarada pelo titular. O RegistrarObra não verifica autoria. required: [full_name, cpf, nationality, cep, state, city, neighborhood, street, number] properties: full_name: { type: string, maxLength: 200 } artistic_name: { type: [string, "null"], maxLength: 200 } cpf: { type: string, description: "CPF válido (com ou sem pontuação)." } email: type: [string, "null"] maxLength: 254 description: Obrigatório para coautores (a partir do segundo autor), que recebem convite para confirmar. phone: { type: [string, "null"], description: "8 a 15 dígitos." } nationality: { type: string, maxLength: 60 } cep: { type: string, description: "8 dígitos." } state: { type: string, maxLength: 60 } city: { type: string, maxLength: 120 } neighborhood: { type: string, maxLength: 120 } street: { type: string, maxLength: 200 } number: { type: string, maxLength: 20 } complement: { type: [string, "null"], maxLength: 120 } BulkItem: type: object required: [title, file_name, file_hash, file_size] properties: title: { type: string, maxLength: 160, description: "Título da obra, como sai no certificado." } description: { type: [string, "null"], maxLength: 600, description: "Resumo próprio; sem ele, vale o do lote." } file_name: type: string maxLength: 255 description: Nome original do arquivo. Arquivos comprimidos (.zip, .rar, .7z, .iso...) são recusados. file_hash: { $ref: "#/components/schemas/Sha256Hex" } file_size: type: integer minimum: 1 maximum: 536870912000 description: Tamanho em bytes (até 500 GB). file_metadata: type: [object, "null"] additionalProperties: true description: Metadados técnicos opcionais do arquivo (tipo, datas). Não coloque conteúdo do arquivo aqui. BulkJobRequest: type: object required: [work_type, authors, items, consent] properties: name: { type: [string, "null"], maxLength: 200, description: "Nome do lote." } work_type: { $ref: "#/components/schemas/WorkType" } description: { type: [string, "null"], maxLength: 600, description: "Resumo usado nas obras sem descrição própria." } external_job_id: allOf: [{ $ref: "#/components/schemas/ExternalId" }] description: Seu identificador do pedido, para idempotência. `order_id` e `job_id` são aceitos como apelidos. consent: type: object description: | Os quatro aceites do site, dados pelo titular por meio do sistema integrador. Todos precisam ser `true`; ficam gravados por obra, com o id da integração. required: [accepted_terms, accepted_privacy, acknowledged_original_file_responsibility, acknowledged_immediate_execution_and_irreversibility] properties: accepted_terms: { type: boolean, const: true } accepted_privacy: { type: boolean, const: true } acknowledged_original_file_responsibility: { type: boolean, const: true } acknowledged_immediate_execution_and_irreversibility: { type: boolean, const: true } authors: type: array minItems: 1 maxItems: 10 items: { $ref: "#/components/schemas/Author" } items: type: array minItems: 1 maxItems: 25 items: { $ref: "#/components/schemas/BulkItem" } BulkJobAccepted: type: object properties: success: { type: boolean, const: true } queued: { type: boolean, const: true } job_id: { type: string, format: uuid } external_job_id: { type: [string, "null"] } total_items: { type: integer } status: { type: string, const: queued } BulkJobIdempotent: type: object properties: success: { type: boolean, const: true } idempotent: { type: boolean, const: true } job_id: { type: string, format: uuid } external_job_id: { type: string } status: { $ref: "#/components/schemas/JobStatus" } total_items: { type: integer } completed_items: { type: integer } failed_items: { type: integer } JobStatus: type: string enum: [queued, processing, completed, partial, failed] description: "`partial`: parte dos itens falhou. Itens `duplicate` não contam como falha." ItemStatus: type: string enum: [queued, processing, completed, failed, duplicate] description: "`completed`: registro criado e crédito consumido. `duplicate`: o arquivo já tinha registro; nenhum crédito consumido." BulkJobDetails: type: object properties: integration_id: { type: string, format: uuid } job: type: object properties: id: { type: string, format: uuid } name: { type: string } status: { $ref: "#/components/schemas/JobStatus" } total_items: { type: integer } queued_items: { type: integer } processing_items: { type: integer } completed_items: { type: integer } failed_items: { type: integer } created_registrations: { type: integer } error_message: { type: [string, "null"] } external_job_id: { type: [string, "null"] } created_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } started_at: { type: [string, "null"], format: date-time } completed_at: { type: [string, "null"], format: date-time } callback_attempts: { type: [integer, "null"], description: "Tentativas de webhook deste lote." } last_callback_sent_at: { type: [string, "null"], format: date-time } callback_last_status: { type: [integer, "null"], description: "Último status HTTP recebido do seu webhook." } callback_last_error: { type: [string, "null"] } items: type: array items: type: object properties: id: { type: string, format: uuid } position: { type: integer } title: { type: string } file_name: { type: string } file_hash: { $ref: "#/components/schemas/Sha256Hex" } file_size: { type: integer } status: { $ref: "#/components/schemas/ItemStatus" } error_message: { type: [string, "null"] } registration_id: { type: [string, "null"], format: uuid } created_at: { type: string, format: date-time } started_at: { type: [string, "null"], format: date-time } completed_at: { type: [string, "null"], format: date-time } RegistrationStatus: type: string enum: [pendente, processando, concluido] description: "`processando`: crédito consumido, emissão em andamento. `concluido`: certificado disponível." AnchorId: type: [string, "null"] description: | Identificador interno da âncora. Trate como opaco, com uma exceção: quando começa com `bitcoin-h:`, o número depois dos dois-pontos é a altura do bloco Bitcoin que confirma a âncora. Qualquer outro valor significa âncora ainda não confirmada. example: "bitcoin-h:915000" Registration: type: object properties: id: { type: string, format: uuid } title: { type: [string, "null"] } description: { type: [string, "null"] } authors: { type: [string, "null"], description: "Nomes da autoria declarada, em texto." } work_type: { $ref: "#/components/schemas/WorkType" } status: { $ref: "#/components/schemas/RegistrationStatus" } payment_status: { type: [string, "null"], description: "Informativo. `paid` quando o crédito foi consumido." } product_type: { type: [string, "null"], example: registro } processing_step: { type: [string, "null"], description: "Etapa interna, informativa. Não dependa dos valores." } file_hash: { $ref: "#/components/schemas/Sha256Hex" } blockchain_tx_id: { $ref: "#/components/schemas/AnchorId" } ficha_hash: type: [string, "null"] description: Impressão digital da ficha de cadastro (documento com a autoria declarada), ancorada junto. ficha_blockchain_tx_id: { $ref: "#/components/schemas/AnchorId" } created_at: { type: [string, "null"], format: date-time } paid_at: { type: [string, "null"], format: date-time } completed_at: { type: [string, "null"], format: date-time } bulk_job_id: { type: [string, "null"], format: uuid } ArtifactLink: type: object properties: available: { type: boolean } endpoint: { type: string, format: uri, description: "URL absoluta de `/api-get-registration-artifact` para este documento." } RegistrationResponse: type: object properties: integration_id: { type: string, format: uuid } registration: { $ref: "#/components/schemas/Registration" } artifacts: type: object description: Documentos do registro. Outras entradas podem aparecer; ignore as que não conhecer. properties: certificate: { $ref: "#/components/schemas/ArtifactLink" } ficha: { $ref: "#/components/schemas/ArtifactLink" } additionalProperties: { $ref: "#/components/schemas/ArtifactLink" } RegistrationList: type: object properties: integration_id: { type: string, format: uuid } registrations: type: array items: type: object properties: id: { type: string, format: uuid } title: { type: [string, "null"] } work_type: { $ref: "#/components/schemas/WorkType" } status: { $ref: "#/components/schemas/RegistrationStatus" } payment_status: { type: [string, "null"] } product_type: { type: [string, "null"] } file_hash: { $ref: "#/components/schemas/Sha256Hex" } created_at: { type: string, format: date-time } paid_at: { type: [string, "null"], format: date-time } completed_at: { type: [string, "null"], format: date-time } bulk_job_id: { type: [string, "null"], format: uuid } next_before: type: [string, "null"] format: date-time description: Cursor da próxima página; nulo quando não há mais. ArtifactResponse: type: object properties: integration_id: { type: string, format: uuid } registration_id: { type: string, format: uuid } artifact: { type: string, enum: [certificate, ficha] } file_name: { type: string, example: certificado-1A2B3C4D.pdf } content_type: { type: string, example: application/pdf } url: { type: string, format: uri, description: "URL assinada. Baixe logo; expira." } expires_in_seconds: { type: integer, example: 120 } VerifyResponse: type: object required: [found, verification_url] properties: integration_id: { type: string, format: uuid } found: { type: boolean } matched: type: string enum: [file, ficha] description: "`file`: é o arquivo registrado. `ficha`: é a ficha de cadastro de um registro." registered_at: { type: [string, "null"], format: date-time } title: { type: [string, "null"] } authors_declared: { type: [string, "null"], description: "Autoria declarada (não verificada)." } work_type: { type: [string, "null"] } anchor: type: object properties: confirmed: { type: boolean } bitcoin_block_height: { type: [integer, "null"] } bitcoin_block_hash: { type: [string, "null"] } bitcoin_block_time: { type: [string, "null"], format: date-time } verification_url: { type: string, format: uri, description: "Página pública de verificação." } HealthCheckResponse: type: object properties: ok: { type: boolean, const: true } safe_test: { type: boolean, const: true } side_effects: { type: boolean, const: false } server_time: { type: string, format: date-time } integration: type: object properties: id: { type: string, format: uuid } name: { type: string } status: { type: string, enum: [active] } key_prefix: { type: string, description: "Primeiros 16 caracteres do token, para identificar qual está em uso." } scopes: type: array items: { $ref: "#/components/schemas/Scope" } rate_limit_per_minute: { type: integer } webhook_configured: { type: boolean } last_used_at: { type: [string, "null"], format: date-time } credits: type: object properties: available: { type: integer, description: "Créditos disponíveis na conta titular." } checks: type: object properties: authentication: { type: string, const: ok } scope: { type: string, const: "health:read" } webhook: { type: string, enum: [configured, not_configured] } next_recommended_calls: type: array items: { type: string } Scope: type: string enum: ["health:read", "bulk:create", "bulk:read", "registration:read", "artifact:read"] WebhookPayload: type: object properties: event: { type: string, example: bulk_job.completed } integration_id: { type: string, format: uuid } external_job_id: { type: [string, "null"] } delivered_at: { type: string, format: date-time } delivery_id: { type: string, format: uuid } job: type: object properties: id: { type: string, format: uuid } name: { type: string } status: { $ref: "#/components/schemas/JobStatus" } total_items: { type: integer } completed_items: { type: integer } failed_items: { type: integer } created_registrations: { type: integer } items: type: array items: type: object properties: id: { type: string, format: uuid } position: { type: integer } title: { type: string } status: { $ref: "#/components/schemas/ItemStatus" } error_message: { type: [string, "null"] } registration_id: { type: [string, "null"], format: uuid } file_hash: { type: [string, "null"] } completed_at: { type: [string, "null"], format: date-time } ```