Uploading images
A meal analysis takes 1 to 4 images of the same meal. Every way of getting an image in ends as a
file resource (file_…) that the analysis references.
| Method | Best for | Size limit |
|---|---|---|
| Multipart upload | Server-side uploads, reusable file ids | 15 MB |
| Presigned upload | Large files, or uploading straight from a device | 25 MB |
| Image URL | Images already hosted on a public HTTPS URL | 15 MB |
| Images in the create request | One-off server-side calls | 15 MB |
Formats and limits
- Formats: JPEG, PNG and WebP. The type is checked against the file's magic bytes, not its name
or
Content-Type. - HEIC/HEIF is not supported in v1. iPhones save HEIC by default; convert to JPEG on the device
before upload (iOS does this when you request
image/jpeg). HEIC files are rejected withunsupported_media_type. - Dimensions: at most 50 megapixels. Animated images use their first frame.
- Per analysis: up to 4 images, all of the same meal (for example two angles). They must belong to the same project and mode as the key.
What we do to your images
- EXIF and other metadata are stripped — GPS position, camera serial numbers, XMP — after the orientation tag has been applied, so the stored image is upright and anonymous.
- We never store your filename. Filenames often contain names or dates.
- Sanitisation is asynchronous. A new file goes
uploaded → processing → ready(orrejectedwith a code). You do not need to wait: an analysis that references a file that is not ready yet waits inqueued, and fails only if the file is rejected. - Retention follows your organization's retention class. Images are always held while an analysis that uses them waits for confirmation, then deleted on schedule. See Data handling.
Multipart upload
POST /v1/files with a single part named file.
curl https://platform.opis.health/v1/files \
-H "Authorization: Bearer $OPIS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F file=@lunch.jpgPresigned upload
Use this to upload large files, or to let a device upload directly to storage while your backend keeps the API key.
-
Reserve the upload. Your backend calls
POST /v1/file-uploadswith the content type, the exact byte size and the SHA-256 of the file (base64):JSON { "content_type": "image/jpeg", "bytes": 2483021, "sha256": "u4E0x…base64…=" }The response is a
file_uploadwith the file'sid(file_…), a presignedupload_url, theupload_method(PUT), theupload_headersyou must send, andexpires_at(15 minutes). -
Upload the bytes to
upload_url, sending exactly theupload_headers— for example:HTTP PUT <upload_url> Content-Type: image/jpeg Content-Length: 2483021 x-amz-checksum-sha256: u4E0x…base64…=Storage rejects the upload if the size, type or checksum differ from what you declared.
-
Complete it.
POST /v1/file-uploads/{id}/complete. The file then goes through the same sanitisation as a multipart upload. Referencing a file whose upload was never completed returns409 file_not_ready.
Image URL
Pass a public HTTPS URL instead of a file_id. We fetch it while handling your create request and
store it as a file; the URL itself is not kept.
{ "images": [{ "url": "https://cdn.example.com/meals/8f2c.jpg" }] }To protect both of us, URL fetching is strict:
httpsonly, on a public address. Private, loopback and link-local addresses (including cloud metadata endpoints) are refused, after DNS resolution as well as before.- At most 2 redirects, to the same host. A 10-second timeout and the 15 MB cap apply while streaming.
- The response must be a JPEG, PNG or WebP image, confirmed by its magic bytes.
Any failure returns 422 url_fetch_failed with a pointer to the offending URL. Signed URLs (for
example S3 presigned GETs) work well as long as they stay valid for a minute or two.
Images in the create request
For one-off server-side calls you can skip the separate upload: send POST /v1/meal-analyses as
multipart/form-data with 1–4 image parts and an optional params part holding the JSON fields
(description, confirmation, external_id, …). The images become files exactly as if you had
uploaded them first.
curl https://platform.opis.health/v1/meal-analyses \
-H "Authorization: Bearer $OPIS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F image=@lunch.jpg \
-F 'params={"external_id":"meal-123"};type=application/json'Errors
| Status | Code | Meaning |
|---|---|---|
| 413 | image_too_large | An uploaded image is over 15 MB; use a presigned upload (up to 25 MB) |
| 415 | unsupported_media_type | Not JPEG, PNG or WebP — including HEIC |
| 422 | invalid_image | Corrupt or truncated, over 50 megapixels, or rejected during sanitisation |
| 422 | url_fetch_failed | The image URL could not be fetched within the rules above |
| 404 | file_not_found | The file_id does not exist in this project and mode, or was deleted |
| 409 | file_not_ready | The presigned upload was never completed |
| 409 | file_in_use | The file is held by an unfinished analysis, so it cannot be deleted yet |
A file rejected during asynchronous sanitisation fails the analyses that reference it (with
error.code invalid_image) and emits the file.rejected webhook. See Errors for the
full catalogue.