Content API
The Content API returns your topics’ content as JSON, in the same order your widgets show it: pinned items first, then the newest posts. Use it to build your own pages, apps or newsletters.
https://q.loop-feed.com/v1/| Endpoint | Auth | What it returns |
|---|---|---|
GET /v1/topic/{topicId}/content | None | A topic’s content |
GET /v1/projects | API key | Your organization’s projects |
GET /v1/project/{projectId}/topics | API key | A project’s active topics |
GET /v1/topics/search | API key | Topics by team, competition or custom field |
GET /v1/topic/{topicId}/count Deprecated | None | A capped item count |
Quick start
Section titled “Quick start”-
Copy a topic’s address from Topic JSON Feed on its Details tab in the dashboard, or list your topics with an API key.
-
Request it:
Terminal window curl "https://q.loop-feed.com/v1/topic/your-topic-id/content?limit=10" -
Show the items. Link each one to its
url, and credit itsauthor. See Branding.
Authentication
Section titled “Authentication”A topic’s content is public: anyone with its topic ID can read it. The endpoints that list or search your
projects and topics need an API key. Create one in the dashboard under
Settings → API Keys and send it in the X-Api-Key header:
X-Api-Key: your-api-keyRequests with a missing or invalid key get 401 with a plain-text body: API key is required or Invalid API key.
1. Get all projects
Section titled “1. Get all projects”GET https://q.loop-feed.com/v1/projectsX-Api-Key: your-api-keyReturns your organization’s projects, in no particular order.
[ { "id": "cmh3y8k2p0001demo5web00001", "name": "Website" }, { "id": "cmh3y8k2p0002demo5app00002", "name": "Mobile App" }]| Property | Type | Description |
|---|---|---|
id | String | The project’s ID |
name | String | The project’s name |
2. Get topics for a project
Section titled “2. Get topics for a project”GET https://q.loop-feed.com/v1/project/{projectId}/topicsX-Api-Key: your-api-keyReturns the project’s active topics, sorted by name. {projectId} can also be the project’s name. A project
that doesn’t exist, or belongs to another organization, returns an empty array.
[ { "id": "cmhujzde8gxd6ncf10epf91dh", "name": "Arsenal", "slug": "arsenal", "url": "https://q.loop-feed.com/v1/topic/cmhujzde8gxd6ncf10epf91dh/content", "metadata": { "sport": "football", "teams": [{ "id": "9", "name": "Arsenal" }], "tournaments": [{ "id": "6409", "name": "England Premier League" }], "teamIds": ["9"], "tournamentIds": ["6409"], "custom": { "desk": "clubs" } } }]| Property | Type | Description |
|---|---|---|
id | String | The topic’s ID |
name | String | The topic’s name |
slug | String | A URL-friendly version of the name |
url | String | The topic’s content address |
metadata | Object or null | See Topic metadata |
Topic metadata
Section titled “Topic metadata”What you set on the topic’s Details tab in the dashboard:
| Property | Type | Description |
|---|---|---|
sport | String | football, rugby (rugby league) or rugby-union |
teams | Array | Teams from Football Association, each { id, name } |
tournaments | Array | Leagues and tournaments from Football Association, each { id, name } |
matches | Array | Matches from Sports Matches, each with matchId, teams, competition and startDateTime |
teamIds | Array | The IDs of teams, plus both teams of every attached match |
tournamentIds | Array | The IDs of tournaments, plus the tournament of every attached match |
matchIds, leagueIds | Array | The IDs of the attached matches and their leagues |
custom | Object | Your Custom Fields. Values are strings or null. |
Team, tournament, league and match IDs are those of LoopFeed’s sports data provider: the same IDs the Feed API and AI curation use.
3. Search topics by metadata
Section titled “3. Search topics by metadata”GET https://q.loop-feed.com/v1/topics/search?teamId=9X-Api-Key: your-api-keyFinds your organization’s topics by team, competition or custom field. Returns the same shape as Get topics for a project.
| Parameter | Description |
|---|---|
teamId | Topics whose teamIds include this team |
tournamentId | Topics whose tournamentIds include this tournament |
leagueId | Same as tournamentId. Ignored when tournamentId is given. |
key and value | Topics whose custom field key equals value exactly. value may be empty but must be present. |
projectId | Only topics of this project |
Give at least one of teamId, tournamentId, leagueId or key. Several filters must all match.
# Topics about a teamGET /v1/topics/search?teamId=9
# Topics about a competitionGET /v1/topics/search?tournamentId=6409
# Topics with a custom field, in one projectGET /v1/topics/search?key=desk&value=clubs&projectId=cmh3y8k2p0001demo5web00001Errors (400):
{"error": "At least one filter is required: teamId, tournamentId/leagueId, or key+value"}{"error": "value is required when key is provided"}
4. Get published content
Section titled “4. Get published content”GET https://q.loop-feed.com/v1/topic/{topicId}/contentReturns the topic’s items: all pinned items first, in your order, then the newest items by publication date. Hidden items are never returned. No API key is needed.
Query parameters
Section titled “Query parameters”| Parameter | Default | Description |
|---|---|---|
limit | 20 | Newest items to return, up to 100. |
offset | 0 | Newest items to skip, for paging. |
since | Only items published at or after this time (Unix seconds). | |
until | Only items published at or before this time (Unix seconds). | |
service | Only one platform: youtube, twitter, instagram or rss. | |
contentType | Only one content type, such as highlights. Needs AI curation. | |
matchId | Only items about this match (its numeric ID). Needs AI curation or matches set in the queue. | |
pinnedOnly | true returns only the pinned items. | |
include | meta wraps the response as { teamIds, tournamentIds, content } with the topic’s team and tournament IDs. |
AI curation filters
Section titled “AI curation filters”With AI curation, every item has a content type and, often, a match. Filter by them:
# Only highlightscurl "https://q.loop-feed.com/v1/topic/your-topic-id/content?contentType=highlights"
# Everything about one matchcurl "https://q.loop-feed.com/v1/topic/your-topic-id/content?matchId=518066"Response
Section titled “Response”[ { "id": "cmh0x1y2z0001abcdpost00001", "externalId": "m0G_UKNoSqY", "service": "youtube", "type": "video", "title": "MADUEKE THE MATCHWINNER! | EXTENDED HIGHLIGHTS | Nottingham Forest vs Arsenal (0-1)", "text": "Noni Madueke's late goal was enough to secure a valuable 1-0 win away to Nottingham Forest...", "url": "https://www.youtube.com/watch?v=m0G_UKNoSqY", "duration": 612, "media": [ { "type": "embed", "embed": "<iframe src='https://www.youtube.com/embed/m0G_UKNoSqY'></iframe>", "url": null, "width": null, "height": null, "duration": 612 }, { "type": "image", "embed": null, "url": "https://i.ytimg.com/vi/m0G_UKNoSqY/maxresdefault.jpg", "width": 1280, "height": 720, "duration": null } ], "language": "en", "date": "2026-10-19T23:00:00Z", "created": "2026-10-19T23:04:12.521Z", "author": { "name": "Arsenal", "userid": "UCpryVRk_VDudG8SHXgWcG0w", "username": "Arsenal", "url": "https://youtube.com/channel/UCpryVRk_VDudG8SHXgWcG0w", "image": null, "verified": true }, "metadata": { "contentType": "highlights", "matches": [ { "sport": "football", "matchId": "518066", "homeTeamId": "11", "homeTeamName": "Nottingham Forest", "awayTeamId": "9", "awayTeamName": "Arsenal", "leagueId": "10001", "leagueName": "England - Premier League", "tournamentId": "6409", "startDateTime": "2026-10-18T15:30:00.000Z", "source": "ai" } ], "entities": [ { "sport": "football", "kind": "team", "id": "9", "name": "Arsenal", "source": "ai" }, { "sport": "football", "kind": "player", "id": "4410", "name": "Noni Madueke", "source": "ai" } ], "custom": {} }, "kudos": { "score": 0 }, "pinned": true, "sortOrder": 1 }]{ "teamIds": ["9"], "tournamentIds": ["6409"], "content": [ { "id": "cmh0x1y2z0001abcdpost00001", "...": "items as in the Response tab" } ]}A topic that doesn’t exist returns an empty array.
Content item schema
Section titled “Content item schema”| Property | Type | Description |
|---|---|---|
id | String | LoopFeed’s ID for the post |
externalId | String | The platform’s own ID, such as the YouTube video ID |
service | String or null | youtube, twitter, instagram or rss |
type | String | video (YouTube), status (X), link (RSS), photo or video (Instagram) |
title | String or null | The title, when the platform has one |
text | String or null | The text or description. May contain HTML such as <br/>. |
url | String | The original post |
duration | Integer or null | Video length in seconds |
media | Array or null | Pictures and players. Each has type (image, photo, video or embed), url, embed (HTML), width, height and duration; unused fields are null. |
language | String | ISO 639-1 language code, such as en |
date | String | When the post was published (RFC 3339, UTC) |
created | String | When LoopFeed stored it |
author | Object | name, userid, username, url, image and verified. url, image and verified may be null. |
metadata | Object or null | contentType, matches, entities and custom, described below |
kudos | Object | Reserved. Its values are currently always 0. |
pinned | Boolean | Whether the item is pinned |
sortOrder | Integer | The pinned item’s position (1 is first); 0 for other items |
metadata holds the item’s labels:
| Property | Description |
|---|---|
contentType | The content type. Present only when set. |
matches | The match the item is about: matchId, both teams, competition and kick-off startDateTime. source is ai (AI curation), review (LoopFeed reviewers) or manual (your editors). Match status and score, when present, are a snapshot from when the match was attached. |
entities | Teams, players and competitions the item is about: sport, kind (team, player or competition), id, name and source. Present only when set. |
custom | The item’s Custom Fields from the queue. |
Caching
Section titled “Caching”Responses are cached for up to a minute, by LoopFeed and its CDN. Changes you make in LoopFeed, such as pinning or
hiding an item, clear them within seconds; new posts appear within a minute. Browsers can keep a response for up to 15
seconds (Cache-Control: public, max-age=15, s-maxage=60, both counting down to when the cached copy expires).
- Requests with the same URL share a cached response. On a page many people open, request the same URL for every
visitor, for example
?limit=20, not one with asinceof each visitor’s own. - Each response has an
ETag. Send it back asIf-None-Matchto get304 Not Modifiedwith no body when nothing changed.
Errors
Section titled “Errors”Invalid parameters return 400 with a JSON message, for example:
{ "error": "invalid limit parameter: must be a valid integer" }| Message | Cause |
|---|---|
invalid since parameter, invalid until parameter, invalid limit parameter, invalid offset parameter | Not a whole number |
invalid contentType parameter | Not a content-type key |
invalid matchId parameter | Not a numeric match ID |
5. Get content count Deprecated
Section titled “5. Get content count ”GET https://q.loop-feed.com/v1/topic/{topicId}/countReturns { "count": 42 }. The count isn’t a total: it’s the number of pinned items plus up to 100 of the newest
items matching service, since and until. It will be removed in a future version. Use the content endpoint
instead.
Keeping up to date
Section titled “Keeping up to date”To pick up new items regularly, poll the content endpoint:
// Every few minutes: fetch what was published since the last run, with an hour's overlapconst since = Math.floor(lastRun / 1000) - 3600;const response = await fetch(`https://q.loop-feed.com/v1/topic/${topicId}/content?since=${since}&limit=100`);const items = await response.json();
for (const item of items) { if (!seen.has(item.id)) { seen.add(item.id); save(item); }}- Overlap and de-duplicate.
sinceuses the time a post was published. Some posts reach LoopFeed a little after they’re published, so overlap your windows and keep only IDs you haven’t seen. - Pinned items come every time. Treat them separately (
pinned: true) or de-duplicate them byid. - Page with
offsetfor more than 100 items, and stop when a page has fewer regular items thanlimit. - Cache responses for at least a minute.
Branding guidelines
Section titled “Branding guidelines”When you show content from social platforms, follow their rules: