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": [{ "type": "image", "url": "https://cdn.example.com/photo.jpg" }] }httpsonly. The host must resolve to a public address: internal URLs,localhostand private IPs are rejected.- Up to 5 redirects, all
httpsas 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.
curl -X POST https://api.uat.postmatic.dev/v1/media \
-H "x-access-key: pm_live_…" \
-F "file=@photo.jpg"{
"success": true,
"media": { "id": "med_…", "url": "https://…/media/…jpg", "width": 1080, "height": 1350, "bytes": 412873, "mime": "image/jpeg" }
}{ "mediaItems": [{ "type": "image", "mediaId": "med_…" }] }multipart/form-data, one file per request, in thefilefield.- Limit of 60 uploads per minute per key.
- The same image uploaded twice returns the same
id(content deduplication). - A
mediaIdis only valid in the project that created it; in another project, creating the post answers400 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": [{ "type": "video", "url": "https://cdn.example.com/reel.mp4" }] }Video upload in three steps
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 }'{
"success": true,
"upload": {
"mediaId": "med_…",
"path": "proj_…/med_….mp4",
"uploadUrl": "https://…/storage/v1/object/upload/sign/…",
"token": "…",
"expiresIn": 7200
}
}curl -X PUT "$UPLOAD_URL" \
-H "content-type: video/mp4" \
--data-binary @reel.mp4curl -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": [{ "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
| Rule | Value |
|---|---|
| Container | MP4 or MOV |
| Video codec | H.264 or HEVC |
| Audio codec | AAC, up to 48 kHz, mono or stereo |
| Frame rate | 23 to 60 per second |
| Width | up to 1920 px |
| Aspect ratio | 0.01:1 to 10:1 (9:16 recommended) |
| Duration | 3 seconds to 15 minutes |
| Size | up to 300 MB |
| Index | moov 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:
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4Limits
| Rule | Value |
|---|---|
| Accepted formats | JPEG, PNG, WebP (detected by content, not extension) |
| Size | up to 8 MB |
| Width | 320 to 1920 px on ingestion; feed accepts up to 1440 px |
| Aspect ratio | 0.1:1 to 10:1 on ingestion; feed requires 4:5 (0.8) to 1.91:1; Story recommends 9:16 |
| Output | JPEG, quality 90 |
When an image is rejected, details.reason says why:
reason | Meaning |
|---|---|
missing | No file field, or a request that is not multipart. |
too_many | More than one file (or too many parts) in the request. |
too_large | Image over 8 MB; video over 300 MB (Reel) or 100 MB (Story and carousel). |
mime | Format not accepted. |
width | Image outside 320 to 1920 px (or 320 to 1440 px for feed); video above 1920 px. |
aspect | Aspect 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). |
duration | Reel outside 3 s to 15 min; Story or carousel item outside 3 s to 60 s. |
fps | Frame rate outside 23 to 60. |
codec | Video codec other than H.264 or HEVC. |
audio | Audio that is not AAC, above 48 kHz, or with more than 2 channels. |
bitrate | Video bitrate above 25 Mbps. |
moov | MP4 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.