Uploading media — API reference
How to upload images and videos to Herpify listings and pairings using presigned upload URLs.
API Updates — March 2026
The media API has been updated to use abstract media identifiers and a token-based upload flow instead of exposing storage implementation details.
- **New upload flow:** Presign endpoint now returns an `uploadToken` instead of a raw storage `key`. After uploading, call POST /api/breeder/media/confirm with the token to create the media record.
- **New response fields:** `mediaId` (opaque identifier) and `mediaUrl` (abstract path like /media/abc123)
- **Deprecated fields:** `url` (storage key) and `cdnUrl` (CDN-specific URL) — will be removed March 2027
- **Backward compatibility:** Both old and new fields are currently returned in responses. Update your integrations to use the new upload flow and `mediaId`/`mediaUrl` fields before March 2027.
Important note
All examples below show the updated token-based upload flow and response format.
What it does
The media endpoint gives you a presigned upload URL so you can upload images and videos directly from your script or app to Herpify's storage — without ever sending the file through Herpify's servers. This is faster, more reliable, and works with large video files.
Who can access it
Breeder plan subscribers only. Authenticate with your API key in the `Authorization: Bearer` header.
How media uploads work
Unlike most data in the API (where you POST JSON to Herpify), media files need a three-step process:
- Step 1 — Call POST /api/breeder/media/presign to get a presigned upload URL and an upload token
- Step 2 — PUT your file directly to the presigned URL (no Authorization header needed)
- Step 3 — Call POST /api/breeder/media/confirm with the upload token to create the media record and link it to your listing or pairing
Important note
The presigned URL expires 5 minutes after it's issued. Complete your upload within that window.
Request a presigned URL — POST /api/breeder/media/presign
| Parameter | Type | Required | Description |
|---|---|---|---|
| uploadType | string | Required | What the media is for: listing_image, listing_video, pairing_image, or profile_image. |
| contentType | string | Required | The MIME type of the file you're about to upload. E.g. "image/jpeg", "image/png", "video/mp4". |
| entityId | string | Optional | Required when uploadType is listing_image, listing_video, or pairing_image. The ID of the listing or pairing you're attaching the media to. |
Important note
You can attach a maximum of 20 images per listing. The presigned URL and upload token expire 5 minutes after issuance.
Complete upload example
Step 1 — Get presigned URL and upload token
curl -X POST https://herpify.com/api/breeder/media/presign \
-H "Authorization: Bearer hm_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"uploadType": "listing_image",
"contentType": "image/jpeg",
"entityId": "clxb1c2d3e4f5g6h7"
}'Step 1 response
{
"data": {
"uploadUrl": "https://[presigned-endpoint]?signature=...",
"uploadToken": "upl_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4",
"expiresAt": "2026-03-27T08:05:00.000Z",
"maxSizeBytes": 15728640
}
}Step 2 — Upload file to presigned URL (no auth needed)
curl -X PUT "https://[presigned-endpoint]?signature=..." \ -H "Content-Type: image/jpeg" \ --data-binary @/path/to/your/photo.jpg
Step 3 — Confirm upload and create media record
curl -X POST https://herpify.com/api/breeder/media/confirm \
-H "Authorization: Bearer hm_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"uploadToken": "upl_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4"
}'Step 3 response
{
"data": {
"mediaId": "abc123xyz456def789ghi",
"mediaUrl": "/media/abc123xyz456def789ghi",
"role": "GALLERY",
"order": 1,
"type": "listing_image",
"entityId": "clxb1c2d3e4f5g6h7"
}
}Confirm upload — POST /api/breeder/media/confirm
After uploading your file to the presigned URL, call this endpoint to create the media record in the database and link it to your listing or pairing.
| Parameter | Type | Required | Description |
|---|---|---|---|
| uploadToken | string | Required | The upload token returned from /api/breeder/media/presign. Format: upl_... |
Important note
Upload tokens expire 5 minutes after issuance. If the token is expired or invalid, you'll receive a 400 error and need to restart the upload flow.
Response format — images in API responses
When you fetch listings or pairings, image objects now include both old (deprecated) and new abstract fields:
Image object structure
{
"id": "clx1a2b3c4d5e6f7g8h9",
// NEW: Abstract media fields (use these)
"mediaId": "xyz9k8j7h6g5f4d3s2a1",
"mediaUrl": "/media/xyz9k8j7h6g5f4d3s2a1",
// DEPRECATED: Will be removed March 2027
"url": "listings/clxb1c2d3e4f5g6h7/abc123.webp",
"cdnUrl": "https://cdn.herpify.com/...",
// Metadata
"role": "COVER",
"order": 0,
"width": 1600,
"height": 1200
}Important note
Update your code to use `mediaUrl` instead of `url` or `cdnUrl`. Abstract media URLs work exactly the same — just reference them in image tags or fetch them directly.
Real-world example
You take photos of your new Coastal Carpet Python hatchlings on your phone, which syncs them to a shared folder. A script watches the folder, detects new images, requests a presigned URL for each, and uploads them directly. Photos go live within minutes of you taking them — no manual uploads needed. Your dashboard then displays each image using the new `mediaUrl` field, which abstracts away the storage implementation.
Was this article helpful?