# ElectricBlaze feed schema v1: one JSON shape for social posts

**Status, checked 2026-09-18:** schema v1 is what `npx electricblaze add` writes today (as demo data) and what the 0.2 JSON API will return. The widget embed uses its own internal data, shown at the end of this page.

> The contract between ElectricBlaze and your component. Render `type`, filter by `format`, and never parse a platform payload in your UI.

Canonical: https://electricblaze.com/developer/instagram-feed/schema.html

## At a glance

- **Use it when:** you write a component that renders social posts and want one shape for posts, reels and carousels, with the rendering decision in a single field.
- **Do not use it when:** you consume the raw Instagram API yourself. Its envelope is different; see [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).
- **What the human does:** nothing. The schema, the types and the sample are public files.
- **Next step:** download [schema.json](https://electricblaze.com/developer/instagram-feed/schema.json) and [sample-feed.json](https://electricblaze.com/developer/instagram-feed/sample-feed.json), or run `npx electricblaze@0.1.2 preview instagram --json`.

## The shape in thirty seconds

A feed is an origin (the account) plus a list of posts. Every post carries what a renderer needs as required fields. Anything specific to one platform stays in `raw`.

lib/eb/feed.d.ts (excerpt):

```ts
interface Feed {
  schemaVersion: 1;
  source: "instagram" | "youtube" | "tiktok" | (string & {});
  origin: { kind: "profile" | "hashtag" | "playlist" | "board" | "channel";
            id: string; name: string; displayName: string; url: string; avatarUrl?: string };
  posts: Post[];
  fetchedAt: string;      // ISO 8601
  demo: boolean;          // true until an account is connected
  nextCursor?: string;
}

interface Post {
  id: string; source: string;
  type: "image" | "video" | "carousel" | "text" | "link";   // what to render
  format: string;                                           // "post" | "reel" on Instagram
  status: "published" | "live" | "upcoming";
  url: string;            // permalink on the platform
  text: string;           // caption, may be empty
  media: { url: string; mime: string; width?: number; height?: number; alt?: string; durationSec?: number }[];
  thumbnailUrl?: string;  // required for image, video and carousel
  aspectRatio?: number;   // width / height: 1, 0.8, 0.5625, 1.7778
  durationSec?: number;
  publishedAt: string;
  stats?: { likes?: number; comments?: number; views?: number; shares?: number };
  raw?: unknown;          // original platform payload, untouched
}
```

The full file with comments is [feed.d.ts on GitHub](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/feed.d.ts). The CLI copies it into your project as `lib/eb/feed.d.ts`.

## type decides layout, format decides badges

Two fields describe a post, and they answer different questions. `type` tells the component which element to draw. `format` repeats what the platform calls the post, so you can filter and label it.

| type | format | Instagram calls it | Render | Posts in the sample |
|---|---|---|---|---|
| `image` | `post` | Photo post | `<img src={thumbnailUrl}>` | 6 |
| `video` | `reel` | Reel | Poster from `thumbnailUrl`, "Reel" badge, 9:16 | 2 |
| `video` | `post` | Video in the feed | Poster from `thumbnailUrl`, play badge | 2 |
| `carousel` | `post` | Album (`CAROUSEL_ALBUM`) | First item, count badge from `media.length` | 2 |

Layout code should switch on `type` only. A new platform format, such as a YouTube `short`, then renders correctly without a code change. `thumbnailUrl` is always present for the three visual types, so a grid never needs to inspect `media`.

## One post, as JSON

A reel from `sample-feed.json`. Its media file is vertical, so `aspectRatio` is 0.5625 (9:16).

posts[1] of sample-feed.json:

```json
{
  "id": "demo-ig-02",
  "source": "instagram",
  "type": "video",
  "format": "reel",
  "status": "published",
  "url": "https://electricblaze.com/demo/instagram#post-02",
  "text": "How we cup a new lot in under 60 seconds.",
  "media": [
    {
      "url": "https://cdn.jsdelivr.net/gh/ElectricBlaze/electricblaze@89e2819/demo/media/reel-01.mp4",
      "mime": "video/mp4",
      "width": 540,
      "height": 960,
      "durationSec": 10
    }
  ],
  "thumbnailUrl": "https://cdn.jsdelivr.net/gh/ElectricBlaze/electricblaze@89e2819/demo/media/reel-01.jpg",
  "aspectRatio": 0.5625,
  "durationSec": 10,
  "publishedAt": "2026-09-10T16:05:00Z",
  "stats": { "likes": 1287, "comments": 64, "views": 18450 }
}
```

The demo posts are synthetic: a coffee roaster with twelve posts, photos under 1:1, 4:5, 9:16 and 16:9, two reels, two videos and two carousels. The media files are licensed from Pexels and Blender Foundation projects and served from jsDelivr, pinned to a commit.

## Files you can use

| File | What it is | Use it for |
|---|---|---|
| [schema.json](https://electricblaze.com/developer/instagram-feed/schema.json) | JSON Schema, draft 2020-12, `$id` on this site | Validation in CI, form generators, other languages |
| [sample-feed.json](https://electricblaze.com/developer/instagram-feed/sample-feed.json) | The 12-post demo feed, byte for byte the file the CLI ships | Fixtures, Storybook, design work |
| [feed.d.ts](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/feed.d.ts) | TypeScript types with comments | Import as `import type { Feed } from "./lib/eb/feed"` |
| [validate.js](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/validate.js) | Dependency-free validator used by `doctor` | Runtime checks without a schema library |

Validate a file with Ajv, which supports draft 2020-12:

```sh
curl -sO https://electricblaze.com/developer/instagram-feed/schema.json
npx -p ajv-cli@5 -p ajv-formats ajv validate --spec=draft2020 -c ajv-formats -s schema.json -d lib/eb/demo/instagram.json
# lib/eb/demo/instagram.json valid
```

We ran this on 2026-09-18: the sample passes, and a post with `thumbnailUrl` removed fails with `must have required property 'thumbnailUrl'`.

Or let the CLI do it: `npx electricblaze doctor --json` validates whichever feed file it finds in the project and names the field that fails.

## How Instagram fields map onto it

This is how an Instagram post becomes a schema v1 post. Field details and their caveats are on [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).

| Instagram field | Schema v1 | Note |
|---|---|---|
| `media_type` IMAGE / VIDEO / CAROUSEL_ALBUM | `type` image / video / carousel | Always present |
| `media_product_type` REELS | `format: "reel"` | Documented for Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18) |
| `thumbnail_url`, else `media_url` | `thumbnailUrl` | `thumbnail_url` exists only on videos |
| `media_url` | `media[0].url` | Left out for copyright-flagged media |
| `children{media_type,media_url,thumbnail_url}` | `media[]` | Only when expanded in `fields=` |
| `permalink` | `url` |  |
| `caption` | `text` | Empty string when absent |
| `timestamp` | `publishedAt` | Normalized to ISO 8601 with Z |
| `like_count`, `comments_count` | `stats.likes`, `stats.comments` | Optional |

## Two shapes you may meet today

The ElectricBlaze widget predates schema v1. Its script loads the Graph API response as it comes, and your code never sees it. We list it so that an agent inspecting network traffic does not mistake it for the public format.

|  | Widget data (internal) | Schema v1 (public contract) |
|---|---|---|
| Envelope | `{"data": [...], "paging": {...}}` | `{"schemaVersion": 1, "origin": {...}, "posts": [...]}` |
| Post fields | `id, caption, media_type, media_url, permalink, timestamp, username` | See above |
| Reels | Not marked: a reel is a `VIDEO` | `format: "reel"` when the source provides it |
| Carousel items | Not expanded | `media[]` |
| Address | Signed URL, minted per page load, expires within hours (observed 2026-09-17) | Your file today; the 0.2 API later |

Code against schema v1 only. Breaking changes will raise `schemaVersion`; fields a renderer does not know should be ignored, not rejected.

## FAQ

### Why both type and format?

`type` is closed and drives layout: five values that every renderer handles. `format` is open and repeats the platform term, so a new format never breaks a grid.

### Is thumbnailUrl always there?

For `image`, `video` and `carousel` posts, yes; the schema makes it required for those types. Text and link posts may omit it.

### Can I extend the schema?

Do not edit `feed.d.ts`; it is the contract. Wrap the post in your own type or read extra data from `raw`.

### Does the widget return this format?

No. The widget renders its own data inside the page. Schema v1 is what the CLI writes and what the 0.2 JSON API will serve.

### Where is the JSON Schema hosted?

At `https://electricblaze.com/developer/instagram-feed/schema.json`, which is also its `$id`.

## Related pages

- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Agent skill](https://electricblaze.com/developer/instagram-feed/skill.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/)

---

ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/schema.html
