Enviando um recurso FHIR
Com o token de acesso em mãos (veja Autenticação), o próximo passo é enviar o registro de saúde. Aqui há uma diferença importante em relação a APIs comuns: o HubSaúde confirma o recebimento na hora, mas processa o conteúdo depois. Seu sistema envia, recebe um comprovante e consulta esse comprovante até o processamento terminar.
Como funciona o envio em duas etapas
O envio é um POST /fhir/{tipo}, ou PUT, PATCH e DELETE em /fhir/{tipo}/{id} para alterar e remover. Quatro cabeçalhos são obrigatórios: Content-Type: application/fhir+json, Accept, Prefer: respond-async e Idempotency-Key. Faltando qualquer um deles, a requisição volta com 400.
Primeiro, o comprovante
A resposta é 202 com corpo vazio, o equivalente a "recebi e vou processar". O cabeçalho Content-Location traz o endereço de uma Task, um recurso FHIR próprio para acompanhar pedidos em andamento (não é o paciente nem o resultado final), e Retry-After sugere quanto esperar antes de consultar.
Depois, o resultado
Consulte com GET o endereço recebido em Content-Location (/Task/{id}) até que Task.status indique conclusão. Em criação, atualização e correção, Task.output.resultReference aponta para o recurso final. Em remoção, o sucesso vem como valueBoolean=true.
Se a rede falhar
Quando você não souber se o envio chegou, reenvie com a mesma Idempotency-Key. O HubSaúde reconhece a repetição, não duplica nada e devolve o mesmo 202, acrescentando Idempotency-Replayed: true quando a operação já terminou.
Autenticação vinculada a certificado
Se o token foi emitido numa conexão com certificado de cliente, ele carrega o campo cnf e fica atrelado a esse certificado (veja Cache e mTLS). Isso quer dizer que o cabeçalho Authorization: Bearer sozinho não basta: a chamada a /fhir/* também precisa ser mTLS, apresentando o mesmo certificado de cliente usado para obter o token — não precisa ser a mesma conexão TCP, só o mesmo certificado. Caso contrário a resposta é 401, acompanhada de WWW-Authenticate: Bearer error="invalid_token" e de um OperationOutcome explicando a recusa.
Criando um Patient
O exemplo abaixo monta um paciente com o mínimo de informação, envia e consulta o resultado. Repare que são chamadas HTTP diretas: os SDKs oficiais cuidam apenas da autenticação, e o envio dos dados fica por sua conta.
- Python
- Java
- TypeScript
- C#
import httpx
import time
import uuid
paciente = {
"resourceType": "Patient",
"identifier": [{"system": "urn:oid:2.16.76.1.3.3", "value": "123456789"}],
"name": [{"use": "official", "text": "Maria da Silva"}],
"birthDate": "1990-01-01",
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/fhir+json",
"Accept": "application/fhir+json",
"Prefer": "respond-async",
"Idempotency-Key": str(uuid.uuid4()),
}
# O mesmo certificado de cliente usado para obter o token (RFC 8705)
with httpx.Client(cert=("certificado.pem", "chave-privada.pem")) as http:
resp = http.post("https://hub.saude.go.gov.br/fhir/Patient", json=paciente, headers=headers)
if resp.status_code != 202:
raise RuntimeError(f"Envio recusado: {resp.status_code} {resp.text}")
task_url = resp.headers["Content-Location"]
while True:
task = http.get(task_url, headers={"Authorization": f"Bearer {token}"}).json()
if task["status"] not in ("accepted", "in-progress"):
break
time.sleep(1)
if task["status"] != "completed":
raise RuntimeError(f"Task não concluída ({task['status']}): {task}")
print(task["output"][0]["valueReference"])
var paciente = """
{
"resourceType": "Patient",
"identifier": [{"system": "urn:oid:2.16.76.1.3.3", "value": "123456789"}],
"name": [{"use": "official", "text": "Maria da Silva"}],
"birthDate": "1990-01-01"
}
""";
// httpClient deve estar configurado com o mesmo certificado de cliente usado para obter o token (RFC 8705)
var request = HttpRequest.newBuilder(URI.create("https://hub.saude.go.gov.br/fhir/Patient"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/fhir+json")
.header("Accept", "application/fhir+json")
.header("Prefer", "respond-async")
.header("Idempotency-Key", UUID.randomUUID().toString())
.POST(HttpRequest.BodyPublishers.ofString(paciente))
.build();
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 202) {
throw new IllegalStateException("Envio recusado: " + response.statusCode() + " " + response.body());
}
String taskUrl = response.headers().firstValue("Content-Location").orElseThrow();
JsonNode task;
String status;
do {
Thread.sleep(1000);
var taskRequest = HttpRequest.newBuilder(URI.create(taskUrl))
.header("Authorization", "Bearer " + token)
.GET()
.build();
var taskResponse = httpClient.send(taskRequest, HttpResponse.BodyHandlers.ofString());
task = objectMapper.readTree(taskResponse.body());
status = task.get("status").asText();
} while (status.equals("accepted") || status.equals("in-progress"));
if (!status.equals("completed")) {
throw new IllegalStateException("Task não concluída (" + status + "): " + task);
}
import { Agent } from "undici";
const paciente = {
resourceType: "Patient",
identifier: [{ system: "urn:oid:2.16.76.1.3.3", value: "123456789" }],
name: [{ use: "official", text: "Maria da Silva" }],
birthDate: "1990-01-01",
};
// O mesmo certificado de cliente usado para obter o token (RFC 8705)
const mtlsAgent = new Agent({ connect: { cert: clientCert, key: clientKey } });
const response = await fetch("https://hub.saude.go.gov.br/fhir/Patient", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/fhir+json",
Accept: "application/fhir+json",
Prefer: "respond-async",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(paciente),
dispatcher: mtlsAgent,
});
if (response.status !== 202) {
throw new Error(`Envio recusado: ${response.status} ${await response.text()}`);
}
const taskUrl = response.headers.get("Content-Location")!;
let task: any;
do {
await new Promise((r) => setTimeout(r, 1000));
const taskResponse = await fetch(taskUrl, {
headers: { Authorization: `Bearer ${token}` },
dispatcher: mtlsAgent,
});
task = await taskResponse.json();
} while (task.status === "accepted" || task.status === "in-progress");
if (task.status !== "completed") {
throw new Error(`Task não concluída (${task.status}): ${JSON.stringify(task)}`);
}
var paciente = new
{
resourceType = "Patient",
identifier = new[] { new { system = "urn:oid:2.16.76.1.3.3", value = "123456789" } },
name = new[] { new { use = "official", text = "Maria da Silva" } },
birthDate = "1990-01-01",
};
// httpClient deve estar configurado com o mesmo certificado de cliente usado para obter o token (RFC 8705)
using var request = new HttpRequestMessage(HttpMethod.Post, "https://hub.saude.go.gov.br/fhir/Patient")
{
Content = JsonContent.Create(paciente, new MediaTypeHeaderValue("application/fhir+json")),
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/fhir+json"));
request.Headers.Add("Prefer", "respond-async");
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var response = await httpClient.SendAsync(request);
if ((int)response.StatusCode != 202)
{
var corpo = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Envio recusado: {(int)response.StatusCode} {corpo}");
}
string taskUrl = response.Content.Headers.ContentLocation!.ToString();
JsonDocument task;
string status;
do
{
await Task.Delay(1000);
using var taskRequest = new HttpRequestMessage(HttpMethod.Get, taskUrl);
taskRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var taskResponse = await httpClient.SendAsync(taskRequest);
task = JsonDocument.Parse(await taskResponse.Content.ReadAsStreamAsync());
status = task.RootElement.GetProperty("status").GetString()!;
} while (status is "accepted" or "in-progress");
if (status != "completed")
{
throw new InvalidOperationException($"Task não concluída ({status}): {task.RootElement}");
}
Testando localmente antes de produção
Vale exercitar tudo isso na sua máquina antes de apontar para o ambiente real. O HubSaúde CLI sobe dois ambientes locais, cada um resolvendo um problema diferente.
Simulador: o fluxo completo
hubsaude sim start sobe uma cópia local do HubSaúde em HTTPS na porta 8443, exigindo mTLS como a produção. É onde você testa autenticação e envio de ponta a ponta. Os dados ficam apenas em memória e desaparecem ao reiniciar.
Servidor FHIR: o formato do recurso
hubsaude server start sobe um servidor HAPI FHIR local, sem autenticação, útil para conferir se o recurso está bem formado antes de enviá-lo para produção. Use hubsaude server start --reset para começar do zero.
Erros comuns
| Status | Causa |
|---|---|
| 400 OperationOutcome(required) | Header obrigatório ausente (Accept, Prefer, Idempotency-Key, Content-Type) |
| 406 | Accept incompatível |
| 415 OperationOutcome(not-supported) | Content-Type malformado ou incompatível |
| 409 | Replay concorrente da mesma Idempotency-Key (requisição original ainda em voo) |
| 413 OperationOutcome(too-long) | Payload excede o limite |
| Task.status=rejected | JSON bem formado, mas FHIR inválido. Confira antes no Validador |
As regras de cada tipo de recurso, como quais campos são obrigatórios em um BRPaciente, ficam nos Guias de Implementação, onde você consulta a versão vigente de cada perfil.