Media
Upload photos and videos up to 1 GB and get a URL to publish with.
You can pass any public https link as video_url, photo_url or media[]; Google Drive, Dropbox and OneDrive share links are turned into direct file links. Or upload the file to PostWire: the bytes go straight from your client to storage, never through the API. Uploaded media is deleted after 30 days.
POST /api/media/upload-url: Get a signed URL to upload a photo or video (up to 1 GB). Files over 50 MB: send "multipart": true, PUT bytes [start, end) to each returned part's upload_url, then GET /api/media/finalize?path= returns one media_url for the whole file. Media passed by URL is fetched by us: up to 1GB for TikTok and YouTube (streamed), 80MB elsewhere.GET /api/media/finalize: Confirm the upload landed and get the media_url to publish withPOST /api/media/upload-link: A link to upload a file from another device (a phone), valid 24 hoursPOST /api/media/video: Make a short vertical video from a post's words (for YouTube and TikTok)
Get a signed URL to upload a photo or video (up to 1 GB). Files over 50 MB: send "multipart": true, PUT bytes [start, end) to each returned part's upload_url, then GET /api/media/finalize?path= returns one media_url for the whole file. Media passed by URL is fetched by us: up to 1GB for TikTok and YouTube (streamed), 80MB elsewhere.
/api/media/upload-urlUpload your own file instead of hosting it yourself. The bytes go straight from your client to storage — they never pass through this API. 1) POST here. 2) PUT the raw file bytes to upload_url. 3) GET /api/media/finalize?path=<path> to receive media_url. 4) pass media_url as video_url or photo_url to /api/post. Media is deleted after 30 days.
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
content_type required | string | One of: image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime, video/webm. |
size_bytes | integer | |
multipart | boolean | true for files over 50 MB: the answer lists parts, each with its own upload_url and byte range [start, end). |
Responses
| Status | Means |
|---|---|
200 | upload_url + path |
413 | code use_multipart (send multipart: true) or too_large (over 1 GB) |
curl -X POST "https://postwire.io/api/media/upload-url" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content_type": "video/mp4",
"size_bytes": 48213504
}'const res = await fetch("https://postwire.io/api/media/upload-url", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"content_type": "video/mp4",
"size_bytes": 48213504
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/media/upload-url",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"content_type": "video/mp4",
"size_bytes": 48213504,
},
)
print(r.status_code, r.json())Example response
{
"path": "<account_id>/2f9c….mp4",
"upload_url": "https://…supabase.co/storage/v1/object/upload/sign/pw-media/…",
"token": "…",
"content_type": "video/mp4",
"max_bytes": 52428800,
"retention_days": 30,
"how": "1) PUT the raw file bytes to upload_url. 2) GET /api/media/finalize?path=<path> …"
}Then PUT the raw bytes to upload_url with the same Content-Type, and call GET /api/media/finalize?path= with the returned path.
Confirm the upload landed and get the media_url to publish with
/api/media/finalizeNeeds an API key: Authorization: Bearer pw_live_….
Parameters
| Field | Type | Description |
|---|---|---|
path required | query, string |
Responses
| Status | Means |
|---|---|
200 | media_url + bytes |
404 | not uploaded yet |
curl "https://postwire.io/api/media/finalize?path=%3Caccount_id%3E%2F2f9c%E2%80%A6.mp4" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/media/finalize?path=%3Caccount_id%3E%2F2f9c%E2%80%A6.mp4", {
method: "GET",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());import os, requests
r = requests.get(
"https://postwire.io/api/media/finalize?path=%3Caccount_id%3E%2F2f9c%E2%80%A6.mp4",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Example response
{
"ok": true,
"path": "<account_id>/2f9c….mp4",
"media_url": "https://…/pw-media/<account_id>/2f9c….mp4?token=…",
"bytes": 48213504,
"content_type": "video/mp4",
"expires_at": "2026-10-04T12:00:00.000Z"
}A link to upload a file from another device (a phone), valid 24 hours
/api/media/upload-linkFor an assistant or a flow that cannot send the file itself: open the url on the device that has the video, upload, and GET /api/media/uploads/{upload_id} (or the dashboard) returns its media_url. 30 links an hour.
Needs an API key: Authorization: Bearer pw_live_….
Responses
| Status | Means |
|---|---|
200 | { url, upload_id, expires_in: 86400, max_bytes, accepts[] } |
curl -X POST "https://postwire.io/api/media/upload-link" \
-H "Authorization: Bearer $POSTWIRE_API_KEY"const res = await fetch("https://postwire.io/api/media/upload-link", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}` },
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/media/upload-link",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
)
print(r.status_code, r.json())Make a short vertical video from a post's words (for YouTube and TikTok)
/api/media/videoYouTube takes only video and TikTok video or photos. This turns the words of a post into an MP4 you can publish there: 1080x1920 (9:16), 8 to 12 seconds, H.264 High with AAC sound, 30 fps. The first line (or first sentence) appears as a large heading and up to two more sentences appear one after another over a softly moving plain background, inside the area TikTok and Shorts leave clear of their buttons and caption. Emoji, links and trailing hashtags are left out; nothing is added that you did not write, and there is no PostWire watermark. Music: one of three royalty-free beds (Pixabay Content License, checked free of YouTube Content ID) at about -14 LUFS, or silence. No AI is used and nothing is published: pass the returned media_url as video_url to /api/post or /api/schedule. The file is stored like an upload and deleted after 30 days. Draws Latin, Greek and Cyrillic text. 10 videos an hour per account; per day 10 on the Free plan and 60 on paid plans. Takes about 3 to 10 seconds.
Needs an API key: Authorization: Bearer pw_live_….
Body (JSON)
| Field | Type | Description |
|---|---|---|
text required | string | The post's words. The first line or sentence becomes the heading; up to two more sentences follow. Up to 5000 characters. |
style | string | Warm off-white or dark background. One of: light, dark. Default "light". |
music | string | auto picks one of the three beds from the text; none gives a silent AAC track. One of: auto, lofi, piano, upbeat, none. Default "auto". |
footer | string | Optional short line under the text, e.g. the brand or shop name. Up to 40 characters. |
Responses
| Status | Means |
|---|---|
200 | { ok, media_url, path, kind: "video", content_type: "video/mp4", bytes, width: 1080, height: 1920, duration_s, music, text_on_video[], poster_url (the last frame as a JPEG), render_ms, expires_at } |
400 | code no_text (not enough words) or unsupported_script (an alphabet the font cannot draw) |
429 | 10 an hour per account, or code video_daily_limit (the plan's videos a day) |
503 | code video_unavailable: the renderer is not available on this server |
curl -X POST "https://postwire.io/api/media/video" \
-H "Authorization: Bearer $POSTWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "We'\''re a family bakery. This Saturday we launch sourdough cinnamon rolls, and the first 50 are free with any coffee.",
"footer": "Butter & Spice",
"music": "auto"
}'const res = await fetch("https://postwire.io/api/media/video", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.POSTWIRE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"text": "We're a family bakery. This Saturday we launch sourdough cinnamon rolls, and the first 50 are free with any coffee.",
"footer": "Butter & Spice",
"music": "auto"
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://postwire.io/api/media/video",
headers={"Authorization": f"Bearer {os.environ['POSTWIRE_API_KEY']}"},
json={
"text": "We're a family bakery. This Saturday we launch sourdough cinnamon rolls, and the first 50 are free with any coffee.",
"footer": "Butter & Spice",
"music": "auto",
},
)
print(r.status_code, r.json())Example response
{
"ok": true,
"media_url": "https://…/pw-media/<account_id>/video-7c1e….mp4?token=…",
"path": "<account_id>/video-7c1e….mp4",
"kind": "video",
"content_type": "video/mp4",
"bytes": 2412044,
"width": 1080,
"height": 1920,
"duration_s": 9,
"music": "piano",
"text_on_video": [
"We're a family bakery.",
"This Saturday we launch sourdough cinnamon rolls, and the first 50 are free with any coffee."
],
"poster_url": "https://…/pw-media/<account_id>/video-7c1e….jpg?token=…",
"render_ms": 3100,
"expires_at": "2026-11-03T12:00:00.000Z"
}Then publish it: POST /api/post with platforms: ["youtube", "tiktok"], the caption as text and video_url set to media_url.