Skip to navigation

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.methodsimSó POST é 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
curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Bundle \
-H "x-api-key: <apiKey>" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "POST",
"url": "Patient",
"ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|01234567890"
},
"resource": {
"resourceType": "Patient",
"birthDate": "1988-03-14",
"gender": "female",
"identifier": [
{
"system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
"use": "usual",
"value": 1234567890
}
],
"name": [
{
"text": "Ana Paula Ribeiro",
"use": "official"
}
]
},
"fullUrl": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
},
{
"request": {
"method": "POST",
"url": "Patient",
"ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|98765432109"
},
"resource": {
"resourceType": "Patient",
"birthDate": "1975-11-02",
"gender": "male",
"identifier": [
{
"system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
"use": "usual",
"value": "98765432109"
}
],
"name": [
{
"text": "Carlos Eduardo Lima",
"use": "official"
}
]
},
"fullUrl": "urn:uuid:987f6543-ba09-87dc-5678-0987654321ab"
}
]
}'

Só 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
curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Bundle \
-H "x-api-key: <apiKey>" \
-H "Content-Type: application/json" \
-d '{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "POST",
"url": "Patient",
"ifNoneExist": "identifier=https://servicos.receita.fazenda.gov.br/servicos/cpf/|01234567890"
},
"resource": {
"resourceType": "Patient",
"birthDate": "1988-03-14",
"gender": "female",
"identifier": [
{
"system": "https://servicos.receita.fazenda.gov.br/servicos/cpf/",
"use": "usual",
"value": 1234567890
}
],
"name": [
{
"text": "Ana Paula Ribeiro",
"use": "official"
}
]
},
"fullUrl": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
},
{
"request": {
"method": "POST",
"url": "Condition"
},
"resource": {
"resourceType": "Condition",
"code": {
"coding": [
{
"code": "E11",
"system": "http://hl7.org/fhir/sid/icd-10"
}
]
},
"identifier": [
{
"system": "https://www.acmesaude.com.br/integracao/condicao/",
"use": "usual",
"value": "CD-8801"
}
],
"subject": {
"reference": "urn:uuid:123e4567-e89b-12d3-a456-426614174000"
},
"verificationStatus": {
"coding": [
{
"code": "confirmed",
"system": "http://terminology.hl7.org/CodeSystem/condition-ver-status"
}
]
}
},
"fullUrl": "urn:uuid:5c9a1e77-30bd-4f18-a2c6-71e4b0532d89"
}
]
}'

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:

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

Só 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
{
"resourceType": "Bundle",
"type": "batch-response",
"entry": [
{
"response": {
"status": "200 OK",
"location": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4"
}
},
{
"response": {
"status": "400 Bad Request",
"location": "urn:uuid:5c9a1e77-30bd-4f18-a2c6-71e4b0532d89",
"outcome": {
"issue": [
{
"code": "not-found",
"details": {
"text": "Patient does not exist"
},
"expression": [
"Condition.subject"
],
"severity": "error"
}
],
"resourceType": "OperationOutcome"
}
}
}
],
"id": "30f8c1a5-27de-4b96-8071-4e5b93c0da26",
"meta": {
"lastUpdated": "2026-08-11T15:02:44.318000Z",
"versionId": "MTc4NjAyMTQ1MjkwNTAwMDM2Nw"
}
}

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