letmepost / api surfaces / Media API
POST /v1/media

Upload once .

Reference everywhere.

Upload bytes once via POST /v1/media. Reference the returned mediaId from every post that uses it. Bytes move once, posts move many times. Required for video on every platform; recommended for any image you'll post more than once.

Multipart form-data · 8 MB images · 500 MB video · API reference →
POST /v1/media ·
curl -X POST https://api.letmepost.dev/v1/media \
  -H "Authorization: Bearer $LMP_KEY" \
  -F "file=@./photo.jpg" \
  -F "kind=image"

# Returns 201 with { id: "med_01HXZ4N9..." }
# Reference that id from posts.create.

Why upload once vs inline base64?

Inline base64

  • Bytes re-uploaded on every multi-target post
  • 33% size overhead from base64 encoding
  • Doesn't work for video, payloads exceed platform caps
  • Slower publish wall-clock on every call
  • No cross-platform variant generation
  • No CDN-backed asset URLs you can reference elsewhere

letmepost Media API

  • Upload once, reference forever (up to retention)
  • S3-backed multipart upload, no base64 overhead
  • Video uploads up to 500 MB, transcoded per platform
  • Publish wall-clock unchanged regardless of media size
  • Per-platform variants generated automatically
  • CDN URL returned for use in your own UI

✓ Upload bytes once, post many times

Bytes hit our S3 bucket once. Subsequent POST /v1/posts calls reference the mediaId. Multi-target posts share the same upload — Bluesky + X + Pinterest publishing the same photo move three platforms' worth of metadata but only one set of bytes.


SUPPORTED TYPES

images, video, and the platform-specific variants we generate
JPEG / PNG / WebP · up to 8 MB
MP4 / MOV video · up to 500 MB
GIF
HEIC → JPEG · auto-converted
Per-platform variants · auto-generated
Alt text

UPLOAD PIPELINE

multipart → s3 → cdn · seconds, not minutes

Client uploads

POST /v1/media with multipart form-data. Single request for files ≤ 100 MB. Larger files use resumable upload with chunked PUTs.

S3 + virus scan

Bytes hit our S3 bucket, ClamAV scans on ingress, content-type sniffed from magic bytes (not the upload's Content-Type header).

Variants generated

Per-platform variants encoded ahead of time. Bluesky 976 KB-capped JPEGs, Pinterest cover frames, IG Reels H.264. Variants come from the source on demand.

Reference on publish

Pass mediaId to POST /v1/posts. We pick the right variant per target. Bytes move once, posts fan out.


FEATURES

things you don't have to build

One upload, many posts

Upload bytes once via POST /v1/media. Reference the returned id from media: [{ mediaId }] on every post that uses it.

Per-platform variants

We pre-encode platform-specific variants (Bluesky JPEG-cap, IG Reels H.264, Pinterest video covers) so the publish path stays fast.

Virus scan on ingress

ClamAV scans every upload before it lands in our active bucket. Failed scans return media_rejected_security with the threat name.

Video transcode pipeline

MP4 → per-platform H.264 variants. Pinterest cover frames extracted automatically. Threads container creation handled at publish time.

30-day retention free

Default 30 days. media.expiring webhook fires 7 days before GC. Pro extends to 90, Business to 365.

CDN-backed asset URLs

Every uploaded asset has a CDN URL — useful for preview UIs in your own dashboard, drag-and-drop reordering, etc.

Reference your uploaded media from the Publishing API.
Publishing API →

CODE EXAMPLE

upload then reference · typescript
upload-and-publish.ts ·
import { Letmepost } from '@letmepost/sdk';
import { readFileSync } from 'node:fs';

const lmp = new Letmepost({ apiKey: process.env.LMP_API_KEY });

// Step 1: upload the bytes (multipart). Bytes only move once.
const media = await lmp.media.upload({
  file: readFileSync('./photo.jpg'),
  kind: 'image',
  altText: 'Receipt-themed landing page',
});
console.log('Got mediaId:', media.id); // 'med_01HXZ4N9...'

// Step 2: reference it from any post that uses it
const result = await lmp.posts.create({
  targets: [
    { platform: 'bluesky',   accountId: 'acc_bsky_xyz' },
    { platform: 'x',         accountId: 'acc_x_xyz' },
    { platform: 'pinterest', accountId: 'acc_pin_xyz' },
  ],
  text: 'Same photo, three platforms, one upload.',
  media: [{ mediaId: media.id }],
});

// Same uploaded bytes are referenced by all three posts.
// Per-platform variants picked automatically.

COMMON QUESTIONS

about uploading media

Can I skip the Media API and inline-base64 small images?

Yes. For images under ~1 MB, inline media: [{ data: "base64...", kind: "image" }] works fine. For video and larger images, use the Media API — inline base64 will hit platform-specific payload caps.

How long are uploads kept?

Default 30 days, free. media.expiring webhook fires 7 days before garbage collection. Pro extends to 90 days, Business to 365.

Does the Media API count toward my post quota?

No. Uploads are free and unlimited. Only successful publishes meter against your quota.

Can I reference the same mediaId on multiple posts?

Yes. Reference forever (up to retention). Multi-target posts share the same upload automatically, bytes only move once.

What size limits apply?

Per upload: 8 MB image, 500 MB video. Platform constraints (e.g. Bluesky's 976 KB per image cap) are enforced at publish time, not upload time, so you can upload high-res once and we re-encode per platform.

How does video transcoding work?

Video uploads are stored as-is plus per-platform variants (H.264 MP4 at platform-specific bitrate caps). Pinterest video pins also get an auto-generated cover frame.

Can I delete an upload?

Yes. DELETE /v1/media/:id. Posts that already published to upstream platforms keep working (those bytes left our system); future references return 404.


OTHER SURFACES

the rest of the api

LEARN MORE


READY TO UPLOAD?

Bytes once. Posts many. Free uploads, unlimited references, 30-day retention on the free tier.

* * * BYTES ONCE · POSTS MANY * * *
SURFACE · MEDIA · /v1/media
→ START FREE