# Atlas - API de envio

> Como entregar uma raspagem **por programa**: sem abrir o painel, sem navegador
> aberto, sem ninguem digitando senha. E o mesmo caminho que a tela de envio
> percorre — as mesmas rotas, a mesma fila, a mesma validacao.

Contrato v2.0: o Atlas guarda **pessoas e os rostos delas**. Cada
entrega pertence a **uma** rede social, declarada no manifesto.

Este documento e o **transporte**: como os bytes chegam. O **formato** do dump
esta resumido na secao seguinte, o bastante para montar uma entrega valida sem
sair daqui, e detalhado campo a campo na especificacao — que e um documento
publico e baixavel:

```bash
curl -sS https://atlas.nextwavetech.com.br/api/docs/export.md   > atlas-especificacao.md   # o contrato inteiro
curl -sS https://atlas.nextwavetech.com.br/api/docs/api.md      > atlas-api-de-envio.md    # este documento
curl -sS https://atlas.nextwavetech.com.br/api/docs/migracao.md > atlas-migracao.md        # converter projeto existente
```

Nenhuma das tres pede chave: sao a documentacao publica de entrega. Se voce
chegou aqui com so este arquivo em maos — porque alguem o baixou e repassou —
e o `export.md` acima que esta faltando.

---

## Onde a API fica

```
https://atlas.nextwavetech.com.br/api
```

**Todo caminho neste documento e relativo a esse endereco.** Quando se le
`POST /imports/uploads`, a URL inteira e:

```
https://atlas.nextwavetech.com.br/api/imports/uploads
```

Ha tres portas, e todas penduram neste mesmo endereco:

| prefixo | para que serve |
| --- | --- |
| *(sem prefixo)* | entrega passo a passo — `/imports`, `/media`. E o grosso deste guia. |
| `/v2` | **entrega rapida**, para milhoes de registros. Ver a secao no fim. |
| `/v1` | **consulta**: ler o acervo ja aprovado. Chave de escopo diferente. |

O numero em `/v2` e do TRANSPORTE, nao do contrato: o formato do dump continua
sendo o 2.0 nas tres. Versionar a rota e nao o conteudo e o que permite as
portas conviverem sem duas definicoes de entrega valida.

### As duas variaveis dos exemplos

Todos os `curl` deste guia usam `$ATLAS` e `$CHAVE`. Defina as duas uma vez e
o resto do documento vira copiar e colar:

```bash
export ATLAS="https://atlas.nextwavetech.com.br/api"
export CHAVE="atlas_..."    # a chave criada no painel, ver a secao seguinte
```

Um teste que responde em um segundo, antes de escrever qualquer integracao:

```bash
curl -sS "$ATLAS/health"
```

```json
{ "ok": true, "contractVersion": "2.0", "database": { "ok": true } }
```

Se isso nao responder, nada abaixo vai responder — e o problema e de rede ou de
endereco, nao da sua chave. `/health` e a unica rota que **nao** pede chave,
exatamente para servir de teste de encanamento.

---

## O dump, em uma pagina

O suficiente para montar uma entrega valida. A especificacao completa
(`/docs/export.md`) tem o campo a campo, as regras de deduplicacao e os erros.

### O envelope

Um **`.zip`** com exatamente dois arquivos **na raiz**:

```
entrega.zip
├── manifest.json
└── profiles.ndjson
```

`profiles.ndjson` e **NDJSON**: um objeto JSON completo **por linha**.
Sem array externo, sem virgula no fim da linha, sem identacao. UTF-8, `\n` ou
`\r\n`. Nao e um JSON unico com uma lista dentro — essa e a confusao mais
comum, e o validador recusa na primeira linha.

As **fotos nao vao no zip**. Elas sobem numa pasta separada, pelos passos 4-9
deste guia, e o que liga uma coisa a outra e uma regra so: `photos[].path` de
cada pessoa e o caminho da foto **dentro** da pasta enviada.

### manifest.json

