> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nylon.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Media

> Three ways to get a file into a post, and what Nylon does to it.

Media in Nylon is a **URL**. `POST /v1/posts` takes `media` as a list of links, a social network fetches or receives those bytes, and that is the whole contract. Everything below is about how to get a URL when you do not already have one.

## Three ways in

<CardGroup cols={3}>
  <Card title="You already host it" icon="link">
    Pass the URL straight to `POST /v1/posts`. Nothing is stored by Nylon and nothing is charged for.
  </Card>

  <Card title="Let Nylon copy it" icon="copy">
    `POST /v1/media` with `{ "url": "…" }`. Nylon downloads it once and hands back a URL of its own.
  </Card>

  <Card title="Send the bytes" icon="upload">
    `POST /v1/media` with the file as the body, or `POST /v1/media/uploads` for anything large.
  </Card>
</CardGroup>

### A link you already host

```json theme={null}
{ "profile_ids": ["…"], "text": "Shipping day.", "media": ["https://cdn.example.com/launch.jpg"] }
```

The URL must be reachable without credentials at the moment the post publishes — which for a scheduled post is *later*. That is the one trap, and the next section is how to avoid it.

### A link Nylon copies

```bash theme={null}
curl -X POST https://api.nylon.dev/v1/media \
  -H "Authorization: Bearer nylon_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://s3.example.com/signed/launch.jpg?expires=…"}'
```

```json theme={null}
{
  "data": {
    "url": "https://…public.blob.vercel-storage.com/media/…/launch.jpg",
    "type": "image",
    "content_type": "image/jpeg",
    "size_bytes": 482113,
    "status": "temporary",
    "expires_at": "2026-09-21T10:14:00Z"
  }
}
```

Worth doing whenever the original is signed, expiring, behind a slow origin, or simply not guaranteed to still be there next Tuesday. A post scheduled for next week is fetched next week; a presigned S3 link that expires in an hour will not be.

### Bytes

Post the file with its own content type, for anything up to 4 MB:

```bash theme={null}
curl -X POST https://api.nylon.dev/v1/media \
  -H "Authorization: Bearer nylon_live_YOUR_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @launch.jpg
```

Larger files cannot go through the API at all — a serverless request body is capped in megabytes and a video is not — so they upload straight to storage. That exchange is handled by the `@vercel/blob` client against [`POST /v1/media/uploads`](/api-reference/create-media-upload).

<Note>
  A `data:` URI also works anywhere `media` takes a URL, and is stored the same way. It exists for agents: a model that has just generated an image has bytes, not a link.
</Note>

## What Nylon keeps, and for how long

Two tiers, and the second one is the point:

| State       | When                           | Kept         |
| ----------- | ------------------------------ | ------------ |
| `temporary` | As soon as a file is stored    | 7 days       |
| `permanent` | When a post using it publishes | Indefinitely |

A file you uploaded and never published deletes itself after a week, along with any compressed copies made from it. That is as much about not paying to store files nobody posted as it is about not holding your media longer than needed.

Uploading the same file twice stores it once — assets are addressed by the hash of their contents.

## Compression

Nylon fixes weight, never shape. An image over a network's byte limit is recompressed before publishing: quality first, then dimensions, stopping the moment it fits. A transparent PNG stays a PNG; an opaque one may become a JPEG.

For the networks that fetch media themselves — Instagram, Threads, TikTok, Google Business and DEV — the compressed copy is hosted by Nylon and that URL is what the platform is given. It is made once per file per limit, so a carousel to five profiles compresses once.

What is never changed: **aspect ratio, exact dimensions, video, and animated GIFs.** Each of those would alter what you published rather than just its file size, so they are refused with the real numbers instead. [`POST /v1/validate`](/api-reference/validate-post) reports all of it before anything goes out.
