Skip to content
opis.Nutrition API

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.

MethodBest forSize limit
Multipart uploadServer-side uploads, reusable file ids15 MB
Presigned uploadLarge files, or uploading straight from a device25 MB
Image URLImages already hosted on a public HTTPS URL15 MB
Images in the create requestOne-off server-side calls15 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 with unsupported_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 (or rejected with a code). You do not need to wait: an analysis that references a file that is not ready yet waits in queued, 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.jpg

Presigned upload

Use this to upload large files, or to let a device upload directly to storage while your backend keeps the API key.

  1. Reserve the upload. Your backend calls POST /v1/file-uploads with 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_upload with the file's id (file_…), a presigned upload_url, the upload_method (PUT), the upload_headers you must send, and expires_at (15 minutes).

  2. Upload the bytes to upload_url, sending exactly the upload_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.

  3. 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 returns 409 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.

JSON
{ "images": [{ "url": "https://cdn.example.com/meals/8f2c.jpg" }] }

To protect both of us, URL fetching is strict:

  • https only, 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.

Shell
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

StatusCodeMeaning
413image_too_largeAn uploaded image is over 15 MB; use a presigned upload (up to 25 MB)
415unsupported_media_typeNot JPEG, PNG or WebP — including HEIC
422invalid_imageCorrupt or truncated, over 50 megapixels, or rejected during sanitisation
422url_fetch_failedThe image URL could not be fetched within the rules above
404file_not_foundThe file_id does not exist in this project and mode, or was deleted
409file_not_readyThe presigned upload was never completed
409file_in_useThe 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.