Arquivo do paciente

Um arquivo do paciente é um documento anexado à ficha dele: um laudo, um resultado de exame, um relatório, um termo assinado. No Nilo Care esses arquivos ficam na aba Arquivos da ficha do paciente — Pacientes › [nome do paciente] › Arquivos —, onde a equipe de cuidado os abre, baixa, renomeia e exclui.

No FHIR o recurso é o Media, e esta integração o usa para as duas pontas: enviar um PDF e consultar o que o paciente tem.

Este recurso não tem atualização. Ao contrário de todos os outros, o mesmo POST não cria e atualiza: se o identifier que você mandar já corresponder a um arquivo existente, a chamada é recusada. Cada arquivo enviado precisa de uma chave nova.

Também não há como apagar nem renomear um arquivo por esta API. Pela integração, só criar e consultar; excluir e renomear são ações da equipe no Nilo Care.

Campos

A coluna No Nilo Care traz o rótulo com que o dado aparece para a equipe; são rótulos, não caminhos de navegação.

CampoObrigatórioO que significaNo Nilo Care
resourceTypesimConstante Media
identifiersimSuas chaves do arquivo. Precisa ser nova a cada arquivo — é por aqui que a API descobre que o arquivo já existe, e arquivo que já existe não é atualizado
statussimExigido pelo FHIR e sem efeito no cadastro. Envie completed, que é o valor que a leitura sempre devolve
subjectsimO paciente do arquivo, por um identificador dele. type é Patient — omitido, é assumidoo arquivo aparece na aba Arquivos da ficha desse paciente
contentsimO arquivo. Informe url ou data
content.urlum dos doisNa escrita, o endereço de onde a plataforma vai baixar o arquivo. Na leitura, o caminho interno de armazenamento — não um link de download
content.dataum dos doisO PDF em base64, embutido na requisição. Nunca é devolvido na leitura
content.titlenãoNome do arquivo. Só é preservado no envio por data. Não é estável: a equipe pode renomear o arquivo no Nilo Care, e o nome novo passa a ser o que a leitura devolveo nome do arquivo na lista da aba Arquivos
content.contentTypesim com dataTipo do conteúdo. Só application/pdf é aceito. Não é guardado nem devolvido
content.sizenãoTamanho em bytes do conteúdo decodificado. Informado, tem de bater exatamente. Não é guardado nem devolvido
id · metanãoSó resposta: identificador Nilo FHIR do arquivo e metadados da gravação

Campos que a Nilo não usa

O Media canônico traz muito mais do que esta integração lê: type, modality, view, encounter, createdDateTime, issued, operator, reasonCode, bodySite, device, height, width, duration, frames, note, partOf, basedOn — e, dentro de content, o hash. Nenhum deles é lido, e por isso nenhum aparece na referência do recurso.

A referência não os declara, mas a API não recusa quem os manda — e nenhum deles tem efeito. O content.hash, em particular, passa e não é conferido: não conte com ele para validar a integridade do envio; use o content.size, que é conferido.

Os campos de topo, no entanto, ficam guardados no recurso e voltam nas leituras seguintes. Se você mandar um type ou um createdDateTime, eles vão reaparecer nas suas consultas — e até responder a uma busca por aquele campo — sem nunca terem significado nada para a plataforma. Parece dado da Nilo, e não é. Omita-os.

Enviar um arquivo

POST
/fhir/resources/Media
1curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Media \
2 -H "x-api-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "resourceType": "Media",
6 "identifier": [
7 {
8 "system": "https://www.acmesaude.com.br/integracao/arquivo/",
9 "value": "77001",
10 "use": "usual"
11 }
12 ],
13 "status": "completed",
14 "subject": {
15 "identifier": {
16 "system": "https://www.acmesaude.com.br/integracao/paciente/",
17 "value": "507823709",
18 "use": "usual"
19 },
20 "type": "Patient"
21 },
22 "content": {
23 "url": "https://arquivos.acmesaude.com.br/laudos/laudo-77001.pdf"
24 }
25}'

A resposta é o recurso gravado, sem envelope:

