Skip to content

REST API v1 · MCP server · Webhooks

Publish Calendone content anywhere.

Calendone writes the articles. Your site, script or AI agent pulls the approved ones and tells Calendone where they went live. Works with any custom platform.

Are you an agent? Read the plain Markdown version: www.calendone.io/developers/api.md

curl "https://www.calendone.io/api/v1/websites/7/articles?status=approved" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"

Get started

Add your site, plan your content in Calendone, then automate publishing with the API or MCP.

Add your site

Create a free account and add your website.

Plan content

Calendone plans topics and writes the articles.

Automate publishing

Create a token under Team → API & MCP and connect your site or agent.

Send the token as Authorization: Bearer YOUR_TOKEN. One token works for every website in your team, and it’s shown only once, so store it safely. Create a free account →

Publishing flow

Run this on a schedule (for example every hour) or let your agent do it.

  1. 1

    GET /websites

    Find your website id.

  2. 2

    GET /websites/{id}/articles?status=approved

    Find articles ready to publish.

  3. 3

    GET /websites/{id}/articles/{article_id}

    Get the full article: HTML, Markdown, meta tags, images.

  4. 4

    Publish it on your site with its slug, meta title and description, body and featured image.

  5. 5

    PUT /websites/{id}/articles/{article_id}/publication

    Send the live URL. The article becomes published and won’t show up as approved again.

If someone edits an article in Calendone after you published it, publication.needs_update becomes true. Fetch it again and update your post.

Article statuses

Only finished articles are returned. Drafts still being written stay inside Calendone.

in_review
Finished, waiting for your approval in Calendone.
approved
Approved and not yet published. This is what you publish.
published
You reported it live with the publication endpoint.
rejected
Rejected in Calendone. Don’t publish it.

Endpoints

Base URL https://www.calendone.io/api/v1. Every request needs Authorization: Bearer YOUR_TOKEN. Responses are JSON and wrap results in data.

Check your token

GET /api/v1/me

Returns the team and token you’re using. Call it first to confirm the token works.

curl "https://www.calendone.io/api/v1/me" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"
Response · 200
{
    "data": {
        "team": {
            "id": 3,
            "name": "Fitalert"
        },
        "token": {
            "name": "fitalert.com blog",
            "abilities": [
                "read",
                "write"
            ],
            "last_used_at": "2026-11-09T08:00:01+00:00"
        }
    }
}

List websites

GET /api/v1/websites

All websites in your team. You need a website id for every article call.

curl "https://www.calendone.io/api/v1/websites" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"
Response · 200
{
    "data": [
        {
            "id": 7,
            "url": "https://fitalert.com",
            "name": "fitalert.com",
            "created_at": "2026-11-01T10:00:00+00:00"
        }
    ]
}

List articles

GET /api/v1/websites/{website_id}/articles

Finished articles for one website, most recently updated first. The list leaves out the article body; fetch one article to get it.