```json
{
  "contractVersion": "2.0",
  "project": "instagram-perfis-br",
  "origin": "instagram",
  "profileCount": 12500,
  "generatedAt": "2026-08-12T03:00:00Z"
}
```

Os cinco acima sao **obrigatorios**. `project` e o slug da entrega (e a pasta
no bucket); `origin` e a rede social, que precisa **ja estar cadastrada** no
painel; `profileCount` e conferido contra o numero de linhas. Opcionais:
`generatedBy`, `toolVersion`, `notes`.

### Uma linha de profiles.ndjson

Seis campos obrigatorios — `externalId`, `username`, `profileUrl`,
`collectedAt`, `photos` (ao menos uma) e `raw` (o payload original da rede,
que nao pode ser objeto vazio):

```json
{"externalId":"17841400000000000","username":"joao.silva","profileUrl":"https://www.instagram.com/joao.silva/","collectedAt":"2026-08-12T02:41:00Z","photos":[{"path":"joao.silva/avatar.jpg","mimeType":"image/jpeg","size":48213,"role":"AVATAR","face":"FACE"}],"raw":{"id":"17841400000000000","username":"joao.silva"},"fullName":"Joao Silva","bio":"Fotografo","location":{"country":"BR","state":"SP","city":"Sao Paulo"},"counts":{"followers":15214,"following":1036,"posts":532},"verified":false,"tags":["fotografia"]}
```

Opcionais, **padronizados** — quem tem o dado entrega com este nome:
`fullName`, `bio`, `location` (`country`/`state`/`city`), `language`,
`verified`, `private`, `counts` (`followers`/`following`/`posts`),
`links`, `joinedAt`, `tags`, `metadata`.

Qualquer outra chave na raiz da linha vira **aviso**, nao erro: o dado passa,
mas nao vira coluna. O lugar do que nao tem campo proprio e `raw` ou
`metadata`.

### Uma foto

`path`, `mimeType` e `size` sao obrigatorios. Opcionais: `role`
(`AVATAR`, `COVER`, `POST`, `GALLERY`, `OTHER` — ausente vale `OTHER`),
`face` (`FACE`, `NOT_FACE`, `UNCERTAIN` — ausente vale
`UNCERTAIN`), `width`, `height`, `hash` (SHA-256), `sourceUrl`,
`capturedAt`. Teto de 200 fotos por perfil.

Sobre `face`: so declare `FACE` ou `NOT_FACE` com o que der para sustentar.
Deixar ausente nao e omissao — e o caminho normal, e o detector do Atlas
responde depois. Um palpite gravado como certeza vira dado que ninguem
consegue distinguir de uma conferencia de verdade.

---

## Antes de tudo: a chave

1. Entre no painel e abra **Chaves de API**.
2. Crie uma chave marcando o escopo **Envio**.
3. Copie o valor. Ele aparece **uma unica vez** — perdeu, revogue e crie outra.

A chave e **sua**: ela carrega a sua pasta no bucket, e tudo que subir por ela
vai para `atlas-scraping/<sua-pasta>/<slug-do-projeto>`. Nenhuma chave escreve na pasta de outra conta.

Mande a chave em **todas** as chamadas, num dos dois cabecalhos:

```
x-api-key: atlas_...
```

ou, se o seu cliente HTTP so souber o padrao:

```
Authorization: Bearer atlas_...
```

Escopos disponiveis: `read`, `upload`. Uma chave de
consulta (`read`) **nao** envia: os riscos sao diferentes e os poderes tambem.

Duas coisas continuam exigindo alguem no painel, e nao ha chave que as faca:
**aprovar** uma importacao e **apagar** arquivos do bucket.

---

## O caminho inteiro, de cima