Response
1{
2 "resourceType": "Media",
3 "identifier": [
4 {
5 "system": "https://landing-zone-api.nilo.services/fhir/resources/NamingSystem/care-api--file-storage",
6 "value": "918273",
7 "use": "usual"
8 },
9 {
10 "system": "https://www.acmesaude.com.br/integracao/arquivo/",
11 "value": "77001",
12 "use": "usual"
13 }
14 ],
15 "status": "completed",
16 "subject": {
17 "identifier": {
18 "system": "https://www.acmesaude.com.br/integracao/paciente/",
19 "value": "507823709",
20 "use": "usual"
21 },
22 "reference": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4",
23 "type": "Patient"
24 },
25 "content": {
26 "url": "patients/44219/document/2026-08-20T14:31:02.884Z_Laudo_do_exame.pdf",
27 "title": "Laudo_do_exame.pdf"
28 },
29 "id": "7b1d90c4-2f65-4a83-9c1e-1d84f0a53b27",
30 "meta": {
31 "lastUpdated": "2026-08-20T14:31:03.512000Z",
32 "versionId": "MTc4NjAyMTQ1MjkwNTAwMDM2Nw"
33 }
34}

Guarde o id: é por ele que se faz a leitura direta. Ao lado do seu identifier, a resposta traz o identificador Nilo do arquivo, no system …/NamingSystem/care-api--file-storage.

A resposta não repete o que você enviou: o content volta como a plataforma o registrou — o caminho de armazenamento em url e o nome do arquivo em title —, sem data, contentType nem size. É a visão da plataforma, e é o mesmo que uma leitura devolve.

Por URL ou em base64

Há duas formas de mandar o conteúdo, e a escolha muda o nome do arquivo e as validações.

content.urlcontent.data
Como funcionaA plataforma baixa o arquivo do endereço informadoO conteúdo vai em base64 na própria requisição
Nome do arquivoMontado a partir do nome do paciente e da data — o content.title é descartadoDerivado do content.title
content.contentTypeNão é exigido nem conferidoObrigatório, e só application/pdf
O que decide o formato aceitoA extensão no caminho da URL, contra uma lista fechadaO contentType e os bytes do conteúdo
Formatos aceitosPDF e também imagem, documento de texto, áudio e vídeoSó PDF
Conferência do conteúdoNenhuma além da extensão e do downloadBase64 válido, PDF de verdade, tamanho e limite

content.url tem precedência. Mandando os dois no mesmo payload, o content.data é ignorado em silêncio e o arquivo baixado da URL é o que fica. A chamada responde 200.

No envio por URL, o formato é decidido pela extensão no caminho da URL, e não pelo contentType. A lista aceita é .pdf, .jpg, .jpeg, .png, .gif, .doc, .docx, .xml, .txt, .mp3, .ogg, .oga, .wav e .mp4.

Duas consequências que pegam quem só espera PDF:

  • URL sem extensão no caminho é recusada — um endereço como https://arquivos.acmesaude.com.br/laudos/77001, ou uma URL assinada cujo nome do objeto não termina em .pdf, não passa. A recusa sai como code: exception, sem expression;
  • por URL, dá para enviar coisa que não é PDF. A restrição a application/pdf vale só para o envio por data. Se a sua integração só deve mandar documentos, garanta isso do seu lado — a API não vai barrar um .jpg.

O endereço tem de estar acessível publicamente no momento da chamada: é a plataforma que faz o download, e ela não recebe as suas credenciais. Uma URL assinada com prazo curto pode expirar antes. Certificado TLS inválido e resposta que não seja 2xx também recusam a chamada, com code: exception.

O nome do arquivo

As regras abaixo valem só para o envio por data — é o único caminho em que o content.title sobrevive. No envio por URL o nome é montado pela plataforma como nome do paciente + data, e o título que você mandou é descartado.

No envio por data, o título passa por uma normalização antes de virar nome de arquivo:

  • acentos são transliterados (óo);
  • tudo que não for letra, número, ., _ ou - vira _;
  • uma extensão .pdf já presente no título não é duplicada;
  • o nome é cortado em 100 caracteres;
  • ., _ e - nas pontas são removidos.
