# Why do Instagram image URLs expire with a 403?

Instagram. Checked September 2026.

Instagram signs every media URL with an expiry. A few days after you fetched it, the signature stops matching and the CDN answers 403. The image still exists. Your link to it died. Anything that stored raw Instagram URLs, a website feed, a report, a database, breaks on a schedule.

Openhandle serves media through `media.openhandle.dev`. The URL in the response is ours and does not expire. Store it once and it keeps working.

## GET /v1/instagram/posts/{id}

```bash
curl "https://api.openhandle.dev/v1/instagram/posts/910100000001" \
  -H "Authorization: Bearer $OPENHANDLE_API_KEY"
```

```ts
import { OpenHandle } from '@openhandle/sdk';

const openhandle = new OpenHandle({ apiKey: process.env.OPENHANDLE_API_KEY! });
const { data } = await openhandle.instagram.post('910100000001').get();

console.log(data.media[0]?.url); // https://media.openhandle.dev/…
```

200 OK:

```json
{
  "platform": "instagram",
  "resource": "post",
  "capturedAt": "2026-09-18T11:26:40Z",
  "source": "live",
  "data": {
    "id": "910100000001",
    "media": [
      {
        "type": "image",
        "url": "https://media.openhandle.dev/instagram/910100000001-media",
        "width": 1080,
        "height": 1350
      }
    ]
  }
}
```

Trimmed to the media array. Video posts carry a video asset plus a cover image. Where Instagram publishes several sizes, they appear in `variants`. The example is a Test post.

## What people try, and what happens

- **Re-fetch the post when the image breaks.** Works, but you find out from a user with a broken page, and every stored link needs the same repair.
- **Download every file yourself.** Works. Now you run storage, a CDN, and a copy job for every post you touch.
- **Strip the signature from the URL.** Answers 403 immediately. The signature is the permission.
- **Hotlink the raw Instagram URL in an image tag.** Shows for a few days, then 403. This is where most broken feed widgets come from.

## How Openhandle media works

- **One stable URL per asset.** The response carries a `media.openhandle.dev` link. It serves the same file every time.
- **Served from our cache and CDN.** We fetch the original when you ask, keep it, and serve it from edge locations. Your page never talks to Instagram’s CDN.
- **Renditions when they exist.** Where Instagram publishes multiple sizes, `variants` lists them with width and height. Pick the one your layout needs.
- **Public posts only.** A post from a private account is never fetched, so its media is never served.

## When this does not work

Media follows the post. If the post is gone, so is the media.

| Situation | Status | Code |
|---|---|---|
| The post is deleted or never existed | 404 | `POST_NOT_FOUND` |
| The account is private | 403 | `PROFILE_PRIVATE` |
| The identifier is malformed | 400 | `INVALID_IDENTIFIER` |

```json
{
  "error": {
    "code": "POST_NOT_FOUND",
    "message": "This post was not found.",
    "requestId": "req_01j8…",
    "retryable": false
  }
}
```

A media link for a post that later got deleted keeps serving from our cache until the cache expires. Account owners can ask us to block their profile, and blocked media stops serving.

## Related

- [Public data notice](https://openhandle.dev/legal/public-data-notice): How long we keep answers and media, and how owners can block a profile.
- [UGC use case](https://openhandle.dev/use-cases/ugc): Collect and license user content with stable media links.
- [Instagram API](https://openhandle.dev/apis/instagram): Every Instagram endpoint with examples.