ParameterInTypeDescription
statusquerystringall (default), approved, in_review, published or rejected. Use approved to find what to publish next.
updated_sincequeryISO 8601Only articles changed at or after this time. Good for polling.
pagequeryintegerPage number, default 1.
per_pagequeryinteger1–100, default 20.
curl "https://www.calendone.io/api/v1/websites/7/articles?status=approved" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"
Response · 200
{
    "data": [
        {
            "id": 412,
            "website_id": 7,
            "status": "approved",
            "title": "How to choose running shoes for flat feet",
            "slug": "how-to-choose-running-shoes-for-flat-feet",
            "excerpt": "What actually matters when you have flat feet, and how to test a shoe before you buy it.",
            "language": "en",
            "primary_keyword": "running shoes for flat feet",
            "planned_for": "2026-11-12",
            "revision": 3,
            "replaces_article_id": null,
            "approved_at": "2026-11-09T08:12:44+00:00",
            "created_at": "2026-11-08T21:03:10+00:00",
            "updated_at": "2026-11-09T08:12:44+00:00",
            "publication": null
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 20,
        "total": 1,
        "last_page": 1
    }
}

Get an article

GET /api/v1/websites/{website_id}/articles/{article_id}

One article with everything you need to publish it: HTML and Markdown body, meta title and description, slug and image URLs. Images are hosted by Calendone; download them if your site serves its own.

curl "https://www.calendone.io/api/v1/websites/7/articles/412" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"
Response · 200
{
    "data": {
        "id": 412,
        "website_id": 7,
        "status": "approved",
        "title": "How to choose running shoes for flat feet",
        "slug": "how-to-choose-running-shoes-for-flat-feet",
        "excerpt": "What actually matters when you have flat feet, and how to test a shoe before you buy it.",
        "language": "en",
        "primary_keyword": "running shoes for flat feet",
        "planned_for": "2026-11-12",
        "revision": 3,
        "replaces_article_id": null,
        "approved_at": "2026-11-09T08:12:44+00:00",
        "created_at": "2026-11-08T21:03:10+00:00",
        "updated_at": "2026-11-09T08:12:44+00:00",
        "publication": null,
        "meta_title": "How to choose running shoes for flat feet",
        "meta_description": "What actually matters when you have flat feet, and how to test a shoe before you buy it.",
        "featured_image": {
            "url": "https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp",
            "alt": "Runner tying a stability shoe",
            "width": 1536,
            "height": 1024
        },
        "images": [
            {
                "url": "https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp",
                "alt": "Runner tying a stability shoe",
                "width": 1536,
                "height": 1024
            }
        ],
        "content_markdown": "![Runner tying a stability shoe](https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp)\n\nFlat feet don’t automatically need…\n\n## What support actually means\n…",
        "content_html": "<p><img src=\"https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp\" alt=\"Runner tying a stability shoe\" /></p>\n<p>Flat feet don’t automatically need…</p>\n<h2>What support actually means</h2>\n<p>…</p>"
    }
}

Mark as published

PUT /api/v1/websites/{website_id}/articles/{article_id}/publication

Tell Calendone an approved article is live and where. Calendone then tracks it and moves it to published. Calling it again updates the record, so retries are safe. Needs a token with write access.

ParameterInTypeDescription
urlbodystring, requiredFull live URL of the article.
remote_idbodystringYour own id for the post, up to 191 characters.
published_atbodyISO 8601When it went live. Default: now.
curl -X PUT "https://www.calendone.io/api/v1/websites/7/articles/412/publication" \
  -H "Authorization: Bearer $CALENDONE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://fitalert.com/blog/running-shoes-flat-feet","remote_id":"post_8812"}'
Response · 200
{
    "data": {
        "id": 412,
        "status": "published",
        "…": "…",
        "publication": {
            "url": "https://fitalert.com/blog/running-shoes-flat-feet",
            "remote_id": "post_8812",
            "published_at": "2026-11-12T07:00:00+00:00",
            "needs_update": false
        }
    }
}

Remove publication

DELETE /api/v1/websites/{website_id}/articles/{article_id}/publication

Use this when you took the article down. It goes back to approved. Needs write access.

curl -X DELETE "https://www.calendone.io/api/v1/websites/7/articles/412/publication" \
  -H "Authorization: Bearer $CALENDONE_TOKEN"
Response · 200
{
    "data": {
        "id": 412,
        "status": "approved",
        "…": "…",
        "publication": null
    }
}

MCP server

Let your agent talk to our agent.

Connect Claude Code, Cursor or any MCP client. Your agent can list approved articles, add them to your codebase or CMS, and report the live URL back. Same token as the REST API.

https://www.calendone.io/api/mcp · Streamable HTTP

ToolArgumentsWhat it does
list_websites—Websites in your team.
list_articleswebsite_id, status?, updated_since?, page?, per_page?Same as List articles.
get_articlewebsite_id, article_idSame as Get an article.
mark_publishedwebsite_id, article_id, url, remote_id?, published_at?Same as Mark as published. Needs write access.
Claude Code
claude mcp add --transport http calendone https://www.calendone.io/api/mcp \
  --header "Authorization: Bearer $CALENDONE_TOKEN"
Cursor, Windsurf and other JSON configs
{
    "mcpServers": {
        "calendone": {
            "url": "https://www.calendone.io/api/mcp",
            "headers": {
                "Authorization": "Bearer YOUR_TOKEN"
            }
        }
    }
}

Try asking your agent: “Check Calendone for approved articles on my site, add them to the blog, and mark them published.”

Webhooks

Let Calendone tell your site when to publish.

Instead of polling, add a URL under Team → API & MCP → Webhooks and choose events. Calendone sends a signed POST with the full article, so your site can publish it straight away.

Events

