Upload Media
Upload a media file to your workspace using a presigned URL.
Upload a media file to your workspace using a presigned URL.
/api/v1/mediaRequest a presigned upload URL and create a media record.
Uploading media is a three-step process:
PENDING_UPLOAD state and returns a temporary upload URL (uploadUrl) and the permanent public URL (publicUrl).PUT the file directly to the uploadUrl. The file goes straight to cloud storage, bypassing the dopost server for faster uploads.POST /api/v1/media/{id}/finalize to verify the upload succeeded. The API confirms the file exists in storage, extracts real metadata (dimensions, duration, thumbnail), and marks the record as READY. Only after this step does the media appear in your library and GET /api/v1/media responses.The uploadUrl is a presigned URL: a temporary link that authorizes a single direct upload to storage. It expires after 1 hour. The publicUrl is the permanent URL you'll reference in posts.
Using the MCP server? The upload_media tool handles all three steps automatically. Just provide the local file path and it returns the publicUrl ready to use in publish_post.
Send the file metadata to POST /api/v1/media. The response includes a presigned uploadUrl where you'll upload the file, and a publicUrl that will be the permanent URL of the file once finalized.
{
"fileName": "photo.jpg",
"contentType": "image/jpeg",
"sizeInBytes": 204800
}{
"fileName": "photo.jpg",
"contentType": "image/jpeg",
"sizeInBytes": 204800
}| Field | Type | Required | Description |
|---|---|---|---|
fileName | string | Yes | Original file name (e.g. photo.jpg) |
contentType | string | Yes | MIME type of the file |
sizeInBytes | number | No | File size in bytes. Optional hint — actual size is determined from storage during finalize |
| Type | MIME |
|---|---|
| JPEG | image/jpeg, image/jpg |
| PNG | image/png |
| GIF | image/gif |
| MP4 | video/mp4 |
| MOV | video/quicktime |
{
"id": "cm3abc123def456",
"uploadUrl": "https://storage.example.com/workspaces/…?X-Amz-Signature=…",
"publicUrl": "https://media.dopost.co/workspaces/ws_123/1719849600000-photo.jpg",
"fileName": "photo.jpg",
"contentType": "image/jpeg",
"expiresIn": 3600
}{
"id": "cm3abc123def456",
"uploadUrl": "https://storage.example.com/workspaces/…?X-Amz-Signature=…",
"publicUrl": "https://media.dopost.co/workspaces/ws_123/1719849600000-photo.jpg",
"fileName": "photo.jpg",
"contentType": "image/jpeg",
"expiresIn": 3600
}| Field | Description |
|---|---|
id | Unique media ID. Use this to reference the file in other endpoints |
uploadUrl | Presigned URL — upload your file here with a PUT request (Step 2) |
publicUrl | Permanent public URL of the file. Use this in publish_post |
expiresIn | Seconds until the uploadUrl expires (1 hour) |
Take the uploadUrl from the previous response and PUT your file to it. The Content-Type header must match the contentType you sent in Step 1.
curl --request PUT \
--url "<uploadUrl>" \
--header 'Content-Type: image/jpeg' \
--data-binary '@/path/to/photo.jpg'curl --request PUT \
--url "<uploadUrl>" \
--header 'Content-Type: image/jpeg' \
--data-binary '@/path/to/photo.jpg'The uploadUrl expires after 1 hour. If it expires, request a new one by repeating Step 1.
A successful PUT returns an empty 200 response from storage. Proceed to Step 3 once you receive it.
/api/v1/media/{id}/finalizeVerify the upload and mark the media record as ready.
After the PUT succeeds, call this endpoint with the id returned in Step 1. The API will:
width, height) and duration for images and videosREADY so it appears in your media librarycurl --request POST \
--url "https://dopost.co/api/v1/media/<id>/finalize" \
--header 'x-api-key: <apiKey>'curl --request POST \
--url "https://dopost.co/api/v1/media/<id>/finalize" \
--header 'x-api-key: <apiKey>'{
"id": "cm3abc123def456",
"url": "https://media.dopost.co/workspaces/ws_123/1719849600000-photo.jpg",
"thumbnailUrl": "https://media.dopost.co/workspaces/ws_123/1719849600000_photo_thumb.webp",
"name": "photo.jpg",
"type": "image/jpeg",
"sizeInBytes": 204800,
"width": 1920,
"height": 1080,
"durationSeconds": null,
"status": "READY"
}{
"id": "cm3abc123def456",
"url": "https://media.dopost.co/workspaces/ws_123/1719849600000-photo.jpg",
"thumbnailUrl": "https://media.dopost.co/workspaces/ws_123/1719849600000_photo_thumb.webp",
"name": "photo.jpg",
"type": "image/jpeg",
"sizeInBytes": 204800,
"width": 1920,
"height": 1080,
"durationSeconds": null,
"status": "READY"
}If you skip this step, the media record stays in PENDING_UPLOAD state and will not appear in GET /api/v1/media responses or be usable in posts. Stale pending records are automatically deleted after 1 hour.
// Step 1: Request upload URL
const initResponse = await fetch("https://dopost.co/api/v1/media", {
method: "POST",
headers: {
"x-api-key": "<apiKey>",
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: "banner.png",
contentType: "image/png",
sizeInBytes: 524288,
}),
});
const { id, uploadUrl, publicUrl } = await initResponse.json();
// Step 2: Upload the file directly to the presigned URL
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "image/png" },
body: fileBuffer,
});
// Step 3: Finalize — verify the upload and mark media as READY
const finalizeResponse = await fetch(`https://dopost.co/api/v1/media/${id}/finalize`, {
method: "POST",
headers: { "x-api-key": "<apiKey>" },
});
const media = await finalizeResponse.json();
// media.status === "READY", media.width, media.height, media.thumbnailUrl are now populated
// Done — use publicUrl in a post
await fetch("https://dopost.co/api/v1/post/publish", {
method: "POST",
headers: {
"x-api-key": "<apiKey>",
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "<accountId>",
text: "Check out this image!",
media: [publicUrl],
}),
});// Step 1: Request upload URL
const initResponse = await fetch("https://dopost.co/api/v1/media", {
method: "POST",
headers: {
"x-api-key": "<apiKey>",
"Content-Type": "application/json",
},
body: JSON.stringify({
fileName: "banner.png",
contentType: "image/png",
sizeInBytes: 524288,
}),
});
const { id, uploadUrl, publicUrl } = await initResponse.json();
// Step 2: Upload the file directly to the presigned URL
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "image/png" },
body: fileBuffer,
});
// Step 3: Finalize — verify the upload and mark media as READY
const finalizeResponse = await fetch(`https://dopost.co/api/v1/media/${id}/finalize`, {
method: "POST",
headers: { "x-api-key": "<apiKey>" },
});
const media = await finalizeResponse.json();
// media.status === "READY", media.width, media.height, media.thumbnailUrl are now populated
// Done — use publicUrl in a post
await fetch("https://dopost.co/api/v1/post/publish", {
method: "POST",
headers: {
"x-api-key": "<apiKey>",
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "<accountId>",
text: "Check out this image!",
media: [publicUrl],
}),
});| Status | Error | Cause |
|---|---|---|
400 | fileName is required | Missing fileName field |
400 | contentType is required | Missing contentType field |
400 | Unsupported content type | MIME type not in the allowed list |
400 | File too large | File exceeds the 1 GB limit |
400 | Media file not found in storage | Finalize called before the file was PUT, or the PUT failed |
401 | Invalid or missing API key | Bad or missing x-api-key header |
403 | API key does not have permission for this action | API key lacks the required scope for this endpoint |
404 | Media not found | The id passed to /finalize does not exist or belongs to another workspace |
429 | Rate limit exceeded | Too many requests. Check X-RateLimit-* response headers and retry after the indicated time |