Carga em lote

Uma carga em lote agrupa várias escritas numa requisição só. Em vez de um POST por recurso, você manda um Bundle com uma entrada por escrita, e recebe de volta o resultado de cada uma.

Serve para dois casos:

  • volume — cadastrar dez pacientes sem fazer dez chamadas;
  • dependência — criar um paciente e, na mesma requisição, uma condição que aponta para ele, sem precisar do identificador que ainda não existe.

Não há atomicidade. Cada entrada é processada de forma independente: uma falha não interrompe as demais, e o que já foi gravado não é desfeito. Um lote de cinco entradas independentes com uma falha grava quatro.

O FHIR chama isso de batch. O tipo transaction, que no padrão significa “tudo ou nada”, é aceito por compatibilidade e processado exatamente como um batch — não confie na palavra.

A independência tem um limite: uma entrada que referencia outra falha junto com ela. Se o paciente não gravar, a condição que aponta para ele é recusada com Dependency <fullUrl> not processed successfully. É a contrapartida do caso de uso de dependência.

Como montar

CampoObrigatórioO que significa
resourceTypesimConstante Bundle
typesimbatch — é o valor a usar
entry[]simUma entrada por escrita
entry[].fullUrlna práticaUm identificador local da entrada, único no lote. Obrigatório para ser referenciado por outra entrada, e é o que identifica a entrada nas falhas
entry[].request.methodsimPOST é implementado
entry[].request.urlsimO tipo do recurso: Patient, Condition
entry[].request.ifNoneExistnãoBusca condicional para evitar duplicata
entry[].resourcesimO recurso, exatamente como no POST avulso dele
POST
/fhir/resources/Bundle
1curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Bundle \
2 -H "x-api-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "resourceType": "Bundle",
6 "type": "batch",
7 "entry": [
8 {
9 "request": {
10 "method": "POST",
11 "url": "Patient",
12 "ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|01234567890"
13 },
14 "resource": {
15 "resourceType": "Patient",
16 "birthDate": "1988-03-14",
17 "gender": "female",
18 "identifier": [
19 {
20 "system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
21 "use": "usual",
22 "value": 1234567890
23 }
24 ],
25 "name": [
26 {
27 "text": "Ana Paula Ribeiro",
28 "use": "official"
29 }
30 ]
31 },
32 "fullUrl": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
33 },
34 {
35 "request": {
36 "method": "POST",
37 "url": "Patient",
38 "ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|98765432109"
39 },
40 "resource": {
41 "resourceType": "Patient",
42 "birthDate": "1975-11-02",
43 "gender": "male",
44 "identifier": [
45 {
46 "system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
47 "use": "usual",
48 "value": "98765432109"
49 }
50 ],
51 "name": [
52 {
53 "text": "Carlos Eduardo Lima",
54 "use": "official"
55 }
56 ]
57 },
58 "fullUrl": "urn:uuid:987f6543-ba09-87dc-5678-0987654321ab"
59 }
60 ]
61}'

POST funciona. PUT, GET, DELETE e PATCH não estão implementados — a entrada é recusada com Request method … not implemented. E os cabeçalhos condicionais ifMatch, ifNoneMatch e ifModifiedSince também recusam a entrada.

O único condicional suportado é o ifNoneExist.

Recomendamos no máximo 10 entradas por lote, e de preferência cinco. Não é um limite validado — a API aceita mais —, mas lotes grandes degradam o processamento, e, como não há rollback, um lote grande que falha no meio é mais difícil de reconciliar do que cinco chamadas avulsas.

Referências entre entradas do mesmo lote

É o que o lote resolve melhor. Uma entrada aponta para outra pelo fullUrl dela, e a Nilo grava na ordem certa, substituindo a referência pelo identificador do recurso que acabou de criar.

POST
/fhir/resources/Bundle
1curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Bundle \
2 -H "x-api-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "resourceType": "Bundle",
6 "type": "batch",
7 "entry": [
8 {
9 "request": {
10 "method": "POST",
11 "url": "Patient",
12 "ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|01234567890"
13 },
14 "resource": {
15 "resourceType": "Patient",
16 "birthDate": "1988-03-14",
17 "gender": "female",
18 "identifier": [
19 {
20 "system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
21 "use": "usual",
22 "value": 1234567890
23 }
24 ],
25 "name": [
26 {
27 "text": "Ana Paula Ribeiro",
28 "use": "official"
29 }
30 ]
31 },
32 "fullUrl": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
33 },
34 {
35 "request": {
36 "method": "POST",
37 "url": "Condition"
38 },
39 "resource": {
40 "resourceType": "Condition",
41 "code": {
42 "coding": [
43 {
44 "code": "E11",
45 "system": "http://hl7.org/fhir/sid/icd-10"
46 }
47 ]
48 },
49 "identifier": [
50 {
51 "system": "https://www.acmesaude.com.br/integracao/condicao/",
52 "use": "usual",
53 "value": "CD-8801"
54 }
55 ],
56 "subject": {
57 "reference": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
58 },
59 "verificationStatus": {
60 "coding": [
61 {
62 "code": "confirmed",
63 "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status"
64 }
65 ]
66 }
67 },
68 "fullUrl": "urn:uuid:5c9a1e77-30bd-4f18-a2c6-71e4b0532d89"
69 }
70 ]
71}'

O fullUrl é um identificador local do lote, não um endereço. Use uma urn:uuid: nova a cada envio — ela não precisa existir em lugar nenhum, e não é gravada.