content.title enviadoNome do arquivo no Nilo Care
"Laudo do exame"Laudo_do_exame.pdf
"Relatório Médico.pdf"Relatorio_Medico.pdf
"../../etc/Relatório Médico.pdf"etc_Relatorio_Medico.pdf
ausentedocument.pdf
POST
/fhir/resources/Media
1curl -X POST https://landing-zone-api.nilo.services/fhir/resources/Media \
2 -H "x-api-key: <apiKey>" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "resourceType": "Media",
6 "identifier": [
7 {
8 "system": "https://www.acmesaude.com.br/integracao/arquivo/",
9 "value": "77002",
10 "use": "usual"
11 }
12 ],
13 "status": "completed",
14 "subject": {
15 "identifier": {
16 "system": "https://www.acmesaude.com.br/integracao/paciente/",
17 "value": "507823709",
18 "use": "usual"
19 },
20 "type": "Patient"
21 },
22 "content": {
23 "title": "Laudo do exame",
24 "contentType": "application/pdf",
25 "data": "JVBERi0xLjQKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoKdHJhaWxlcgo8PC9Sb290IDEgMCBSPj4KJSVFT0YK",
26 "size": 72
27 }
28}'

O que é conferido no envio por data

Nesta ordem, e todas as recusas são 400:

  1. O identifier é casado antes de tudo. Chave já usada é recusada aqui, sem que o paciente ou o conteúdo cheguem a ser olhados. Antes disso, o recurso ainda passa pela validação de forma do FHIR, que recusa com code: structure.
  2. O paciente é resolvido em seguida. Arquivo de paciente que não existe não é decodificado nem sobe — não há resíduo de uma chamada recusada.
  3. content.contentType presente e igual a application/pdf.
  4. content.data é base64 válido. Espaços e quebras de linha são tolerados: um base64 quebrado em 76 colunas funciona.
  5. O conteúdo decodificado não é vazio.
  6. O conteúdo decodificado não passa de 10485760 bytes (10 MiB).
  7. content.size, se você mandou, bate exatamente com o tamanho decodificado.
  8. O conteúdo é um PDF: os primeiros bytes precisam ser %PDF-. Um arquivo de outro tipo com contentType: application/pdf é recusado aqui.

O limite de 10 MiB vale para o conteúdo depois de decodificado, não para o tamanho do corpo da requisição — em base64 o payload é cerca de um terço maior. A mensagem do erro too-long sempre informa o limite exato em bytes do seu ambiente.

O paciente

O paciente vem em subject.identifier, e é por aí:

1"subject": {
2 "identifier": {
3 "system": "https://www.acmesaude.com.br/integracao/paciente/",
4 "value": "507823709"
5 },
6 "type": "Patient"
7}

subject.reference sozinho não resolve o paciente, mesmo apontando para um Patient que existe. Sem subject.identifier a chamada é recusada, e o erro sai como code: exception, sem expression — veja Erros. Mande sempre o identificador.

O identificador pode ser o do seu sistema ou o identificador Nilo do paciente; nos dois casos ele tem de resolver para um paciente do seu ambiente. Não resolvendo, a recusa é Patient does not exist.

Buscar

GET
/fhir/resources/Media
1curl -G https://landing-zone-api.nilo.services/fhir/resources/Media \
2 -H "x-api-key: <apiKey>" \
3 -d _count=50 \
4 -d _lastUpdated=eq2013-01-14 \
5 --data-urlencode _page_token=Cjj3YopYuf%2F%2F%2F%2F%2BABd%2BbgE0m...dSANQAFoLCUSM7w9VYUqaEANglLWUugQ%3D \
6 --data-urlencode identifier=https://www.acmesaude.com.br/integracao/arquivo/|77001 \
7 --data-urlencode patient=Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4 \
8 --data-urlencode patient:identifier=https://www.acmesaude.com.br/integracao/paciente/|507823709 \
9 -d status=completed \
10 --data-urlencode subject=Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4 \
11 --data-urlencode subject:identifier=https://www.acmesaude.com.br/integracao/paciente/|507823709

A resposta é sempre um Bundle do tipo searchset:

