upload_media
Upload a photo (raw bytes or multipart).
/v1/media/uploadSend the image as the raw body (Content-Type image/jpeg, image/png or image/webp) with the fields on the query string, or as multipart/form-data with a file field plus the same fields. The server checks the real file type, applies the EXIF rotation, scales the long side to 2048 px, stores WebP without EXIF, GPS, XMP or IPTC metadata (the capture date is kept as capturedOn) and makes a 640 px thumbnail for media rows. At most 6 MB and 6000 px per side: resize and compress first. target=blob returns a storageId for chapter covers and intro photos instead of a media row. Upload each photo once; update_media changes its fields later.
curl -X POST "https://sceniq.earth/api/v1/media/upload?ownerType=spot&ownerId=k1709e2vj4sk8q6x0d3m5r7t9w1y3b5c&caption=Grand%20Prismatic%20Spring%20from%20the%20overlook%2C%20mid%20morning&credit=Mara%20Lindgren&alt=A%20deep%20blue%20pool%20ringed%20with%20orange%20and%20yellow%2C%20steam%20drifting%20over%20the%20boardwalk%20below&focalX=0.52&focalY=0.46" \
-H "Authorization: Bearer $SCENIQ_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary @grand-prismatic-overlook.jpgQuery parameters
- ownerTypestringoptional
What the photo belongs to: a spot, the guide (store thumbnail and sales page material), a chapter, or the sales page. Required unless target is blob.
spotproductcollectionlisting - ownerIdstringoptional
Id of the owner row (
spotId,productId,collectionIdorlistingId). Required unless target is blob. - targetstringoptional
media (the default) registers a photo; blob stores the file and returns a
storageIdforcoverStorageId(update_chapter) orphotoStorageId(set_chapter_intro).mediablob - captionstringoptional
Caption buyers read under the photo.
- creditstringoptional
Photographer credit, rendered as Photo: Name. Every photo that is not the creator's own carries one.
- altstringoptional
Alt text: what the photo shows, for screen readers.
- focalXnumberoptional
Focal point 0 (left) to 1 (right); the apps keep it in frame when they crop.
- focalYnumberoptional
Focal point 0 (top) to 1 (bottom).
- sourcePageUrlstringoptional
The page the photo was licensed from (not the image file).
- licencestringoptional
Licence name (Unsplash License, CC BY 4.0).
- creditUrlstringoptional
The photographer's profile page.
- capturedOnstringoptional
Capture date (YYYY-MM-DD). Read from the file's metadata when absent.
- locationVerifiedBystringoptional
How the photo's location was confirmed.
geotaglandmarkphotographer_captioncreatorother - reviewNotesstringoptional
Notes for the creator's review; never shown to buyers.
- metaobjectoptional
A JSON object with any of the fields above plus shows ({ kind, label? }) and
takenAt({ lat, lon, precision }). On the query string it is JSON text.
Form fields
- filestringoptional
Multipart only: the image file.
Returns
201 Created with UploadedPhoto or UploadedBlob.
{
"mediaId": "kg71s3d5f7g9h1j3k5k7z9x1c3v5b7n9",
"url": "https://quiet-heron-512.convex.cloud/api/storage/grand-prismatic-overlook.webp",
"thumbUrl": "https://quiet-heron-512.convex.cloud/api/storage/grand-prismatic-overlook-640.webp",
"ownerType": "spot",
"ownerId": "k1709e2vj4sk8q6x0d3m5r7t9w1y3b5c",
"caption": "Grand Prismatic Spring from the overlook, mid morning",
"credit": "Mara Lindgren",
"order": 1024,
"contentType": "image/webp",
"width": 2048,
"height": 1365,
"bytes": 684312,
"originalBytes": 5412880,
"optimized": true,
"metadataStripped": true,
"thumbBytes": 61240,
"capturedOn": "2026-09-14"
}Errors
Every error is { error: { code, message } }. Act on the code.
- 400invalid_argumentA required field is missing, a field is unknown, or a value does not match its type (the Convex validator's path and value are in the message).
- 400invalid_requestA writer refused a value: a map link on a host that is not allowed, a title that is too long, a date that does not exist, too many items in a list. The message is the writer's own sentence.
- 400unsupported_imageAn upload's bytes are not a JPEG, PNG or WebP file, or their dimensions cannot be read. The bytes are checked, not the Content-Type header.
- 400image_too_largeAn upload is more than 6,000 px on a side.
- 401unauthenticatedThe request carries no key. Send it as Authorization: Bearer sk_sceniq_... (or X-Api-Key).
- 401invalid_api_keyThe key is unknown, revoked or expired.
- 403api_scope_requiredA read-only key called a write operation, or a key without the publish option called publish_changes.
- 403forbiddenThe key belongs to a creator account that is no longer active.
- 404not_foundThe id does not exist or belongs to another creator. The two answers are identical on purpose, so ids never reveal what exists.
- 413payload_too_largeA JSON body over 2 MB, an image over 6 MB, a batch upload over 19 MB, or an MCP message over 2 MB.
- 429rate_limitedOver a limit: requests per key per minute, per creator per hour, uploads per key per minute, or check_links per guide per hour.
- 500internal_errorAn unexpected failure. The message is hidden on purpose.