Send articles to your own endpoint from Growth Calendar
When your site is not one of the connected CMSs, Growth Calendar can POST each finished article to an address you control. Your receiver decides what to do with it: commit it to a repository, write it into your own database, hand it to a static site generator.
Articles are sent as Markdown, so nothing has to be un-rendered on your side.
There is one setting for the whole workspace, under CMS in the top right of Growth Calendar.
What you need
An endpoint that accepts POST over https, and a secret you invent. The secret is sent with every request so your receiver can tell Growth Calendar apart from anyone who learns the URL.
Connecting
- Open CMS in the top right of Growth Calendar and select Webhook.
- Enter the full endpoint URL, path included, such as
https://example.com/api/articles. - Enter the shared secret and select Connect.
Connecting sends one test request to the address, so the receiver has to be deployed before this step. The test is a POST with this body:
{ "event": "ping" }
Anything that answers 2xx passes. A receiver that ignores the body entirely still connects.
The URL has to use https, cannot include a query string, and cannot use a non-standard port. A trailing slash is removed, so https://example.com/api/articles/ is stored as https://example.com/api/articles. If your framework treats those as different routes, register the one without the slash.
What gets sent
Every article arrives as a POST with these headers:
Authorization: Bearer <your secret>
Content-Type: application/json
And this body:
{
"event": "article.publish",
"articleId": "trk_abc:2026-08-05",
"trackId": "trk_abc",
"date": "2026-08-05",
"status": "published",
"title": "SEO basics",
"slug": "seo-basics",
"description": "Where to start with search.",
"markdown": "# SEO basics\n\nSee [keyword research](https://example.com/blog/keyword-research).",
"imageUrls": ["https://cdn.timothe.ai/…/a.webp"],
"url": "https://example.com/blog/seo-basics"
}
Fields may be added over time. Existing field names will not change or disappear.
What your receiver has to do
Answer 2xx only when you have stored the article. A 2xx is taken as success and the article is marked as sent. If your receiver accepts the request and then fails to save it, Growth Calendar has no way to know.
Treat the same articleId as the same article. Revising an article and sending it again reuses the id, so a receiver that inserts blindly will accumulate duplicates.
Answering with a body is optional. If you return JSON like this, both values are used:
{ "id": "your-own-id", "url": "https://example.com/blog/seo-basics" }
id replaces articleId as the key sent on later requests for this article, and url is recorded as where the article lives. Returning nothing at all is fine.
Setting the public site URL
Because a receiver is not asked where the article ended up, Growth Calendar does not know the address unless you say. Fill in Public site URL on the same settings screen, such as https://example.com/blog, and articles are treated as living at that address plus their slug.
Without it, sibling articles cannot link to anything sent through the webhook. See the section on internal links below.
Choosing how articles are sent
Once connected, the same screen offers three choices.
What draft means is up to your receiver. Growth Calendar only reports which one was asked for.
Sending a single article
Use Publish to CMS at the top right of an article. The control next to it switches between sending it as a draft and as published.
About internal links
Growth Calendar links articles in the same cluster to each other. When an article is sent, links to siblings that already have an address become real URLs; the rest are unwrapped to plain text, because a link to an address that does not exist would send readers to an error page.
For webhook destinations, an article has an address once the public site URL is set. Without it, no article sent this way is ever linked to by another.
About images
Images keep their Timothe-hosted addresses in imageUrls and in the Markdown. They are not uploaded anywhere. Copy them into your own storage in your receiver if you would rather not depend on those addresses.
When it does not work
"Could not reach that endpoint"
Nothing answered at that address. Check the receiver is deployed and reachable from the public internet, and that the URL has no typo. An address that resolves to a private or internal network is refused.
"The endpoint refused the secret"
The receiver answered 401 or 403. Check that it compares the same value that was entered here, against the Authorization header, in the form Bearer <secret>.
"Nothing is listening at that URL"
The receiver answered 404. The domain is right and the path is not. Enter the full path to the endpoint, not just the domain.
"That endpoint does not accept POST"
The receiver answered 405. Every article is sent as a POST, including the test request.
"Site URL must use https" / "must not contain a query or fragment" / "must not specify a port"
The endpoint has to be a plain https URL. If your receiver expects a token in the query string, put it in the shared secret instead and read it from the Authorization header.
"The endpoint rejected the article"
The receiver answered with an error while an article was being sent. The status code is in the message; check the receiver's own logs for the request that carries that articleId.
Other CMSs
WordPress, Ghost, Shopify and Notion connect from the same screen, and publish to those directly rather than through your own code.
