Desenvolvedores

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.

Arquivos para ferramentas: OpenAPI 3.1 (YAML), OpenAPI (JSON), esta página em markdown e tudo num arquivo, para assistentes de IA.

Regras que importam

  • 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

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).

MétodoCaminhoEscopoPara quê
GET/api-health-checkhealth:readTestar a conexão e ver o saldo de créditos
POST/api-submit-bulk-registration-jobbulk:createRegistrar de 1 a 25 obras (consome crédito)
GET/api-get-bulk-jobbulk:readSituação de um lote e de cada item
GET/api-get-registrationregistration:readSituação de um registro
GET/api-list-registrationsregistration:readRegistros mais recentes, com paginação
GET/api-get-registration-artifactartifact:readLink de download do certificado ou da ficha
GET/api-verify-hashregistration:readA verificação pública, por impressão digital

Fluxo em três passos

  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). 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 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

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:

EscopoPermite
health:readTestar a conexão e ler o saldo de créditos
bulk:createCriar lotes (consome crédito)
bulk:readConsultar lotes criados por esta integração
registration:readConsultar e listar registros; verificar impressão digital
artifact:readBaixar certificado e ficha

Limites

LimiteValor
Requisições por integraçãoDefinido no contrato: 30 por minuto por padrão, até 600. Acima disso, 429 com Retry-After
Obras por lote1 a 25
Corpo do lote256 KB
Arquivoaté 500 GB; arquivo vazio e arquivo comprimido (.zip, .rar, .7z, .iso...) são recusados
Título160 caracteres
Descrição600 caracteres
Nome do arquivo255 caracteres
Autores por obra1 a 10
external_job_idaté 128 caracteres: letras, números e . _ : -
Link de download120 segundos
Listagematé 50 registros por página

Idempotência

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

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:

TypeScript
const BASE = "https://auth.registrarobra.com.br/functions/v1";

export async function api<T>(caminho: string, init: RequestInit = {}): Promise<T> {
  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

Testa token, escopo e webhook, sem efeito colateral, e mostra o saldo de créditos da conta titular. Escopo health:read.

Terminal
curl -s https://auth.registrarobra.com.br/functions/v1/api-health-check \
  -H "Authorization: Bearer $REGISTRAROBRA_TOKEN"
TypeScript
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

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).
Terminal
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
    }]
  }'
TypeScript
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

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.

Terminal
curl -s "https://auth.registrarobra.com.br/functions/v1/api-get-bulk-job?external_job_id=pedido-2026-0042" \
  -H "Authorization: Bearer $REGISTRAROBRA_TOKEN"
TypeScript
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.

GET/api-get-registration

Um registro da conta (ou do workspace) da integração. Escopo registration:read.

Terminal
curl -s "https://auth.registrarobra.com.br/functions/v1/api-get-registration?registration_id=3c9d8e7f-6a5b-4c4d-9e3f-2a1b0c9d8e7f" \
  -H "Authorization: Bearer $REGISTRAROBRA_TOKEN"
TypeScript
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

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.

Terminal
curl -s "https://auth.registrarobra.com.br/functions/v1/api-list-registrations?limit=20" \
  -H "Authorization: Bearer $REGISTRAROBRA_TOKEN"
TypeScript
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

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.

Terminal
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
TypeScript
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

A mesma verificação pública da página 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.

Terminal
curl -s "https://auth.registrarobra.com.br/functions/v1/api-verify-hash?file_hash=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" \
  -H "Authorization: Bearer $REGISTRAROBRA_TOKEN"
TypeScript
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

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

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.<status> — 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çalhoConteúdo
Content-Typeapplication/json
User-AgentRegistrarObra-Webhooks/1.0
x-registrarobra-eventO evento, como bulk_job.completed
x-registrarobra-deliveryId desta tentativa (muda a cada tentativa)
x-registrarobra-timestampMomento do envio, em segundos Unix
x-registrarobra-signaturet=<segundos>,v1=<HMAC-SHA256 em hex>

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 <t>.<corpo cru>. Use o corpo exatamente como chegou, antes de interpretar o JSON, e recuse t a mais de 5 minutos do seu relógio.

TypeScript
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:

TypeScript
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

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" }
    ]
  }
}
StatusQuando
400Parâmetro ausente ou malformado; corpo inválido, com details.issues (field e message para cada problema)
401Token ausente, inválido ou revogado
403Escopo insuficiente, acesso à API não liberado para o titular, ou titular sem permissão no workspace
404Lote ou registro não encontrado para esta integração
405Método HTTP errado
409Impressão digital já registrada (details.file_hashes); créditos insuficientes (details.required_credits, details.available_credits); documento pedido antes da conclusão
410Endpoint descontinuado
413Corpo maior que o limite do endpoint
429Limite por minuto atingido; espere os segundos de Retry-After
500Erro inesperado; a resposta traz request_id (e o cabeçalho x-request-id) para o suporte

Situações

ObjetoValorSignificado
LotequeuedNa fila
LoteprocessingCriando os registros
LotecompletedTodos os itens resolvidos (registrados ou já registrados antes)
LotepartialParte dos itens falhou
LotefailedNenhum item foi registrado
ItemcompletedRegistro criado e 1 crédito consumido
ItemduplicateO arquivo já tinha registro; nenhum crédito consumido
ItemfailedNão registrado; veja error_message
RegistropendenteCriado, aguardando confirmação (crédito ainda não consumido)
RegistroprocessandoCrédito consumido; emissão em andamento
RegistroconcluidoCertificado 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

  • 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), ou recebe créditos conforme o contrato, pela equipe.

Usando com IA

Se você integra com a ajuda de um assistente de IA, aponte-o para /llms-full.txt, que traz esta documentação e a especificação num arquivo só, e para a especificação OpenAPI. Um começo de conversa:

Texto
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

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 ou use a página 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

  • 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).