```
1. POST   /imports/uploads                  declara o dump (.zip)
2. PUT    /imports/uploads/:id/parts/:n     manda as partes  (repete)
3. POST   /imports/uploads/:id/complete     cria a importacao
   -> a fila valida o dump, confere a REDE do manifesto e para em
      AWAITING_FILES esperando as fotos

4. POST   /media/batches                    abre o lote de fotos
5. POST   /media/batches/:id/files          declara mais blocos (repete)
6. POST   /media/batches/:id/presign        pede URLs assinadas (repete)
7. PUT    <url assinada>                    os bytes vao DIRETO para o bucket
8. POST   /media/batches/:id/uploaded       avisa o que ja subiu (repete)
9. POST   /media/batches/:id/close          conferencia final

10. PUT   /imports/:id/metadata             titulo, descricao, categoria
    (a rede NAO entra aqui: veio do manifesto e ja foi conferida)
11. POST  /imports/:id/finalize             grava no banco
    -> um admin aprova no painel e a raspagem entra no acervo
```

Nao ha caminho curto: todo perfil tem ao menos uma foto, entao os passos 4-9
sempre acontecem.

**Antes do passo 1**, a rede social precisa estar cadastrada no painel. Um
`origin` desconhecido no manifesto reprova a entrega inteira na validacao — de
proposito: aceitar qualquer slug faria um typo virar uma rede fantasma.

---

## Passo 1-3: o dump, em partes

O dump passa pela API (e ela quem o grava e enfileira a validacao), e vai **em
partes**: cada parte e uma requisicao independente, reenviavel sozinha, e varias
podem estar no ar ao mesmo tempo. Um POST unico de 8 GB que cai aos 90% custa
8 GB; aqui custa uma parte.

### Declarar

```bash
curl -X POST "$ATLAS/imports/uploads" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{"projectSlug":"instagram-perfis-br","filename":"entrega.zip","size":8123456789}'
```

Resposta:

```json
{
  "id": "clx...",
  "status": "OPEN",
  "partSize": 67108864,
  "partCount": 122,
  "receivedParts": [],
  "missingParts": [1, 2, 3, "..."],
  "expiresAt": "2026-01-04T12:00:00.000Z"
}
```

`size` e obrigatorio, e nao apurado no fim: e ele que define o tamanho **exato**
de cada parte, inclusive o da ultima. Com isso o servidor recusa uma parte
truncada na hora, em vez de descobrir o dump corrompido horas depois.

**Chamar de novo com os mesmos dados devolve o MESMO envio**, com a lista do que
ja chegou. E assim que se retoma sem guardar o id em lugar nenhum.

### Mandar as partes

Corpo cru, sem multipart, sem JSON. As partes sao blocos de `partSize` bytes do
arquivo, na ordem; so a ultima e menor.

```bash
# parte 1: os primeiros partSize bytes do arquivo
dd if=entrega.zip bs=67108864 skip=0 count=1 2>/dev/null | \
curl -X PUT "$ATLAS/imports/uploads/$ID/parts/1" \
  -H "x-api-key: $CHAVE" \
  -H "content-type: application/octet-stream" \
  --data-binary @-
```

Opcional e recomendado: mande o SHA-256 da parte em `x-atlas-part-sha256`. O
servidor confere e recusa bytes que se corromperam no caminho — sem isso, um
dump quebrado so aparece na validacao.

Mande ate ~4 partes em paralelo. Ordem nao importa.

### Retomar

```bash
curl "$ATLAS/imports/uploads/$ID" -H "x-api-key: $CHAVE"
```

`missingParts` e o roteiro: mande **so** o que esta nela. As partes ficam
guardadas por alguns dias; depois disso o envio expira e o disco e liberado.

### Fechar

```bash
curl -X POST "$ATLAS/imports/uploads/$ID/complete" -H "x-api-key: $CHAVE"
```

Junta as partes, confere o tamanho e devolve a **importacao** criada. Dali em
diante e a esteira de sempre: acompanhe por `GET /imports/:id`, ou pelo stream
de progresso em `GET /imports/:id/stream` (SSE, sem autenticacao — o id e a
credencial).

Se o dump referencia arquivos, a importacao para em `AWAITING_FILES`. E a deixa
para o proximo passo.

---

## Passo 4-9: os arquivos

