Storage Upload API
POST /v1/storage/uploads
Creates a temporary S3 upload URL.
Authentication: required for documents; optional for public profile-photo and trip-cover uploads.
Request body:
purposestring, required:document,profile_photo, ortrip_coverfilenamestring, requiredcontent_typestring, requiredcontent_lengthinteger: required for documents, optional for public imagesvisibilitystring:privatefor documents,publicfor public images; defaults by purposeparent_typestring: required for documents (trip,activity,hosting,transportation); optional for trip covers (trip)parent_idpositive integer: exact parent ID, required wheneverparent_typeis sent
Defaults:
documentdefaults toprivate; public document uploads are rejected.profile_photoandtrip_coverdefault topublic; private public-image uploads are rejected.
Validation:
- Document uploads require both parent fields and an exact
content_lengthfrom 1 to 104857600 bytes (100 MiB). The MCP inline upload tool limits files to 8 MiB and MCP upload preparation to 25 MiB. - Parent fields must be sent together.
profile_photomust not sendparent_typeorparent_id.trip_covermay omitparent_typeandparent_id.- If a
trip_coverparent is provided, it must useparent_type="trip"andparent_id.
Permissions:
document: authenticated trip membership plus document visibility and edit permissions. Owners require active Pro; permitted collaborators do not need their own Pro.profile_photo: no authentication required.trip_cover: no authentication required.
Behavior
The API returns a presigned S3 PUT URL. The client uploads bytes directly to S3 using upload_url and must send the exact headers returned by this endpoint.
Do not send Tripsy auth headers to S3.
Profile photos and trip cover images are stored in public image storage. Use public_url when updating photo_url or cover_image_url.
Profile photo example
curl -X POST "https://api.tripsy.app/v1/storage/uploads" \
-H "Content-Type: application/json" \
-d '{
"purpose": "profile_photo",
"filename": "avatar.jpg",
"content_type": "image/jpeg"
}'
{
"upload_url": "https://storage.example.com/upload/...",
"method": "PUT",
"headers": {
"Content-Type": "image/jpeg",
"x-amz-acl": "public-read"
},
"object_key": "2a9f6b3ea63a4f0d8cb8fba0abed9d2b2a9f6b3ea63a4f0d8cb8fba0abed9d2b2a9f6b3ea63a4f0d8cb8fba0abed9d2b2a9f6b3ea63a4f0d8cb8fba0abed9d2b.jpg",
"bucket": "PUBLIC_PROFILE_IMAGES_BUCKET",
"purpose": "profile_photo",
"visibility": "public",
"expires_at": "2026-04-22T13:15:00Z",
"public_url": "https://cdn.example.com/profile-images/2a9f6b3ea63a4f0d8cb8fba0abed9d2b.jpg"
}
Client migration flow
- Call
POST /v1/storage/uploads. - Upload bytes to
upload_url. - For
profile_photo, callPATCH /v1/meand sendphoto_url=public_url. - For
trip_cover, callPATCH /v1/trips/{id}and sendcover_image_url=public_url.
Public image uploads may happen before the user or trip exists. Attaching the returned public_url is the step that still requires the normal authenticated update API.
Document upload example
- Prepare a private file upload for the exact parent.
parent_idis the trip ID for a trip parent, or the itinerary item's ID for a child parent.
curl -X POST 'https://api.tripsy.app/v1/storage/uploads' \
-H 'Authorization: Token YOUR_TOKEN_HERE' \
-H 'Content-Type: application/json' \
-d '{
"purpose": "document",
"parent_type": "trip",
"parent_id": 42,
"filename": "photo.jpg",
"content_type": "image/jpeg",
"content_length": 12345
}'
Use the file's actual byte count for content_length. The response includes upload_url, method="PUT", exact headers (including Content-Type and Content-Length), object_key, bucket, purpose="document", visibility="private", content_length, expires_at, and upload_token. It does not include public_url.
- PUT the file bytes to
upload_urlwith every returned header exactly as supplied. URLs and receipts expire after 15 minutes by default. Never forward your Tripsy token to S3. - Attach the file to the same parent using
object_keyasurl, the same MIME type asfile_type, andupload_token.
The receipt is bound to the caller, exact parent, key, and MIME type. Preparing or uploading bytes does not create a document by itself. If attachment fails after the upload succeeds, retry attachment with the same valid receipt.