Para referenciar um recurso que já existe fora do lote, use o identifier dele, como em qualquer escrita avulsa.

As entradas são processadas na ordem do array. O que a Nilo antecipa são só as dependências de uma entrada: se a entrada 2 referencia a 5, a 5 é gravada antes da 2.

Quando a ordem importa por outro motivo — desligar e religar um webhook, por exemplo —, é a ordem do array que vale.

Não monte um ciclo. A referenciando B e B referenciando A não tem solução, e a entrada falha com erro inesperado (500), não com uma recusa limpa.

Três erros comuns de referência interna, todos recusando a entrada:

  • Reference <fullUrl> not found in bundle entries — o fullUrl referenciado não existe no lote;
  • Reference <fullUrl> has not identifier — a entrada referenciada não tem identifier próprio, e sem ele não há o que substituir na referência;
  • Reference <system>|<value> not found — a referência por identifier aponta para um recurso que não existe fora do lote.

Evitar duplicata com ifNoneExist

ifNoneExist é uma busca. Se ela encontrar um recurso, a entrada não grava nada e devolve o que já existe:

1"request": {
2 "method": "POST",
3 "url": "Patient",
4 "ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|01234567890"
5}

identifier= é aceito no ifNoneExist. Qualquer outro parâmetro de busca recusa a entrada com ifNoneExist only allow the query param 'identifier'.

E encontrando mais de um recurso, a entrada também é recusada, com Query … returned more than one resource. Use um identificador que resolva para um recurso só.

ifNoneExist é diferente do identifier do recurso: o identifier decide entre criar e atualizar — veja Identificadores —; o ifNoneExist decide entre criar e não fazer nada. Numa integração que reenvia o mesmo lote, o identifier normalmente já resolve — o ifNoneExist é para quando você quer garantir que nada seja tocado.

A resposta

Response
1{
2 "resourceType": "Bundle",
3 "type": "batch-response",
4 "entry": [
5 {
6 "response": {
7 "status": "200 OK",
8 "location": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4"
9 }
10 },
11 {
12 "response": {
13 "status": "400 Bad Request",
14 "location": "urn:uuid:5c9a1e77-30bd-4f18-a2c6-71e4b0532d89",
15 "outcome": {
16 "issue": [
17 {
18 "code": "not-found",
19 "details": {
20 "text": "Patient does not exist"
21 },
22 "expression": [
23 "Condition.subject"
24 ],
25 "severity": "error"
26 }
27 ],
28 "resourceType": "OperationOutcome"
29 }
30 }
31 }
32 ],
33 "id": "30f8c1a5-27de-4b96-8071-4e5b93c0da26",
34 "meta": {
35 "lastUpdated": "2026-08-11T15:02:44.318000Z",
36 "versionId": "MTc4NjAyMTQ1MjkwNTAwMDM2Nw"
37 }
38}

O 200 da requisição não diz que as escritas deram certo. Ele diz que o lote foi processado. O resultado de cada entrada está em entry[].response.status, como texto: 200 OK, 400 Bad Request, 409 Conflict, 429 Too Many Requests ou 500.

Uma integração que só olha o status HTTP da requisição perde todas as falhas.

E repare que o 500 vem sem frase, ao contrário dos outros quatro: quem parseia “código + texto” quebra nele. Leia só os três primeiros caracteres.

Campo da respostaO que traz
typeConstante batch-response — mesmo quando você mandou transaction
entry[]Uma entrada por entrada enviada, na mesma ordem
entry[].response.statusO status daquela entrada
entry[].response.locationNo sucesso, o caminho do recurso gravado; na falha, o fullUrl que você enviou
entry[].response.outcomeSó nas falhas: o mesmo OperationOutcome da escrita avulsa

As entradas da resposta não trazem fullUrl. A correspondência com o que você enviou é pela posição no array — e, nas falhas, pelo location, que repete o seu fullUrl.

Quando o lote inteiro é recusado

Quatro coisas derrubam o lote antes de qualquer entrada:

  • type diferente de batch ou transaction;
  • payload que não é um Bundle válido pelo FHIR;
  • identificador Nilo de outro ambiente em qualquer ponto do payload;
  • uma entrada sem resource — que, em vez de falhar sozinha, derruba a requisição inteira com erro inesperado (500).

Nos três primeiros a resposta é 400 com um OperationOutcome, e nada foi gravado.

Quais recursos podem ir num lote

Qualquer recurso que tenha POST avulso nesta API. Um recurso somente leitura numa entrada é recusado — com uma mensagem limpa, ao contrário do POST direto:

Resource ClinicalImpression not supported

É a única situação em que a tentativa de escrever um recurso somente leitura dá um erro tratável. No POST avulso, ela falha com erro inesperado do servidor.

Nem toda recusa dentro de uma entrada é limpa. Uma validação que acontece antes de a escrita começar — como uma classe de atendimento não suportada — sai com status 500 e um texto de erro de linguagem, em vez do OperationOutcome do recurso. Confira o payload de cada entrada como se fosse enviá-la sozinha.

O que a integração não cobre

  • atomicidade — não há rollback, nem no transaction;
  • métodos além de POST;
  • os condicionais ifMatch, ifNoneMatch e ifModifiedSince;
  • fullUrl nas entradas da resposta;
  • qualquer critério de ifNoneExist que não seja identifier=.