Aqui os bytes **nao passam pela API**. Voce declara o que vai enviar, o Atlas
devolve URLs assinadas presas a `atlas-scraping/<sua-pasta>/<slug-do-projeto>`, e o seu programa envia direto para
o bucket. E o que permite uma pasta de 30 GB subir sem sobrecarregar a VPS — e e
exatamente o que o navegador faz na tela de envio.

### O que o dump espera

```bash
curl "$ATLAS/imports/$IMPORT_ID/expected-files?offset=0" -H "x-api-key: $CHAVE"
```

Devolve os caminhos de foto que o dump cita e quais ja estao no bucket. Nao ha o
que adivinhar: a lista sai da propria varredura do dump.

### Declarar em blocos

```bash
curl -X POST "$ATLAS/media/batches" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{
    "projectSlug": "instagram-perfis-br",
    "importId": "'$IMPORT_ID'",
    "files": [
      {"path": "joao.silva/avatar.jpg", "contentType": "image/jpeg", "size": 184320, "face": "FACE"}
    ]
  }'
```

Ate **5.000 arquivos por chamada**; os
blocos seguintes vao em `POST /media/batches/:id/files`. O corpo de uma
requisicao nunca cresce com o tamanho da entrega — e o que permite centenas de
milhares de arquivos e retomar de onde parou.

Passe `importId`: e ele que amarra o lote a importacao que esta esperando.

### Nao reenviar o que ja subiu

```bash
curl -X POST "$ATLAS/media/batches/$LOTE/reconcile" -H "x-api-key: $CHAVE"
```

Varre o bucket e tira da fila o que ja esta la. Rode **antes** de comecar a
enviar: e o que impede reenviar horas de fotos apos uma queda.

### Assinar e enviar, em janelas

```bash
curl -X POST "$ATLAS/media/batches/$LOTE/presign" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{"limit": 200}'
```

Resposta traz `items`, `nextCursor` e `remaining`. Para cada item:

- **`uploadUrl`** — um `PUT` com o arquivo inteiro.
- **`multipart`** — arquivos acima de 64 MB vem
  com uma URL por parte. Envie cada parte, guarde o `ETag` de cada uma e feche
  com `POST /media/assets/:id/complete`.

Passe o `nextCursor` na chamada seguinte. Sem ele a janela volta ao inicio e
voce reassina sempre os mesmos arquivos.

### Reportar durante o envio

```bash
curl -X POST "$ATLAS/media/batches/$LOTE/uploaded" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{"items":[{"assetId":"clx...","size":184320,"etag":"\"abc123\""}]}'
```

Ate 1.000 por chamada. **Reporte durante o
envio, nao so no fim**: e o que faz a retomada pular o que ja subiu. Se o
processo morrer aos 80%, so os 20% restantes voltam para a fila.

### Fechar

```bash
curl -X POST "$ATLAS/media/batches/$LOTE/close" -H "x-api-key: $CHAVE"
```

O lote vai para `VERIFYING` e a fila confere objeto por objeto no bucket. O
resultado e `COMPLETED` ou `PARTIAL` — neste caso, peca as URLs de novo com
`{"onlyMissing": true}` e mande so o que faltou.

---

## Passo 10-11: metadados e gravacao

```bash
curl -X PUT "$ATLAS/imports/$IMPORT_ID/metadata" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{"title":"Perfis do Instagram - BR","description":"...","categorySlug":"redes-sociais"}'

curl -X POST "$ATLAS/imports/$IMPORT_ID/finalize" -H "x-api-key: $CHAVE"
```

`finalize` confere os arquivos no bucket e grava no banco. Depois disso a
importacao fica **aguardando aprovacao**: um admin decide no painel, e so entao
os registros aparecem em `/v1`.

---

## Ensaio antes da entrega

Antes de mandar 30 GB, mande uma amostra: `"isTest": true` em
`POST /imports/uploads` roda a **mesma** validacao e nunca grava no banco. Um
dump de algumas centenas de perfis responde em segundos e mostra exatamente os
mesmos erros que a entrega inteira mostraria — depois de horas de upload.

