API v1 · Development contract
Integrate a feed. Keep every revision in sync.
This describes the current development candidate. Public API access is not enabled. Examples are synthetic and cannot be published as real news. Commercial coverage and licensing remain subject to launch qualification.
Use a server-side integration. Keep the API key in your secret store, outside browser code, public logs and source control. Each key belongs to one organization; each feed and cursor is scoped to that organization.
Start with a complete snapshot
GET /api/v1/feeds/{feed_id}/snapshot
Authorization: Bearer <server-api-key>Start without a cursor. The first response pins a complete feed view to one snapshot and coverage revision. Receive and stage every page before changing which existing articles are available.
# Set CONTENTFEED_BASE_URL, CONTENTFEED_FEED_ID and CONTENTFEED_API_KEY
# in your server environment. Use the HTTPS origin supplied for your feed.
set -eu
umask 077
CONTENTFEED_OUTPUT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/contentfeed-response.XXXXXXXX")"
printf "Response directory: %s\n" "$CONTENTFEED_OUTPUT_DIR"
curl --disable --proto =https --connect-timeout 5 --max-time 10 --fail-with-body --silent --show-error --config - <<EOF
url = "${CONTENTFEED_BASE_URL}/api/v1/feeds/${CONTENTFEED_FEED_ID}/snapshot"
header = "Authorization: Bearer ${CONTENTFEED_API_KEY}"
output = "${CONTENTFEED_OUTPUT_DIR}/snapshot-page.json"
EOFThe command supplies the key through standard input rather than a command-line argument. It creates a fresh private temporary directory for the response and prints that directory’s path. Keep the response private and remove it when finished. Do not enable shell tracing or curl verbose output with real credentials. These commands fetch one page; they are not a complete importer.
- Validate the complete response, every article and its organization/feed metadata before storing a page. Keep snapshot staging separate from the visible article set.
- While
has_moreis true, passnext_snapshot_cursoras thecursorquery parameter to the same snapshot endpoint. Keep the samesnapshot_idandcoverage_revisionthroughout the run. - An empty
itemsarray can still have another page. Pagination scans at most 50 article heads per request, including articles excluded by filters. - Only the terminal page has a
resume_cursor. In one transaction, apply staged versions, mark previously associated articles absent from this completed snapshot as unavailable in this feed, save the resume cursor, and clear staging. - Continue with incremental changes. A snapshot captures a fixed point; changes committed after that point arrive through the incremental endpoint.
An interrupted snapshot must leave the existing visible set intact. Persist staging and progress so another worker can resume. Use a generation or equivalent transaction fence so a late worker cannot commit an obsolete page.
Read feed changes
GET /api/v1/feeds/{feed_id}/items?cursor={saved_cursor}
Authorization: Bearer <server-api-key># Set CONTENTFEED_CURSOR to the terminal resume_cursor, then next_cursor.
# Keep it unchanged until its page commits successfully.
set -eu
umask 077
CONTENTFEED_OUTPUT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/contentfeed-response.XXXXXXXX")"
printf "Response directory: %s\n" "$CONTENTFEED_OUTPUT_DIR"
curl --disable --proto =https --connect-timeout 5 --max-time 10 --fail-with-body --silent --show-error --get --config - <<EOF
url = "${CONTENTFEED_BASE_URL}/api/v1/feeds/${CONTENTFEED_FEED_ID}/items"
header = "Authorization: Bearer ${CONTENTFEED_API_KEY}"
data-urlencode = "cursor=${CONTENTFEED_CURSOR}"
output = "${CONTENTFEED_OUTPUT_DIR}/changes-page.json"
EOFCommit every validated page and its next_cursor in the same transaction. Continue until has_more is false, then poll again later with that saved cursor. An empty page still advances progress. A request without a cursor replays the retained change journal; it does not replace complete reconciliation.
Use the stable article ID as your key. Apply newer versions, even if only publication approval changed. An equal version with identical content is a retry; an equal version with different content is a conflict to investigate. Never let a late response overwrite a newer version.
Distribution and public approval
Managed article IDs use pub_ followed by 32 lowercase hexadecimal characters and require publisher_delivery metadata. Check its organization_id and feed_id against your server configuration. Reject missing or mismatched metadata for managed IDs.
Article status: published means released to the feed. It does not by itself authorize public publication. Keep the article private until that publisher’s exact manuscript version has publication_state: public, a publication_decision_id and public_at. Use the exact reviewed source text; local edits cannot inherit that decision.
A newer private or withdrawn decision removes earlier public approval. A source or manuscript correction can return a story to review. Preserve its ID and audit history, and apply the newer decision even when the words are unchanged.
Source and text permissions
Fictional examples carry synthetic: true and rights.license_id: synthetic-demo-only. They have no real-source metadata and must never appear as real reporting.
Observed match reports carry synthetic: false, rights.license_id: publisher-approved-text-v1 and attribution Match data: API-Tennis. Their source contains only provider: api-tennis, a numeric-string event_key and a positive integer revision. This source revision differs from the article version, which also advances for editorial decisions.
The text permission identifies the reviewed copy for the named publisher and feed. It does not transfer raw provider data or authorize resale of the feed. Public display still requires the separate publisher decision above. Keep the data attribution and clearly disclose AI-assisted reporting. Commercial terms and account activation are completed separately before launch.
Reject mixed source declarations, missing real-source metadata and real articles without managed publisher decisions. Private capture hashes, provider responses and internal review records are not part of this API.
Removals, withdrawals and returning articles
Apply a remove action immediately. If the article remains published, it has left this feed’s coverage: mark its feed association unavailable and preserve its copy, identity and editorial history. If its status is retracted, remove the public text and retain a truthful withdrawal notice.
Absence from a completed snapshot also changes availability, not editorial approval. If the same approved version returns to coverage, its original ID and URL can become available again. Never infer absence from one partial or failed page, or delete an unrelated feed’s association.
Filtering
Owners can choose players, player countries and competitions in the workspace. Values within one group combine with OR; populated groups combine with AND. An empty group adds no restriction. Player country means representation in the event, not tournament location or article language.
Coverage changes keep the same feed ID and advance its revision. Existing snapshot and incremental cursors then return 410. Reconcile a new complete snapshot to apply the new coverage; changing back to an earlier selection still requires a fresh snapshot. An unchanged save does not create another revision.
Errors and recovery
Error responses contain an error code and are not cacheable. Keep cursor values opaque: do not parse, edit, combine or transfer them between feeds or endpoints.
- 400 · invalid_cursor
- The cursor is malformed, for another organization/feed, or for the wrong endpoint. Check stored state and configuration; do not delete local articles.
- 401 · unauthorized
- The key is missing, invalid or revoked. Pause polling until valid credentials are configured. Keep articles and import progress.
- 404 · feed_not_found
- The feed is unknown or outside the key’s organization. Verify the feed/key pair without exposing credentials.
- 410 · cursor_expired or coverage_changed
- Start a new snapshot generation. Discard only obsolete staging, preserve the currently visible articles, and reconcile absence only after the new snapshot completes. Tokens expire after seven days.
- 429 · quota_exceeded
- Respect the
Retry-Afterdelay in seconds. The current quota counts successful feed requests per organization per UTC day, including snapshot requests. It is not a published commercial billing unit. - 503 · service_unavailable, or a timeout
- Keep the last committed cursor and staging. Retry with bounded exponential backoff and jitter. After the attempt limit, alert your operator and leave the request recoverable.
Do not follow redirects while sending a feed credential. Successful responses have Cache-Control: private, no-store. A successful retry can consume another request from the daily quota even if your importer recognizes the page as already applied.
Complete response examples
These examples include the article fields in the development schema. All IDs, decision references, hashes and cursor strings are illustrative. The cursors are not signed tokens and cannot be sent to a live endpoint. The synthetic-only license shown here grants no right to represent these examples as real news.
An empty, nonterminal snapshot page
{
"kind": "snapshot",
"snapshot_id": "22222222-2222-4222-8222-222222222222",
"coverage_revision": 3,
"items": [],
"has_more": true,
"next_snapshot_cursor": "illustrative-snapshot-continuation",
"resume_cursor": null
}A terminal snapshot with one private article
{
"kind": "snapshot",
"snapshot_id": "22222222-2222-4222-8222-222222222222",
"coverage_revision": 3,
"items": [
{
"action": "upsert",
"article": {
"id": "pub_0123456789abcdef0123456789abcdef",
"version": 1,
"event_id": "evt_example_001",
"sport": "tennis",
"type": "match_report",
"language": "en",
"status": "published",
"title": "Synthetic example: Alex Vale wins in straight sets",
"summary": "Alex Vale beat Jamie North 6-4, 6-3 in a fictional match.",
"search_description": "A fictional match report used to illustrate the API response.",
"body_text": "Alex Vale beat Jamie North 6-4, 6-3 in a fictional match at the Example Open.\n\nThis is synthetic documentation content, not a real news report.",
"athlete_ids": [
"ath_alex_vale",
"ath_jamie_north"
],
"athlete_countries": [
"LT",
"GB"
],
"entity_refs": {
"competition": {
"id": "comp_example_open",
"name": "Example Open"
},
"athletes": [
{
"id": "ath_alex_vale",
"name": "Alex Vale"
},
{
"id": "ath_jamie_north",
"name": "Jamie North"
}
]
},
"competition_id": "comp_example_open",
"event_country": null,
"published_at": "2026-09-07T10:00:00.000Z",
"updated_at": "2026-09-07T10:00:00.000Z",
"correction_note": null,
"synthetic": true,
"rights": {
"license_id": "synthetic-demo-only",
"attribution_text": "Synthetic documentation example"
},
"publisher_delivery": {
"schema_version": 1,
"organization_id": "org_example",
"feed_id": "feed_example",
"manuscript_revision": 1,
"text_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"decision_id": "11111111-1111-4111-8111-111111111111",
"distribution_decision_id": "11111111-1111-4111-8111-111111111111",
"publication_decision_id": null,
"publication_state": "private",
"public_at": null
}
}
}
],
"has_more": false,
"next_snapshot_cursor": null,
"resume_cursor": "illustrative-delta-cursor"
}A later public decision for the same article
{
"items": [
{
"action": "upsert",
"article": {
"id": "pub_0123456789abcdef0123456789abcdef",
"version": 2,
"event_id": "evt_example_001",
"sport": "tennis",
"type": "match_report",
"language": "en",
"status": "published",
"title": "Synthetic example: Alex Vale wins in straight sets",
"summary": "Alex Vale beat Jamie North 6-4, 6-3 in a fictional match.",
"search_description": "A fictional match report used to illustrate the API response.",
"body_text": "Alex Vale beat Jamie North 6-4, 6-3 in a fictional match at the Example Open.\n\nThis is synthetic documentation content, not a real news report.",
"athlete_ids": [
"ath_alex_vale",
"ath_jamie_north"
],
"athlete_countries": [
"LT",
"GB"
],
"entity_refs": {
"competition": {
"id": "comp_example_open",
"name": "Example Open"
},
"athletes": [
{
"id": "ath_alex_vale",
"name": "Alex Vale"
},
{
"id": "ath_jamie_north",
"name": "Jamie North"
}
]
},
"competition_id": "comp_example_open",
"event_country": null,
"published_at": "2026-09-07T10:00:00.000Z",
"updated_at": "2026-09-07T10:05:00.000Z",
"correction_note": null,
"synthetic": true,
"rights": {
"license_id": "synthetic-demo-only",
"attribution_text": "Synthetic documentation example"
},
"publisher_delivery": {
"schema_version": 1,
"organization_id": "org_example",
"feed_id": "feed_example",
"manuscript_revision": 1,
"text_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"decision_id": "33333333-3333-4333-8333-333333333333",
"distribution_decision_id": "11111111-1111-4111-8111-111111111111",
"publication_decision_id": "33333333-3333-4333-8333-333333333333",
"publication_state": "public",
"public_at": "2026-09-07T10:05:00.000Z"
}
}
}
],
"next_cursor": "illustrative-next-delta-cursor",
"has_more": false
}This version changes the publisher decision without changing the text. A content correction follows the same stable-ID, newer-version rule and can carry a correction_note.
A subsequent withdrawal
{
"items": [
{
"action": "remove",
"article": {
"id": "pub_0123456789abcdef0123456789abcdef",
"version": 3,
"event_id": "evt_example_001",
"sport": "tennis",
"type": "match_report",
"language": "en",
"status": "retracted",
"title": "Synthetic example withdrawn",
"summary": "This fictional example has been withdrawn.",
"search_description": "A fictional match report used to illustrate the API response.",
"body_text": "This fictional example has been withdrawn.",
"athlete_ids": [
"ath_alex_vale",
"ath_jamie_north"
],
"athlete_countries": [
"LT",
"GB"
],
"entity_refs": {
"competition": {
"id": "comp_example_open",
"name": "Example Open"
},
"athletes": [
{
"id": "ath_alex_vale",
"name": "Alex Vale"
},
{
"id": "ath_jamie_north",
"name": "Jamie North"
}
]
},
"competition_id": "comp_example_open",
"event_country": null,
"published_at": "2026-09-07T10:00:00.000Z",
"updated_at": "2026-09-07T10:10:00.000Z",
"correction_note": "Synthetic withdrawal demonstration.",
"synthetic": true,
"rights": {
"license_id": "synthetic-demo-only",
"attribution_text": "Synthetic documentation example"
},
"publisher_delivery": {
"schema_version": 1,
"organization_id": "org_example",
"feed_id": "feed_example",
"manuscript_revision": 1,
"text_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"decision_id": "44444444-4444-4444-8444-444444444444",
"distribution_decision_id": "11111111-1111-4111-8111-111111111111",
"publication_decision_id": null,
"publication_state": "withdrawn",
"public_at": null
}
}
}
],
"next_cursor": "illustrative-withdrawal-delta-cursor",
"has_more": false
}