Response
1{
2 "resourceType": "Bundle",
3 "type": "searchset",
4 "entry": [
5 {
6 "fullUrl": "https://landing-zone-api.nilo.services/fhir/resources/Media/7b1d90c4-2f65-4a83-9c1e-1d84f0a53b27",
7 "resource": {
8 "resourceType": "Media",
9 "identifier": [
10 {
11 "system": "https://landing-zone-api.nilo.services/fhir/resources/NamingSystem/care-api--file-storage",
12 "value": "918273",
13 "use": "usual"
14 },
15 {
16 "system": "https://www.acmesaude.com.br/integracao/arquivo/",
17 "value": "77001",
18 "use": "usual"
19 }
20 ],
21 "status": "completed",
22 "subject": {
23 "identifier": {
24 "system": "https://www.acmesaude.com.br/integracao/paciente/",
25 "value": "507823709",
26 "use": "usual"
27 },
28 "reference": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4",
29 "type": "Patient"
30 },
31 "content": {
32 "url": "patients/44219/document/2026-08-20T14:31:02.884Z_Laudo_do_exame.pdf",
33 "title": "Laudo_do_exame.pdf"
34 },
35 "id": "7b1d90c4-2f65-4a83-9c1e-1d84f0a53b27"
36 },
37 "search": {
38 "mode": "match"
39 }
40 },
41 {
42 "fullUrl": "https://landing-zone-api.nilo.services/fhir/resources/Media/c58f2a19-70d4-4be6-9a02-3ef17c8b4d51",
43 "resource": {
44 "resourceType": "Media",
45 "identifier": [
46 {
47 "system": "https://landing-zone-api.nilo.services/fhir/resources/NamingSystem/care-api--file-storage",
48 "value": "918274",
49 "use": "usual"
50 }
51 ],
52 "status": "completed",
53 "subject": {
54 "identifier": {
55 "system": "https://www.acmesaude.com.br/integracao/paciente/",
56 "value": "507823709",
57 "use": "usual"
58 },
59 "reference": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4",
60 "type": "Patient"
61 },
62 "content": {
63 "url": "patients/44219/image/2026-08-21T09:12:44.117Z_foto-receita.jpg",
64 "title": "foto-receita.jpg"
65 },
66 "id": "c58f2a19-70d4-4be6-9a02-3ef17c8b4d51"
67 },
68 "search": {
69 "mode": "match"
70 }
71 }
72 ],
73 "link": []
74}

Busca sem resultados não é erro: volta 200 com um Bundle cujo entry é uma lista vazia. Trate a ausência de resultados pela lista vazia, não esperando um 404.

Response
1{
2 "resourceType": "Bundle",
3 "type": "searchset",
4 "entry": [],
5 "link": []
6}

Parâmetros de busca suportados

NomeTipoDescriçãoExpressão
identifiertokenIdentificador do arquivo, em system|value. Aceita a sua chave ou o identificador Nilo do arquivoMedia.identifier
subject:identifiertokenArquivos de um paciente, pelo identificador deleMedia.subject.identifier
subjectreferenceArquivos de um paciente, pelo id Nilo FHIR deleMedia.subject
patient:identifier · patientidemSinônimos dos dois acima: aqui o subject é sempre um pacienteMedia.subject
statustokenAceito, e inútil: todo arquivo é completedMedia.status
_lastUpdateddateData da última gravação no store, com os prefixos eq, ge, le

A busca útil é a dos arquivos de um paciente — um GET sem nenhum filtro devolve os arquivos de todos os pacientes do seu ambiente, página por página, o que raramente é o que se quer:

$curl --request GET \
> --url 'https://landing-zone-api.nilo.services/fhir/resources/Media?subject:identifier=https://www.acmesaude.com.br/integracao/paciente/|507823709' \
> --header 'x-api-key: SUA_API_KEY'

Os demais parâmetros canônicos do Media existem e não encontram nada aqui, porque a Nilo não preenche o campo correspondente: type, modality, view, created, encounter, operator, device, site e based-on. A exceção é o campo que você mesmo tenha enviado: ele fica no recurso e passa a ser encontrável — mais um motivo para omitir o que a plataforma não usa.

