Breeder API

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

ParameterTypeRequiredDescription
uploadTypestringRequiredWhat the media is for: listing_image, listing_video, pairing_image, or profile_image.
contentTypestringRequiredThe MIME type of the file you're about to upload. E.g. "image/jpeg", "image/png", "video/mp4".
entityIdstringOptionalRequired 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.

ParameterTypeRequiredDescription
uploadTokenstringRequiredThe 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?