E onde os erros caros aparecem barato: rede nao cadastrada, `username`
repetido, perfil sem foto, `path` que nao bate com a pasta.

---

## Erros e novas tentativas

| Codigo | O que significa | O que fazer |
| --- | --- | --- |
| 400 | O corpo ou o tamanho da parte esta errado | Ler a mensagem: ela diz o valor esperado. Nao insista. |
| 401 | Chave ausente, invalida, revogada ou expirada | Criar outra no painel. |
| 403 | A chave nao tem o escopo, ou o recurso e de outra conta | Criar uma chave com escopo `upload`. |
| 409 | O projeto ja pertence a outra rede social | Um projeto tem uma rede para sempre. Use outro `projectSlug`. |
| 404 | Nao existe **ou** nao e seu | Conferir o id. |
| 409 | Estado incompativel (lote fechado, partes faltando) | A mensagem diz o que falta. |
| 413 | Passou do limite de tamanho | Dividir a entrega. |
| 429, 5xx | Sobrecarga ou falha temporaria | Tentar de novo com espera crescente. |

Tudo aqui e **idempotente por desenho**: redeclarar um arquivo ja declarado nao
duplica nada, reenviar uma parte substitui a anterior, e reabrir um envio com os
mesmos dados devolve o mesmo id. Repetir e sempre seguro — o que nao e seguro e
supor que uma chamada que falhou nao chegou.

---

## Entrega rapida (`/v2`): milhoes de registros

O fluxo acima foi desenhado para um navegador numa conexao que cai, e paga por
isso em ida e volta. Numa entrega de **um milhao de perfis** isso vira ~6.000
requisicoes so de coordenacao — e e o que faz a entrega levar dias.

O `/v2` e o mesmo acervo com menos conversa. Mesma validacao, mesma fila,
mesma aprovacao: o que muda e o transporte. **O contrato continua o 2.0** — o
numero da rota e do transporte, nao do formato.

| | fluxo classico | `/v2` |
| --- | --- | --- |
| NDJSON de 570 MB | 122 requisicoes | **1** |
| declarar fotos | ~200 | 0 |
| assinar URLs | ~5.000 | so o que faltar |
| reportar enviadas | ~1.000 | 0 |
| descobrir o que chegou | — | 1 listagem |

### 1. Abrir

```bash
curl -X POST "$ATLAS/v2/deliveries" \
  -H "x-api-key: $CHAVE" -H "content-type: application/json" \
  -d '{"project":"mastodon-perfis-br","origin":"mastodon","profileCount":8215}'
```

A resposta traz o `id` da entrega e o **prefixo do bucket** onde as fotos vao.
A rede e conferida aqui, e nao na validacao: descobrir que ela nao existe
depois de subir um milhao de fotos e o erro mais caro desta esteira.

### 2. As fotos

Mesmas rotas de `/media` do fluxo classico, com o `id` da entrega como lote.
O que muda e a orientacao, e ela vale mais que qualquer rota nova:

> **Mande com 64 conexoes em paralelo.**

Medido nesta instalacao, contra este bucket: **2,9 fotos/s** com uma conexao,
**185/s** com 64 — quase linear, e a 64 ainda longe do limite de banda. E
latencia por objeto, nao tamanho. Um milhao de fotos leva **4 dias** em serie e
**~1,5 h** em paralelo. Nenhuma API muda isso; so o paralelismo de quem envia.

### 3. Os perfis, numa requisicao

```bash
gzip -c perfis.ndjson | \
curl -X PUT "$ATLAS/v2/deliveries/$ID/profiles?origin=mastodon&profileCount=8215" \
  -H "x-api-key: $CHAVE" \
  -H "content-type: application/x-ndjson" \
  -H "content-encoding: gzip" \
  --data-binary @-
```

O corpo e o `profiles.ndjson` **cru** — sem zip, sem multipart, sem
particionar. Ele e gravado enquanto chega, entao o tamanho nao depende de
memoria. `content-encoding: gzip` e descompactado no caminho e vale muito:
NDJSON de perfil comprime **~17x** (medido).