Qual identificador a referência do paciente carrega depende da sua implantação. Na configuração que usa identificadores externos nas referências, subject traz o identificador do seu sistema e o filtro acima funciona como está. Sem ela, vem o identificador Nilo do paciente, e é esse system que você tem de usar no filtro. Confira numa resposta de leitura qual dos dois está lá antes de montar a busca em volume.

A paginação é por _count e _page_token; havendo página seguinte, o Bundle traz a URL pronta em link.

A busca não devolve só o que você enviou

A consulta expõe todos os arquivos do paciente, e não só PDF. Anexos que chegaram por outros canais — foto, áudio e vídeo trocados na conversa com o paciente, por exemplo — também saem como Media quando estão associados ao paciente. E como o content.contentType não é preenchido na leitura, não há campo que diga o tipo: se a sua integração só trata documentos, olhe a extensão no content.title.

Arquivos criados dentro do Nilo Care aparecem na consulta com apenas o identificador Nilo, no system …/NamingSystem/care-api--file-storage. Só os que vieram por integração trazem também a sua chave. É por essa diferença que você separa uns dos outros.

Arquivo que não esteja associado a um paciente não é exposto por esta API.

content.url na leitura não é um link de download. Para os arquivos enviados por integração é o caminho de armazenamento — algo como patients/{id}/document/{timestamp}_arquivo.pdf —, e um GET nesse valor não baixa nada. O formato varia conforme a origem do arquivo, e em alguns casos é um endereço completo; não construa a sua integração em cima dessa diferença.

Esta API não entrega o conteúdo dos arquivos: ela expõe o registro deles. Para obter o arquivo em si, use a fonte que você já tem — no envio por URL, o seu próprio endereço de origem.

Ler por ID

GET
/fhir/resources/Media/:id
1curl https://landing-zone-api.nilo.services/fhir/resources/Media/id \
2 -H "x-api-key: <apiKey>"

Diferente da busca, esta leitura não devolve um Bundle — mas também não devolve o recurso nu. A resposta é um envelope com fullUrl, search e resource, e o arquivo está em resource. Ler content ou subject na raiz da resposta não encontra nada.

Response
1{
2 "fullUrl": "https://landing-zone-api.nilo.services/fhir/resources/Media/7b1d90c4-2f65-4a83-9c1e-1d84f0a53b27",
3 "resource": {
4 "resourceType": "Media",
5 "identifier": [
6 {
7 "system": "https://landing-zone-api.nilo.services/fhir/resources/NamingSystem/care-api--file-storage",
8 "value": "918273",
9 "use": "usual"
10 },
11 {
12 "system": "https://www.acmesaude.com.br/integracao/arquivo/",
13 "value": "77001",
14 "use": "usual"
15 }
16 ],
17 "status": "completed",
18 "subject": {
19 "identifier": {
20 "system": "https://www.acmesaude.com.br/integracao/paciente/",
21 "value": "507823709",
22 "use": "usual"
23 },
24 "reference": "Patient/ba200cfa-dae0-46cf-81a0-008e3f7414b4",
25 "type": "Patient"
26 },
27 "content": {
28 "url": "patients/44219/document/2026-08-20T14:31:02.884Z_Laudo_do_exame.pdf",
29 "title": "Laudo_do_exame.pdf"
30 },
31 "id": "7b1d90c4-2f65-4a83-9c1e-1d84f0a53b27",
32 "meta": {
33 "lastUpdated": "2026-08-20T14:31:03.512000Z",
34 "versionId": "MTc4NjAyMTQ1MjkwNTAwMDM2Nw"
35 }
36 },
37 "search": {
38 "mode": "match"
39 }
40}

A leitura por ID responde 404 quando o id não existe — inclusive quando o arquivo existia e foi excluído no Nilo Care.

Valores aceitos

content.contentType

Um só: application/pdf. Qualquer outro valor é recusado com not-supported, e o conteúdo ainda é conferido byte a byte contra o formato — declarar PDF não basta, tem de ser PDF.

A restrição vale para o envio por data. No envio por URL quem decide é a extensão da URL, e ela aceita mais formatos — veja Por URL ou em base64. E a leitura devolve arquivos de qualquer formato, porque devolve tudo que o paciente tem.

