CKG Data Ingestion
Upload FHIR Bundle
Upload your FHIR bundle JSON to the pre-signed URL after requesting it via the Upload FHIR Push Upload URL API
PUT
/
fhir-push
/
upload
cURL
# Upload URL obtained from Step 1
UPLOAD_URL="<upload_url_from_step_1>"
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/json" \
-H "x-goog-content-length-range: 0,524288000" \
--data-binary @my-fhir-bundle.json
# 200 OK response indicates successimport requests
upload_url = "<upload_url_from_step_1>"
headers = {
"Content-Type": "application/json",
"x-goog-content-length-range": "0,524288000"
}
with open("my-fhir-bundle.json", "rb") as f:
response = requests.put(
upload_url,
headers=headers,
data=f,
timeout=300 # 5 minutes for large files
)
response.raise_for_status()
print(f"Upload successful: {response.status_code}")import fs from "fs";
const uploadUrl = "<upload_url_from_step_1>";
const bundleContent = fs.readFileSync("my-fhir-bundle.json", "utf-8");
const response = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"x-goog-content-length-range": "0,524288000",
},
body: bundleContent,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
console.log("Upload successful");<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sdp.suki.ai/fhir-push/upload",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'resourceType' => 'Bundle',
'type' => 'transaction',
'entry' => [
[
'resource' => [
'resourceType' => 'Patient',
'id' => 'patient-789',
'name' => [
[
'family' => 'Doe',
'given' => [
'John'
]
]
]
]
],
[
'resource' => [
'resourceType' => 'Encounter',
'id' => 'enc-123',
'subject' => [
'reference' => 'Patient/patient-789'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sdp.suki.ai/fhir-push/upload"
payload := strings.NewReader("{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://sdp.suki.ai/fhir-push/upload")
.header("Content-Type", "application/json")
.body("{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sdp.suki.ai/fhir-push/upload")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body""{
"code": 400,
"message": "invalid request"
}{
"code": 403,
"message": "forbidden"
}{
"code": 404,
"message": "not found"
}{
"code": 404,
"message": "not found"
}After requesting an upload URL via Request upload URL API, use HTTP PUT to upload your FHIR bundle directly to the pre-signed URL.
After a successful upload (200 OK response), proceed to Step 3:
This is Step 2 of the CKG data ingestion workflow. Complete Step 1 first to obtain the upload URL.
FHIR bundle requirements
Your bundle must satisfy the following requirements:| Requirement | Description |
|---|---|
| Valid JSON | Must parse without syntax errors |
| Maximum size | You can upload a bundle up to 500 MB in size |
| Resource size | Each FHIR resource can be up to 10 MB in size |
| Binary resource size | Binary resources can be up to 40 MB in size |
| Multiple patients | Bundles can include resources from multiple patients |
| No DELETE operations | Bundle entries must not use the DELETE method |
If the upload URL expires before you upload, request a new URL via Request upload URL API.
Best practices for summary generation
For optimal summary generation keep the following in mind:- Include key identifiers - Include consistent patient, encounter, and practitioner identifiers in your FHIR resources so they match the
fhir_encounter_idandfhir_practitioner_idvalues you use later. - Use correlation IDs - Supply meaningful
correlation_idvalues to track related uploads.
Suki maps generation and retrieval requests to the data ingested through CKG using the encounter and practitioner identifiers you provide.
Troubleshooting
Upload URL expired
Upload URL expired
If you see a 403 Forbidden or 410 Gone error, the upload URL has expired (15 minutes). Request a new URL via Step 1 and retry the upload.
File too large
File too large
If you see a 400 Bad Request mentioning size, your bundle exceeds 500 MB. Options:
- Split the bundle into multiple smaller bundles.
- Remove unnecessary resources.
- Use separate uploads for each encounter or date range.
Invalid JSON
Invalid JSON
Ensure your FHIR bundle is valid JSON before uploading. Use a JSON validator or linter to check syntax.
Query Parameters
The complete pre-signed upload URL returned from GET /api/v1/fhir-push/upload-url. This is a Google Cloud Storage URL, not a Suki API endpoint.
Example:
"https://storage.googleapis.com/suki-fhir-push/..."
Body
application/json
Your FHIR bundle as raw JSON (not form-encoded, not base64-encoded).
FHIR Bundle resource containing Patient, Encounter, Practitioner, and other clinical resources.
Response
Upload successful. The FHIR bundle has been accepted and will be processed. Continue to Step 3 to poll ingestion status using the transaction_id from Step 1.
The response is of type string.
Example:
""
Last modified on August 7, 2026
Was this page helpful?
โI
cURL
# Upload URL obtained from Step 1
UPLOAD_URL="<upload_url_from_step_1>"
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/json" \
-H "x-goog-content-length-range: 0,524288000" \
--data-binary @my-fhir-bundle.json
# 200 OK response indicates successimport requests
upload_url = "<upload_url_from_step_1>"
headers = {
"Content-Type": "application/json",
"x-goog-content-length-range": "0,524288000"
}
with open("my-fhir-bundle.json", "rb") as f:
response = requests.put(
upload_url,
headers=headers,
data=f,
timeout=300 # 5 minutes for large files
)
response.raise_for_status()
print(f"Upload successful: {response.status_code}")import fs from "fs";
const uploadUrl = "<upload_url_from_step_1>";
const bundleContent = fs.readFileSync("my-fhir-bundle.json", "utf-8");
const response = await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"x-goog-content-length-range": "0,524288000",
},
body: bundleContent,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
console.log("Upload successful");<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sdp.suki.ai/fhir-push/upload",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'resourceType' => 'Bundle',
'type' => 'transaction',
'entry' => [
[
'resource' => [
'resourceType' => 'Patient',
'id' => 'patient-789',
'name' => [
[
'family' => 'Doe',
'given' => [
'John'
]
]
]
]
],
[
'resource' => [
'resourceType' => 'Encounter',
'id' => 'enc-123',
'subject' => [
'reference' => 'Patient/patient-789'
]
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sdp.suki.ai/fhir-push/upload"
payload := strings.NewReader("{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://sdp.suki.ai/fhir-push/upload")
.header("Content-Type", "application/json")
.body("{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sdp.suki.ai/fhir-push/upload")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"resourceType\": \"Bundle\",\n \"type\": \"transaction\",\n \"entry\": [\n {\n \"resource\": {\n \"resourceType\": \"Patient\",\n \"id\": \"patient-789\",\n \"name\": [\n {\n \"family\": \"Doe\",\n \"given\": [\n \"John\"\n ]\n }\n ]\n }\n },\n {\n \"resource\": {\n \"resourceType\": \"Encounter\",\n \"id\": \"enc-123\",\n \"subject\": {\n \"reference\": \"Patient/patient-789\"\n }\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body""{
"code": 400,
"message": "invalid request"
}{
"code": 403,
"message": "forbidden"
}{
"code": 404,
"message": "not found"
}{
"code": 404,
"message": "not found"
}