Postpone Logo
Scheduling Posts

Uploading Media

Upload media to your Content Library from a URL or from a local file.

The API offers two ways to upload media to your Content Library:

  • From a URL: uploadMediaFromUrl, when your file is already hosted at a public URL.
  • From a local file: startMediaUpload + completeMediaUpload, when the file lives on disk.

Images, videos, and GIFs are supported. Direct uploads are capped at 500 MiB per file.

Uploading Media from a URL

Adds a file to your Content Library from a public URL.

mutation uploadMediaFromUrl(
  $url: String!
  $name: String
  $mediaLabelIds: [ID]
) {
  uploadMediaFromUrl(
    url: $url
    name: $name
    mediaLabelIds: $mediaLabelIds
  ) {
    success
    media {
      id
      name
      url
      mimeType
      size
    }
    errors {
      field
      code
      message
    }
  }
}
url
String required
Public URL of the media file. Must be publicly accessible and link directly to the file.
name
String
Display name for the file in your Content Library. If omitted, the name is derived from the filename in the url.
mediaLabelIds
[ID]
IDs of existing Content Library labels to assign. Labels must already exist and belong to your account. Unknown or foreign IDs are ignored.

Direct Upload

When your file is on disk, upload it in three steps.

Start the upload

Call startMediaUpload with the exact file size in bytes and the file's mime type. It returns an uploadUrl and an uploadToken.

mutation startMediaUpload($size: BigInt!, $mimeType: String!) {
  startMediaUpload(size: $size, mimeType: $mimeType) {
    success
    uploadToken
    uploadUrl
    expiresAt
    errors {
      field
      code
      message
    }
  }
}
size
BigInt required
Size of the file in bytes. Must be greater than 0 and no more than 500 MiB. The upload URL is tied to this exact size, so the PUT in the next step must send a matching Content-Length.
mimeType
String required
Mime type of the file. Must be an image or a video type, such as image/png, image/jpeg, image/webp, image/gif, image/heic, video/mp4, or video/quicktime. The upload URL is tied to this exact value, so the PUT in the next step must send it as the Content-Type header, verbatim.

Upload the file

Send the raw file bytes to uploadUrl with an HTTP PUT. Three headers are required:

  • Content-Length must match the size you passed to startMediaUpload. The URL is tied to the declared size, and a mismatch is rejected.
  • Content-Type must match the mimeType you passed to startMediaUpload, verbatim. This is what makes the file's type detectable once it lands in your Content Library.
  • If-None-Match: * must be sent exactly as shown. The URL is signed with it, and requests without it are rejected.
curl --request PUT \
--upload-file ./campaign-hero.jpg \
--header 'Content-Length: 2048576' \
--header 'Content-Type: image/jpeg' \
--header 'If-None-Match: *' \
"https://uploads.example.com/...&signature=..."
Most HTTP clients (including curl with --upload-file) set Content-Length automatically from the file. Only set it by hand if your client doesn't.
The uploadUrl is valid until expiresAt, roughly an hour after it's issued. Upload the file and call completeMediaUpload before then, or start over.
Each uploadUrl accepts one successful upload. If the transfer fails partway through, just retry the same PUT. A 412 Precondition Failed response means a file was already uploaded with this URL. No need to retry, move on to completeMediaUpload.

Complete the upload

Once you finished uploading the file, you must call completeMediaUpload with the uploadToken and a display name. This adds the uploaded file to your Content Library and returns the created media.

mutation completeMediaUpload(
$uploadToken: String!
$name: String!
$mediaLabelIds: [ID]
) {
completeMediaUpload(
  uploadToken: $uploadToken
  name: $name
  mediaLabelIds: $mediaLabelIds
) {
  success
  media {
    id
    name
    url
    mimeType
    size
  }
  errors {
    field
    code
    message
  }
}
}
uploadToken
String required
The token returned by startMediaUpload.
name
String required
Display name for the file in your Content Library. A directly uploaded file carries no filename of its own, so this is the only name the file gets. It is also what you pass as name if you reference the file by name when scheduling.
mediaLabelIds
[ID]
IDs of existing Content Library labels to assign.
completeMediaUpload transfers the uploaded file into your Content Library, so the request takes longer for bigger files. Large videos may not finish in time, in which case the request fails with an upload timeout error or an HTTP 504. When that happens, wait a few seconds and call completeMediaUpload again with the same uploadToken. Retry until it returns success: true or a different error. Retrying is safe: if an earlier attempt already added the file to your library, the same media is returned instead of a duplicate.
An upload token maps to a single file: once completed, calling completeMediaUpload with the same token again returns the same media rather than creating a new one. Call startMediaUpload again for the next file. If a completion attempt fails, you can retry it while the upload is still valid (until expiresAt).

Scheduling a Post with the Uploaded Media

Both upload paths return the created media, including its id. Pass that id in the media list of any scheduling mutation to attach the file to a post:

mutation scheduleTweet($input: ScheduleTweetInput!) {
  scheduleTweet(input: $input) {
    success
    errors {
      field
      code
      message
    }
  }
}

You can also reference the file by the name you gave it when uploading, which is handy when you don't keep track of IDs:

{
  "media": [{ "name": "campaign-hero.jpg" }]
}

Names are matched case-insensitively and the first match wins, so prefer id when several files share a name.

Uploaded files stay in your Content Library, so the same id can be attached to as many posts as you like. Uploading is only needed once per file.

Uploading is optional. If your file is already at a public URL and you don't need it in your Content Library beforehand, you can always skip these mutations and pass { "url": "https://example.com/images/hero.jpg" } directly in media.

See Attaching Media for the full rules on media, including how many files each platform accepts.

Rate Limits

Media uploads count against a separate, stricter budget on top of the general API rate limit:

Free Accounts

10 uploads per hour

Paid Accounts

100 uploads per hour

An upload counts against this budget when it begins, that is, on each uploadMediaFromUrl or startMediaUpload call.

When you exceed these limits, the mutation returns success: false with a Media upload rate limit exceeded error.

The two budgets are tracked separately, so exhausting your upload budget never blocks your regular API requests. They aren't mutually exclusive, though: like every API call, an upload mutation also counts against the general rate limit.

Error Handling

The mutations return success: false with details in errors when there's a failure.

Always check success and inspect errors before using the returned media.