Published content
Read published articles and their authors and sections.
articles_listpublished:readarticle_readpublished:readauthors_listpublished:readsections_listpublished:readYOUR AI TOOLS. YOUR CONTENT WORKFLOW.
Connect your AI client through MCP. Let it discover stories, help shape drafts, and work across your editorial workflow—with the permissions you choose.
MCP is included in every plan. REST APIs and webhooks come with it.
$ connect https://app.endata.com/mcp
// Your editorial rules travel with your content.
await client.listTools()Conceptual illustration · real requests belowTHE STAR OF YOUR TOOLCHAIN
Move beyond copying text between windows. MCP gives a compatible AI client structured tools: read content, inspect drafts, save revisions, and—with an explicit owner grant—queue publication.
tools/list 1 sample tools visible
YOU ASK
published:read{
"name": "articles_list",
"arguments": {
"publicationId": "PUBLICATION_UUID",
"input": {
"limit": 3
}
}
}Selected examples only, not the full tool list or exact response schemas. Permission sets are illustrative. Tool names and arguments follow the MCP contract; no API key, AI model, or network request is used here.
CONNECT / STREAMABLE HTTP
Connect a compatible MCP client using Streamable HTTP at https://app.endata.com/mcp. Tool schemas come from the same operation registry as the HTTP API.
The app calls this a project. API routes and request fields retain publication and publicationId; use those exact names in code.
Permission-aware discoveryTools outside a key’s scopes are omitted. Every call rechecks authorization.
Explicit write requestsPass publicationId, input, and a requestKey for mutations. Owner grants still apply.
Add this to ~/.codex/config.toml. Make your project API key available as ENDATA_API_KEY in the environment that starts Codex.
[mcp_servers.endata]
url = "https://app.endata.com/mcp"
bearer_token_env_var = "ENDATA_API_KEY"Add this to your project’s .mcp.json. Set ENDATA_API_KEY in the environment before starting Claude Code. Keep the key itself out of configuration files and source control.
{
"mcpServers": {
"endata": {
"type": "http",
"url": "https://app.endata.com/mcp",
"headers": { "Authorization": "Bearer ${ENDATA_API_KEY}" }
}
}
}These examples use the clients’ documented HTTP MCP configuration. Connection and available tools depend on your key’s scopes and project permissions.
// Node.js with @modelcontextprotocol/sdk installed.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from
"@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-endata-tool", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(
new URL("https://app.endata.com/mcp"),
{ requestInit: { headers: {
Authorization: `Bearer ${process.env.ENDATA_API_KEY}`,
} } }
));
try {
const result = await client.callTool({
name: "articles_list",
arguments: {
publicationId: process.env.ENDATA_PUBLICATION_ID,
input: { limit: 20 },
},
});
if (result.isError) throw new Error(JSON.stringify(result.content));
console.log(result.content);
} finally {
await client.close();
}This uses the official MCP TypeScript SDK, not an Endata-specific SDK. Configure bearer credentials through your client’s secure connection settings. API-key connections are supported; personal OAuth and client registration are deferred.
01 / CONNECT YOUR PROJECT
Create a project API key in Settings → Developer → API Keys. Start with published:read and run your first request on a server or in your build environment.
An organization owner creates a key for one project.
Store ENDATA_API_KEY and ENDATA_PUBLICATION_ID privately.
Fetch approved articles and build your reader experience.
// Server-side JavaScript. Keep credentials out of browser bundles.
const base = "https://app.endata.com/api/v1";
const publication = process.env.ENDATA_PUBLICATION_ID;
const headers = { Authorization: `Bearer ${process.env.ENDATA_API_KEY}` };
const response = await fetch(
`${base}/publications/${publication}/articles?limit=20`,
{ headers, cache: "no-store" }
);
if (!response.ok) throw new Error(`Endata returned ${response.status}`);
const { items, editionId, hasMore } = await response.json();
for (const article of items) {
console.log(article.title, article.slug);
}Abbreviated example. UUID placeholders represent real IDs returned by your project.
{
"publication": {
"id": "PUBLICATION_UUID",
"name": "Industry Brief",
"slug": "industry-brief"
},
"editionId": "EDITION_UUID",
"items": [
{
"id": "ARTICLE_UUID",
"slug": "a-new-perspective",
"title": "A new perspective on your industry",
"summary": "The developments that matter, with your editorial context.",
"body": "<p>Reviewed article content.</p>",
"sources": [
{
"title": "Original report",
"url": "https://example.com/report"
}
],
"disclosure": "Prepared with AI assistance and reviewed by the editorial team."
}
],
"hasMore": false
}Keep bearer keys out of browser bundles, public environment variables, URLs, and source control. These examples never ask you to paste a real key into this page.
02 / ACCESS WITH INTENTION
Keys belong to one project and act as service accounts. Choose only the scopes your integration needs. Expiration is optional; revocation is checked on every request.
published:readPublished articles, authors, and sections. Start here for a website reader.
editorial:readRead drafts, revision history, research, media, sharing, and publishing status.
editorial:writeCreate and revise stories; manage research, sources, images, and story actions.
editorial:approveApprove an exact story revision.
publishing:managePublish, retry batches, stage removals, and send or cancel social shares.
administration:manageConfigure project details, research automation, domains, integrations, and webhooks.
organization:manageManage the organization containing the key’s publication.
Scopes, service roles, and plan limits all apply. Consequential actions such as publishing, scheduling, deletion, and administrative changes also require explicit owner unattended grants. Personal OAuth is not available yet.
Publishing, external sharing, and invitations also require the API key creator’s verified email. A 403 with email_unverified means that person must verify their email in the platform before retrying.
03 / THE BUILDING BLOCKS
Explore 51 operations across 10 areas. Every operation uses the same POST route structure. The live catalog contains the exact scopes and input schemas.
Social operations support LinkedIn and Bluesky. Connecting an account first requires interactive consent in Distribution. Newsletter integrations collect subscribers; they do not send email campaigns.
Read published articles and their authors and sections.
articles_listpublished:readarticle_readpublished:readauthors_listpublished:readsections_listpublished:readCreate drafts, inspect history, approve exact revisions, and manage story state.
publication_readeditorial:readoverview_readeditorial:readstories_listeditorial:readstory_readeditorial:readstory_historyeditorial:readstory_createeditorial:writestory_updateeditorial:writestory_previeweditorial:readstory_approveeditorial:approvestory_actioneditorial:writeactivity_listeditorial:readConfigure research, manage sources, inspect runs, and retry candidates.
research_readeditorial:readresearch_runeditorial:writeresearch_configureadministration:manageresearch_source_saveeditorial:writeresearch_retryeditorial:writeRead batch status with editorial:read; publish, retry, or stage removals with publishing:manage.
publishing_readeditorial:readpublication_publishpublishing:managepublication_retrypublishing:managestory_unpublishpublishing:manageList, upload, import, and crop images with reuse permission and attribution.
media_listeditorial:readmedia_uploadeditorial:writemedia_importeditorial:writemedia_cropeditorial:writeManage authors, coverage sections, project details, and custom domains.
taxonomy_saveadministration:managepublication_updateadministration:managedomains_listadministration:managedomain_addadministration:managedomain_removeadministration:manageInspect connections and share stories through LinkedIn and Bluesky. Connect accounts in the platform first.
connections_listadministration:manageconnection_removeadministration:manageshare_readeditorial:readstory_sharepublishing:manageshare_cancelpublishing:manageInspect, configure, test, or remove email, analytics, team notification, and other catalog integrations.
integrations_listadministration:manageintegration_saveadministration:manageintegration_testadministration:manageintegration_removeadministration:manageManage team access, invitations, and projects within plan limits.
team_readorganization:manageteam_inviteorganization:manageinvitation_revokeorganization:managemember_updateorganization:managepublication_createorganization:manageRegister receivers and manage delivery history and replay.
webhooks_listadministration:managewebhook_createadministration:managewebhook_actionadministration:managewebhook_replayadministration:manageAPI / YOUR PRESENTATION LAYER
Use Endata as your editorial backend and build the front end your audience needs. Your website, app, or customer portal can render the same approved articles with its own layout, navigation, and brand.
Perspective
Industry
Research
Sample content · Prepared with AI assistance and reviewed by the editorial team.
Same sample article data, three custom layouts. Your application owns the design, routing, and rendering. This preview does not navigate to real stories.
Bring your own stackFetch on your server or at build time. Render article content, sections, authors, and source references with your own components.
Keep your design freedomThe API supplies content, not a prescribed UI. Preserve disclosure and attribution as you shape the reading experience.
04 / CONTENT THAT STAYS CONSISTENT
Use the article GET routes for content delivery. Pin subsequent pages to the first response’s editionId, then replace your local content only after the entire refresh succeeds.
/api/v1/publications/{publicationId}/articles/api/v1/publications/{publicationId}/articles/{slug}Paginationlimit: 1–100 (default 20). offset: 0–10,000.
Filteringq: up to 200 characters. section: section slug. edition: edition UUID.
// Pin subsequent pages to the first page's edition.
// Reuse base, publication and headers from the quick start.
const collected = [];
let editionId;
let offset = 0;
while (true) {
const query = new URLSearchParams({ limit: "100", offset: String(offset) });
if (editionId) query.set("edition", editionId);
const response = await fetch(
`${base}/publications/${publication}/articles?${query}`,
{ headers, cache: "no-store" }
);
if (!response.ok) throw new Error(`Refresh failed: ${response.status}`);
const page = await response.json();
editionId ??= page.editionId;
collected.push(...page.items);
if (!page.hasMore) break;
offset += page.items.length;
if (!page.items.length || offset > 10000) {
throw new Error("Pagination limit reached; preserve the previous export.");
}
}
// Only replace your previous export after every page succeeds.
// A successful empty edition means the old content should be removed.Preserve sources, disclosure, and image attribution. Resolve image paths against the project’s public website origin, not app.endata.com. Never send an API key with a media request.
Failed refreshes should preserve your previous export. A successful empty edition should remove old content. Older retained editions can remain readable; staged unpublishing is not historical erasure.
05 / MORE THAN A CONTENT FEED
Prepare drafts, save revisions, approve reviewed content, and queue publishing as distinct operations. A write’s request key makes network retries safe without repeating the mutation.
/api/v1/publications/{publicationId}/operations/{operationId}// Server-side operation helper. Reuse a request key only for identical input.
async function operation(name, input, requestKey) {
const response = await fetch(
`${base}/publications/${publication}/operations/${name}`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
"Idempotency-Key": requestKey,
},
body: JSON.stringify(input),
}
);
if (!response.ok) throw new Error(`Operation failed: ${response.status}`);
return response.json();
}
// Use actual IDs from the story you have reviewed.
await operation("story_approve", {
postId: "STORY_UUID",
revisionId: "REVIEWED_REVISION_UUID",
}, "approve-story-revision-001");
// Separate, consequential action: requires publishing:manage
// AND an explicit owner unattended grant for the service key.
await operation("publication_publish", {}, "publish-edition-001");
// Publishing is asynchronous. Poll publishing_read or receive events.This example extends the JavaScript quick start’s base, publication, and headers. Replace UUID placeholders with actual story and revision IDs. A publishing batch includes all eligible approved revisions and staged removals in the publication—not just one story.
Exact revisionsReview the content that will be approved. Handle conflicts by reading and reviewing the latest state.
Asynchronous workResearch and publication return run or batch identifiers. Poll status or consume lifecycle events.
06 / FOLLOW THE EVENT
Walk through approval, publishing, and webhook delivery. Switch scenarios to see why a receiver needs retries and duplicate protection.
Ready. Step through the lifecycle to see how requests become a live edition and a verified event.
Illustrative states, not API response bodies. Assumes the required scopes and owner grants. No network requests. Real deliveries may arrive out of order; retries run on a backoff schedule.
07 / WHEN SOMETHING CHANGES
Register an HTTPS endpoint under Settings → Developer → Webhooks. Choose events, save the signing secret when it is issued, and verify the exact request body before processing.
Webhook-Id Stable event ID · deduplicate hereWebhook-Delivery-Id Delivery ID · changes on manual replayWebhook-Timestamp Unix seconds · allow five minutesWebhook-Signature v1=hex HMAC-SHA256 digestimport { createHmac, timingSafeEqual } from "node:crypto";
// Pass the exact, unmodified UTF-8 request body and request headers.
function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers.get("Webhook-Timestamp");
const signature = headers.get("Webhook-Signature");
if (!timestamp || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
if (!/^v1=[a-f0-9]{64}$/.test(signature || "")) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + "." + rawBody, "utf8")
.digest();
const received = Buffer.from(signature.slice(3), "hex");
return timingSafeEqual(expected, received);
}
// After verification, parse and validate the event.
// Atomically persist it with a unique constraint on Webhook-Id.
// Return 2xx after persistence; duplicate events should also return 2xx.
// Process asynchronously and reconcile current state through the API.At least once, not in orderPersist a unique event ID before responding 2xx. Use the API to reconcile current state.
Retries and replayUp to eight attempts over approximately 22 hours. Inspect 30 days of history and replay deliveries.
Events carry resource IDs and transition metadata, not article bodies. “edition.published” signals actual edition activation. Your receiver must trigger its own build or refresh; registering a webhook does not deploy your website.
08 / READY FOR THE REAL WORLD
Use an 8–160 character Idempotency-Key with letters, numbers, periods, underscores, colons, or hyphens. The same operation and input return the original result for seven days. Changed input with the same key returns 409.
120 integration requests per minute per principal. General JSON requests: 1 MB. HTTP media_upload: up to 14 MB JSON with 10 MB decoded image data. MCP retains the 1 MB limit.
Draft allowances, project counts, seats, and approval rules still apply. Integrations are included in every plan; they do not bypass its limits.
Use a public HTTPS endpoint on port 443. Redirects are refused and delivery times out after ten seconds. Persist promptly, then process asynchronously.
400Validate input against the operation schema.401Check that the key is valid and has not expired or been revoked.403Check scopes, service role, owner grants, and whether the key creator has verified their email.402An existing plan limit was reached.409Resolve a changed revision or an idempotency-key conflict.429Wait until the next minute before retrying.Bring your workflow. We’ll help you find the right starting point.
Talk integrations