Overview
Import complete Apple Health data exports via XML files using one of these methods:- S3-compatible multipart upload (Recommended) - For large files, uploads parts directly to an S3-compatible bucket (AWS S3 or self-hosted), reports progress, and explicitly queues processing.
- S3 presigned POST with SNS - AWS-only workflow for deployments configured with
APPLE_XML_UPLOAD_COMPLETION_MODE=sns. - Direct Upload - For smaller files or testing, uploads directly to the API
Authentication
All endpoints require authentication via Bearer token (user login) or API key.Endpoints
Method 1: S3 Presigned POST (SNS mode only)
Uploads directly to AWS S3. This workflow starts processing only whenAPPLE_XML_UPLOAD_COMPLETION_MODE=sns and the bucket publishes object-created events
through SNS. In the default client mode, use the multipart workflow below.
Step 1: Request Presigned URL
string
required
The ID of the user to import data for
string
default:""
Custom filename (max 200 characters)
integer
default:"300"
URL expiration time in seconds (60 - 3600)
integer
default:"52428800"
Maximum file size in bytes (1KB - 5GiB). Default is 50MB.
Step 2: Upload File to S3
Use theupload_url and form_fields from the previous response to upload your XML file:
Important: When uploading to S3 with presigned POST, you must include all
form_fields as form data, and the file field must be last.Method 1b: Multipart Upload (S3-compatible)
The portal uploads large exports using multipart upload, which splits the file into parts uploaded in parallel via presigned URLs. It works identically against any S3-compatible bucket (AWS S3 or a self-hosted server), so a local deployment with no AWS account can still accept multi-gigabyte Apple Health exports through the UI. The flow is four calls (all authenticated, all under/import/apple/xml/s3/multipart):
1
create
POST .../multipart/create with { "filename", "content_type", "file_size" } →
returns { "upload_id", "key", "bucket", "part_size" }. Split the file into parts of
part_size bytes (the last part may be smaller).2
sign
POST .../multipart/sign with { "key", "upload_id", "part_numbers": [1, 2, ...] } →
returns a presigned PUT URL for each part.3
upload parts
PUT each part’s bytes to its presigned URL. Capture the ETag response header of
every part.4
complete
POST .../multipart/complete with { "key", "upload_id", "parts": [{ "part_number", "etag" }] }.
In the default client completion mode this also dispatches the import task and
returns a task_id. Use POST .../multipart/abort to discard an incomplete upload.Multipart XML uploads accept files from 5 MiB through 5 GiB. The backend returns its
configured recommendation (
APPLE_XML_MULTIPART_PART_SIZE_BYTES, 100 MiB by default)
from the create endpoint. Always use that returned part_size instead of hard-coding
it in a client.Browser part uploads read the
ETag from the PUT response, so the bucket’s CORS policy
must allow PUT and expose the ETag header (ExposeHeaders: ["ETag"]).Completion mode - the deployment’s
APPLE_XML_UPLOAD_COMPLETION_MODE controls how a
finished upload starts processing, and thus the /complete response you get: client
(default) dispatches the import from the /complete call and returns 202 Accepted with
a task_id; sns instead waits for an S3 bucket event and /complete only finalizes the
object (200 OK). Only the selected mode dispatches, so a file is never processed twice.Complete multipart example
This script authenticates with an API key, creates and signs an upload, sends every part, captures itsETag, and completes the upload. If any step fails after creation,
it makes a best-effort abort request so uploaded parts do not linger.
upload_xml_multipart.py
client mode:
202 Accepted
GET /api/v1/users/{user_id}/sync/stream. The XML import’s
terminal sync.status event uses the returned task ID as its run_id; on success or
failure the portal refreshes the user’s health data. Recent results are also available
from GET /api/v1/users/{user_id}/sync/runs for 24 hours.
Multipart responses and errors
429 and transient 503 responses
as retryable with exponential backoff.
lifecycle-rule.json
Complete Example Workflow
- S3 Presigned POST (SNS mode)
- Direct Upload
1
Login to Get Access Token
2
Request Presigned URL
3
Upload File to S3
4
Processing Happens Automatically
With
APPLE_XML_UPLOAD_COMPLETION_MODE=sns (AWS), the system automatically:- Detects the S3 upload via S3 event notification → SNS
- SNS sends an HTTPS notification to the backend
process_aws_uploadCelery task downloads and processes the XML file- Imports workouts and time series data to database
client mode, the presigned-POST method above has no completion
trigger - use Method 1b (multipart) and its /complete call instead, which is
what the portal uses and which works for any S3-compatible bucket.View Complete Script (Copy & Run)
View Complete Script (Copy & Run)
upload_xml_s3.py
Data Imported
Workouts
- Activity type (running, cycling, swimming, etc.)
- Duration and timestamps
- Distance, calories, elevation
- Heart rate statistics (min/max/avg)
Time Series Samples
- Heart rate
- Steps
- Active energy
- Distance
- Blood oxygen
- And 100+ other metrics
Best Practices
Use S3 for Large Files
Files over 10MB should use multipart upload to avoid API request limits
Handle Async Processing
Import processing is asynchronous. Don’t expect immediate data availability
Monitor Task Status
Use Celery Flower or logs to monitor processing status
Dedupe Handled Automatically
Records with the same external_id won’t be duplicated
Related
- Apple Health Setup Guide - Complete guide with export instructions
- Quick Integration - Getting started
- Error Handling - Common errors and solutions