status

Um só: completed. É o que a leitura sempre devolve, é o único que a referência declara e é o que você deve enviar. Os outros valores do FHIR R4 não são usados: a plataforma não guarda situação de arquivo, e o valor devolvido é sempre completed.

Efeitos colaterais

Reenviar um identifier já usado não atualiza — recusa. Não há como trocar o paciente nem o conteúdo de um arquivo já enviado: para isso, mande um arquivo novo com chave nova e peça à equipe que exclua o anterior no Nilo Care.

O nome, esse sim, muda depois: a equipe pode renomear o arquivo no Nilo Care, e o nome novo passa a ser o que a leitura devolve em content.title. Não trate o title como o valor que você enviou.

Se o arquivo foi excluído no Nilo Care, o registro deixa de existir para esta API — a consulta não o devolve mais, e a chave que ele usava volta a estar livre. Reenviar aquele mesmo identifier depois disso cria um arquivo novo, em vez de ser recusado.

Falhando o registro do arquivo, o conteúdo que já subiu é apagado — nesse caso a chamada recusada não deixa arquivo órfão.

Um erro depois do registro não desfaz o arquivo. O apagamento acima cobre a falha do registro em si; se a chamada falhar num passo posterior, você recebe 400 mas o arquivo já está na ficha do paciente. E como nada foi gravado do lado FHIR, repetir a chamada com a mesma chave não é reconhecido como reenvio: cria um segundo arquivo. Depois de um erro, confira a ficha antes de repetir.

Este endpoint nunca responde 204: não há remoção de arquivo por integração.

Erros

Recusa é 400, e o corpo é um OperationOutcome: issue[].expression aponta o campo culpado e issue[].details.text explica o motivo.

codeexpressionMensagemQuando
requiredMedia.identifierField is requiredO payload não tem identifier
business-ruleMedia.identifierUnable to use any of the provided identifiers to match an existing resource, which can lead to duplicates.identifier, mas nenhum utilizável — falta system ou falta value
requiredMedia.subjectField is requiredO payload não tem subject
not-foundMedia.subjectPatient does not existO subject.identifier não resolve para um paciente do seu ambiente
requiredMedia.contentEither content.url or content.data is requiredNem url nem data no content
requiredMedia.content.contentTypeField is requiredEnvio por data sem contentType
not-supportedMedia.content.contentTypeThe contentType '…' is not supported. Only 'application/pdf' is supported.contentType diferente de application/pdf
invalidMedia.content.dataField is not a valid base64-encoded contentO data não é base64 válido
requiredMedia.content.dataField is requiredO data é base64 válido, mas decodifica para nada
too-longMedia.content.dataField exceeds the maximum allowed size of 10485760 bytesConteúdo acima do limite do ambiente
invalidMedia.content.sizeField does not match the content size of … bytesO size enviado não bate com o conteúdo
invalidMedia.content.dataField content is not a valid PDF fileO conteúdo não começa com %PDF-
exceptionPermissionDenied: Update Media is not allowed.O identifier enviado já corresponde a um arquivo
exceptionFieldRequiredError: ['Field reference.identifier is required']subject sem identifier
exceptionUnsupported file type: …Envio por URL cujo caminho não termina em uma extensão reconhecida
exceptionmensagem do erro HTTP, ou The SSL certificate for the URL is not valid.Envio por URL que não pôde ser baixada

Os payloads completos estão na aba Referência, em POST /fhir/resources/Media.

Response
1{
2 "issue": [
3 {
4 "code": "exception",
5 "details": {
6 "text": "PermissionDenied: Update Media is not allowed."
7 },
8 "severity": "error"
9 }
10 ],
11 "resourceType": "OperationOutcome"
12}

As quatro últimas linhas da tabela não são validações — são falhas não tratadas, e a primeira delas é o caso mais comum deste recurso: o reenvio de uma chave. O 400 sai com code: exception, sem expression, e com uma mensagem de erro de linguagem em vez de uma explicação do campo.

Nenhuma delas é contrato: não tente interpretar o texto. Trate code: exception como “payload recusado, motivo não classificado” e confira a chave, o subject e a URL enviados.