postmatic

Media

Two ways to attach images to a post, by public URL or direct upload, and the format, size and aspect limits Postmatic validates before publishing.

Each item in a post's mediaItems points to an image in one of two ways: a public URL, which Postmatic downloads and stores, or a mediaId from an image uploaded beforehand. Either way the image goes through the same validation and is converted to JPEG.

By URL

mediaItems by URL
{ "mediaItems": [{ "type": "image", "url": "https://cdn.example.com/photo.jpg" }] }
  • https only. The host must resolve to a public address: internal URLs, localhost and private IPs are rejected.
  • Up to 5 redirects, all https as well.
  • 30-second download timeout.
  • The download happens when the post is created, before the response. The URL does not need to stay reachable afterwards.

A failed download answers 400 MEDIA_FETCH_FAILED or 400 MEDIA_UNREACHABLE; an image outside the limits answers 400 MEDIA_INVALID.

By upload

Send the image first and use the returned id in your posts. Useful when images have no public URL or when you want to validate before scheduling.

POST /v1/media
curl -X POST https://api.uat.postmatic.dev/v1/media \
  -H "x-access-key: pm_live_…" \
  -F "file=@photo.jpg"
Response 201
{
  "success": true,
  "media": { "id": "med_…", "url": "https://…/media/…jpg", "width": 1080, "height": 1350, "bytes": 412873, "mime": "image/jpeg" }
}
mediaItems by mediaId
{ "mediaItems": [{ "type": "image", "mediaId": "med_…" }] }
  • multipart/form-data, one file per request, in the file field.
  • Limit of 60 uploads per minute per key.
  • The same image uploaded twice returns the same id (content deduplication).
  • A mediaId is only valid in the project that created it; in another project, creating the post answers 400 VALIDATION_ERROR.

Video

A video publishes as a Reel. There are two paths, and the difference between them is where the file comes from at publish time.

By URL. Postmatic reads a few slices of the file to validate it and passes the same URL to Instagram, which fetches the video. No bytes are copied. In exchange, the URL needs to stay reachable until publishing, which matters for a post scheduled weeks ahead.

By upload. The file goes straight from your server to our storage, without passing through the API, and publishing no longer depends on anything of yours. It is the recommended path for schedules and for large files.

mediaItems by URL
{ "mediaItems": [{ "type": "video", "url": "https://cdn.example.com/reel.mp4" }] }

Video upload in three steps

1. Ask for the URL
curl -X POST https://api.uat.postmatic.dev/v1/media/upload-url \
  -H "x-access-key: pm_live_…" \
  -H "content-type: application/json" \
  -d '{ "filename": "reel.mp4", "mimeType": "video/mp4", "bytes": 41234567 }'
Response 201
{
  "success": true,
  "upload": {
    "mediaId": "med_…",
    "path": "proj_…/med_….mp4",
    "uploadUrl": "https://…/storage/v1/object/upload/sign/…",
    "token": "…",
    "expiresIn": 7200
  }
}
2. Send the file to the URL
curl -X PUT "$UPLOAD_URL" \
  -H "content-type: video/mp4" \
  --data-binary @reel.mp4
3. Confirm
curl -X POST https://api.uat.postmatic.dev/v1/media/confirm \
  -H "x-access-key: pm_live_…" \
  -H "content-type: application/json" \
  -d '{ "mediaId": "med_…" }'

Confirmation validates the file and returns the media record. Use the mediaId in the post:

mediaItems by mediaId
{ "mediaItems": [{ "type": "video", "mediaId": "med_…" }] }

The signed URL is valid for 2 hours. A file that fails validation is removed from storage and never becomes a record.

Video limits

RuleValue
ContainerMP4 or MOV
Video codecH.264 or HEVC
Audio codecAAC, up to 48 kHz, mono or stereo
Frame rate23 to 60 per second
Widthup to 1920 px
Aspect ratio0.01:1 to 10:1 (9:16 recommended)
Duration3 seconds to 15 minutes
Sizeup to 300 MB
Indexmoov at the start of the file

A video with no audio track is accepted.

The index needs to come first

Instagram requires the MP4 index (the moov box) to sit at the start of the file, before the video data. It is the same requirement as any video that starts playing before it finishes downloading, and most editors call the option faststart. A file with the index at the end is rejected with reason: moov.

To fix an existing file, rewrite the container without re-encoding:

Move the index to the start
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4

Limits

RuleValue
Accepted formatsJPEG, PNG, WebP (detected by content, not extension)
Sizeup to 8 MB
Width320 to 1920 px on ingestion; feed accepts up to 1440 px
Aspect ratio0.1:1 to 10:1 on ingestion; feed requires 4:5 (0.8) to 1.91:1; Story recommends 9:16
OutputJPEG, quality 90

When an image is rejected, details.reason says why:

reasonMeaning
missingNo file field, or a request that is not multipart.
too_manyMore than one file (or too many parts) in the request.
too_largeImage over 8 MB; video over 300 MB (Reel) or 100 MB (Story and carousel).
mimeFormat not accepted.
widthImage outside 320 to 1920 px (or 320 to 1440 px for feed); video above 1920 px.
aspectAspect ratio outside destination limits: feed 4:5 to 1.91:1; Story 0.1:1 to 10:1; carousel with items of different ratios (details.position indicates which).
durationReel outside 3 s to 15 min; Story or carousel item outside 3 s to 60 s.
fpsFrame rate outside 23 to 60.
codecVideo codec other than H.264 or HEVC.
audioAudio that is not AAC, above 48 kHz, or with more than 2 channels.
bitrateVideo bitrate above 25 Mbps.
moovMP4 index at the end of the file.

Carousel requires all items to have the same aspect ratio (1% tolerance): Instagram would crop all of them to match the first item's ratio, so we prefer to refuse rather than surprise you.

Retention

Images are kept so the post can be published and shown again in the dashboard. There is no automatic expiry nor a route to delete media yet; see the Roadmap.

Media — Postmatic Documentation