Developer docs
Zumika has three surfaces: an embed widget for showing a feed, a public JSON feed for building your own front end, and a REST API for managing feeds, sources and posts.
Embed widget
Place a container with the feed’s public key and load the script once per page. The widget renders into the page itself (no iframe), so it inherits your fonts and can be styled with CSS.
<div data-zumika-feed="FEED_PUBLIC_KEY"></div> <script async src="https://zumika.limegreentangerine.net/embed.js"></script>
Design settings are saved against the feed in the dashboard. Override them per placement with attributes:
- data-zumika-layout
- waterfall, grid, carousel, reel (portrait carousel) or list. Ignored when the feed is split into sections.
- data-zumika-limit
- Posts per load, 1 to 50
- data-zumika-manual
- Skip automatic rendering; call Zumika.render() yourself
JavaScript API:
const wall = Zumika.render("#wall", {
feed: "FEED_PUBLIC_KEY",
layout: "grid", // optional override
limit: 9, // optional override
settings: { colors: { accent: "#0a66c2" }, radius: 0 },
});
wall.loadMore(); // fetch the next page
Zumika.init(); // render any new [data-zumika-feed] elements
el.addEventListener("zumika:loaded", (e) => e.detail.posts);
el.addEventListener("zumika:post-click", (e) => {
e.preventDefault(); // cancel the popup and handle e.detail.post yourself
});Every element carries a class starting zumika- (.zumika-card, .zumika-text, .zumika-btn). Colours are CSS variables on .zumika: --zc card, --zt text, --zm secondary text, --za accent, --zb border.
Authentication
The REST API is HTTPS only. Create an API key under API & plugins in a workspace and send it as a Bearer token. A key gives full access to that workspace, so keep it on the server and never in browser code.
curl https://zumika.limegreentangerine.net/api/v1/feeds \ -H "Authorization: Bearer zumika_xxxxxxxxxxxxxxxx"
The key is also accepted as an X-API-Key header or an api_key query parameter. Request bodies are JSON. Wherever a feed ID is expected, the feed’s public key works too.
Feeds
GET/api/v1/feeds
List the workspace’s feeds with their post counts.
POST/api/v1/feeds
Create a feed.
- name
- Required. Shown only in the dashboard.
- moderation
- auto (default) or manual. Manual holds new posts as pending.
GET/api/v1/feeds/{id}
One feed, including its widget settings.
PATCH/api/v1/feeds/{id}
Update a feed. Send only what should change.
- name
- New name
- moderation
- auto or manual
- settings
- Object of widget settings: layout, sections, columnWidth, gap, radius, perPage, loadMore, click, showAuthor, showText, showStats, showDate, showNetwork, textLines, colors, customCss, autoplay (0, 3, 5 or 8 seconds), arrows, header (show, title, text, buttonLabel, buttonUrl), ratingSummary
DELETE/api/v1/feeds/{id}
Delete a feed with its sources and posts. This cannot be undone.
Sections
settings.sections splits a wall into rows, each with its own layout and filter: a Shorts carousel, a Vimeo carousel, then everything else. Featured sections are shown first and highlighted. A section with no posts is hidden. With an empty array the wall shows every post in settings.layout.
{
"settings": {
"sections": [
{ "title": "Shorts", "layout": "reel", "featured": true, "sourceIds": ["SOURCE_ID"], "perPage": 10 },
{ "title": "Latest videos", "layout": "carousel", "networks": ["youtube", "vimeo"] },
{ "title": "Everything else", "layout": "waterfall" }
]
}
}- title
- Heading shown above the row. Optional.
- layout
- waterfall, grid, carousel, reel or list
- featured
- true to show the section first, highlighted
- networks
- Network keys to include. Empty means all.
- sourceIds
- Source IDs to include. Empty means all.
- perPage
- Posts per load, 1 to 50
{
"success": true,
"feeds": [
{
"id": "cmf2k1x9a0001",
"name": "Homepage wall",
"public_key": "h3Jq9sLm2Xw_",
"moderation": "manual",
"created_at": "2026-11-13T10:24:00.000Z",
"post_count": 184
}
]
}Sources
GET/api/v1/feeds/{id}/sources
List a feed’s sources, with the time and any error from the last fetch.
POST/api/v1/feeds/{id}/sources
Add a source. Zumika fetches it straight away and reports how many posts were added.
- type
- Required. A source type from the table below.
- tag
- The hashtag, username, URL or ID to collect. Not needed for account-level connected sources.
- connection_id
- Required for types that need a connected account.
- interval_minutes
- 15, 30, 60 (default), 180, 720 or 1440
DELETE/api/v1/sources/{id}
Remove a source and the posts collected from it.
GET/api/v1/source-types
The table below as JSON. No key needed.
| type | Network | tag |
|---|---|---|
| instagram_account | Uses connection_id | |
| instagram_hashtag | Hashtag + connection_id | |
| instagram_mentions | Uses connection_id | |
| facebook_page | Uses connection_id | |
| x_user | X / Twitter | Username |
| x_hashtag | X / Twitter | Hashtag or search query |
| x_mentions | X / Twitter | Username |
| tiktok_account | TikTok | Uses connection_id |
| linkedin_company | Uses connection_id | |
| youtube_channel | YouTube | Channel ID or @handle |
| youtube_videos | YouTube | Channel ID or @handle |
| youtube_shorts | YouTube | Channel ID or @handle |
| youtube_playlist | YouTube | Playlist ID |
| youtube_search | YouTube | Search query |
| google_reviews | Google Reviews | Place ID |
| reddit_subreddit | Subreddit | |
| reddit_search | Search query | |
| bluesky_user | Bluesky | Handle |
| bluesky_hashtag | Bluesky | Hashtag or search query |
| mastodon_hashtag | Mastodon | instance#hashtag |
| mastodon_user | Mastodon | @user@instance |
| flickr_tag | Flickr | Tag |
| flickr_user | Flickr | User ID (NSID) |
| vimeo_user | Vimeo | Username |
| vimeo_search | Vimeo | Search query |
| tumblr_blog | Tumblr | Blog name |
| tumblr_tag | Tumblr | Tag |
| pinterest_user | Username | |
| pinterest_board | username/board-name | |
| rss_feed | RSS | Feed URL |
Posts
GET/api/v1/feeds/{id}/posts
List posts, pinned first, then newest first.
- status
- approved (default), pending, rejected or all
- network
- Filter by network key, for example instagram
- source_id
- Filter by source
- limit
- 1 to 100, default 25
- offset
- Number of posts to skip
PATCH/api/v1/posts/{id}
Moderate a post.
- status
- approved, pending or rejected
- pinned
- true or false
- text
- Replacement text, or null to restore the original
{
"success": true,
"total": 184,
"has_more": true,
"posts": [
{
"id": "cmf2k4b7c00a3",
"network": "instagram",
"url": "https://www.instagram.com/p/…",
"text": "Golden hour on the quay.",
"author": { "name": "harbourcafe", "handle": "@harbourcafe", "avatar": "https://…", "url": "https://…" },
"media_type": "image",
"image": "https://zumika.limegreentangerine.net/media/…/….webp",
"image_width": 1080,
"image_height": 1350,
"video": null,
"likes": 212,
"comments": 14,
"views": null,
"rating": null,
"posted_at": "2026-11-12T17:02:11.000Z",
"status": "approved",
"pinned": false,
"source_id": "cmf2k2p1m0007"
}
]
}media_type is none, image, video (a playable file in video) or embed (a player URL in video, for YouTube, Vimeo and TikTok).
Public feed JSON
GET/api/v1/public/feeds/{public_key}/posts
What the widget itself calls. No API key, CORS open, approved posts only. Use it to build a custom front end. Responses are cached for 60 seconds.
- limit
- 1 to 50, default 12
- offset
- Number of posts to skip
- networks
- Comma-separated network keys, for example youtube,vimeo
- sources
- Comma-separated source IDs
Returns feed (name and widget settings), total, has_more and posts in the shape above.
Errors
Errors return { "success": false, "error": "…" } with one of these statuses:
- 401
- Missing, wrong or revoked API key
- 404
- The feed, source or post does not exist in this workspace
- 422
- A value was missing or not allowed; the message says which
- 500
- Something failed on our side; try again