Nao ha `manifest.json` aqui: o servidor o monta com o que voce declarou. Por
isso `?origin=` e obrigatorio — a rede vale para a entrega inteira.

`profileCount` e **opcional, mas mande**. E ele que detecta upload truncado: a
validacao confere o numero declarado contra as linhas que chegaram. Sem ele o
servidor conta sozinho, a entrega passa, e essa checagem deixa de valer — a
resposta avisa com `"contagemConferida": false`.

### 4. Descobrir as fotos

```bash
curl -X POST "$ATLAS/v2/deliveries/$ID/scan" -H "x-api-key: $CHAVE"
```

Lista o prefixo no bucket e casa com os `photos[].path` declarados. Substitui
declarar, assinar e reportar — as tres coisas que faziam o cliente conversar
milhares de vezes com a API sobre arquivos que ele ja tinha. Uma listagem
devolve mil chaves por chamada: um milhao sai em ~4 minutos.

Se as fotos **ja estao no bucket** de uma entrega anterior, este passo sozinho
resolve: nada precisa subir de novo.

### 5. Acompanhar

```bash
curl "$ATLAS/v2/deliveries/$ID" -H "x-api-key: $CHAVE"
```

Devolve o estado do lote e da importacao juntos. Dali em diante e a esteira de
sempre: metadados, `finalize` e a aprovacao de um admin.

### O que o `/v2` NAO acelera

A validacao roda a **19.000 linhas/s** (um milhao em um minuto) e a gravacao ja
vai em lote — nenhuma das duas era gargalo, e por isso nenhuma foi reescrita.
Uma segunda esteira seria uma segunda definicao de "entrega valida", e as duas
divergiriam na primeira correcao feita so de um lado.

---

## A outra porta: ler o acervo (`/v1`)

Entregar e ler sao APIs diferentes, com chaves diferentes. Tudo acima usa uma
chave de escopo `upload`; para **ler**, crie uma de escopo `read` — ela nao
envia nada, e e a que se entrega a um consumidor.

O prefixo `/v1` existe so aqui, e e o unico numero de versao do Atlas. Ele
promete formato de RESPOSTA: os campos abaixo nao mudam de significado sem virar
`/v2`.

```
GET https://atlas.nextwavetech.com.br/api/v1/profiles          as pessoas do acervo
GET https://atlas.nextwavetech.com.br/api/v1/profiles/:id      uma pessoa, com tudo
GET https://atlas.nextwavetech.com.br/api/v1/datasets          as raspagens publicadas
GET https://atlas.nextwavetech.com.br/api/v1/origins           as redes sociais
GET https://atlas.nextwavetech.com.br/api/v1/tags              as tags em uso
GET https://atlas.nextwavetech.com.br/api/v1/files/:id         redireciona para a foto (URL assinada, temporaria)
```

```bash
curl -sS "$ATLAS/v1/profiles?limit=20&includePhotos=true" \
  -H "x-api-key: $CHAVE_DE_LEITURA"
```

So aparece aqui o que um admin **aprovou**. Uma entrega gravada e ainda pendente
nao existe para esta porta — nem para o proprio dono da chave. E a mesma
fronteira que o painel aplica, e e por isso que `finalize` nao e o fim: sem a
aprovacao, o consumidor continua sem ver.

O bucket e privado, entao `/v1/files/:id` nao devolve o arquivo: devolve um
redirecionamento (302) para uma URL assinada que expira. Siga o redirect (o
`curl -L` faz isso) e nao guarde o endereco final — ele morre em uma hora.

---

## Ja existe um programa pronto

Se voce nao precisa de integracao propria, o Atlas ja entrega dois clientes que
falam este protocolo: o **aplicativo do Windows** e o comando de terminal
`atlas-upload`. Os dois retomam sozinhos e nao exigem navegador aberto. Veja a
pagina de download do painel.