<!-- Fonte: https://registrarobra.com.br/desenvolvedores — gerado no build a partir do mesmo arquivo da página. -->

# 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<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 {#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.<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ç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=<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.

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