article.ready_to_publish
An approved article reached its planned publish date and isn’t published yet.
article.approved
An article was approved in Calendone.
article.ready_for_review
Calendone finished writing an article and it needs approval.
article.updated
A published article was edited in Calendone and your copy is out of date.

article.ready_to_publish is checked every minute. It fires once per article on its planned date (UTC), or right away if an approved article’s date has already passed. Every event fires once per article; the Send test button sends a ping event.

Request

HeaderValue
Calendone-EventEvent name, e.g. article.ready_to_publish.
Calendone-DeliveryUnique delivery id. Retries reuse it, so you can ignore repeats.
Calendone-TimestampUnix time when the request was signed.
Calendone-Signaturev1= followed by HMAC-SHA256 of "{timestamp}.{raw body}" using your signing secret, hex encoded.
Body
{
    "id": "6f1c2a8e-4b1d-4d1e-9a51-0c7f3b2e9d10",
    "event": "article.ready_to_publish",
    "created_at": "2026-11-12T00:01:04+00:00",
    "team_id": 3,
    "website": {
        "id": 7,
        "url": "https://fitalert.com"
    },
    "data": {
        "article": {
            "id": 412,
            "website_id": 7,
            "status": "approved",
            "title": "How to choose running shoes for flat feet",
            "slug": "how-to-choose-running-shoes-for-flat-feet",
            "excerpt": "What actually matters when you have flat feet, and how to test a shoe before you buy it.",
            "language": "en",
            "primary_keyword": "running shoes for flat feet",
            "planned_for": "2026-11-12",
            "revision": 3,
            "replaces_article_id": null,
            "approved_at": "2026-11-09T08:12:44+00:00",
            "created_at": "2026-11-08T21:03:10+00:00",
            "updated_at": "2026-11-09T08:12:44+00:00",
            "publication": null,
            "meta_title": "How to choose running shoes for flat feet",
            "meta_description": "What actually matters when you have flat feet, and how to test a shoe before you buy it.",
            "featured_image": {
                "url": "https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp",
                "alt": "Runner tying a stability shoe",
                "width": 1536,
                "height": 1024
            },
            "images": [
                {
                    "url": "https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp",
                    "alt": "Runner tying a stability shoe",
                    "width": 1536,
                    "height": 1024
                }
            ],
            "content_markdown": "![Runner tying a stability shoe](https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp)\n\nFlat feet don’t automatically need…\n\n## What support actually means\n…",
            "content_html": "<p><img src=\"https://calendone-io.s3.eu-north-1.amazonaws.com/website-content/7/412/cover.webp\" alt=\"Runner tying a stability shoe\" /></p>\n<p>Flat feet don’t automatically need…</p>\n<h2>What support actually means</h2>\n<p>…</p>"
        }
    }
}

Verify the signature and reply

Check the signature with your signing secret before trusting the request. Answer with any 2xx status. If you publish the article right away, reply with {"url": "…", "remote_id": "…"} and Calendone marks it published, just like the publication endpoint.

$body = $request->getContent();
$timestamp = $request->header('Calendone-Timestamp');
$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, env('CALENDONE_WEBHOOK_SECRET'));

abort_unless(hash_equals($expected, (string) $request->header('Calendone-Signature')), 401);
abort_if(abs(time() - (int) $timestamp) > 300, 401); // reject old requests

$article = json_decode($body, true)['data']['article'];
// ...save the article, then optionally reply with where it went live:
return response()->json(['url' => "https://fitalert.com/blog/{$article['slug']}"]);

Retries

Only HTTPS URLs on the standard port are allowed. Each request times out after 10 seconds, and redirects aren’t followed. If your URL doesn’t answer with 2xx, Calendone retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After 20 failures in a row the webhook is paused, and you can turn it back on in the app.

Errors

Errors return {"error": {"code": "...", "message": "..."}} with a matching HTTP status.

StatusCodeMeaning
401—Missing, wrong or revoked token. Body: {"message": "Unauthenticated."}
403forbiddenThe token is read-only and the endpoint needs write access.
404website_not_found / article_not_foundThe id doesn’t exist or belongs to another team.
409not_approvedOnly approved articles can be marked published.
422invalid_status / invalid_url / invalid_dateA parameter has the wrong format. The message says which.
429—More than 60 requests a minute. Wait for the Retry-After header and try again.

Limits and versioning

  • 60 requests per minute.
  • v1 only gets additive changes: new fields or endpoints. Ignore fields you don’t know.
  • Questions or a missing endpoint? Tell us.