# Connect your site — API setup brief (/docs/en/api-setup)

# Connect your site to Daily Blog Post — setup brief

This page is the **complete brief** to connect a custom site to Daily Blog
Post. It is written for two readers at once:

* a developer who follows the steps by hand;
* an AI coding agent (Claude Code, Cursor, Codex…) that receives this page
  and an API token, and does the work.

Raw Markdown of this page, to paste or to fetch from an agent:
`https://dailyblogpost.app/docs/en/api-setup.mdx`. An agent connected to the
[Daily Blog Post MCP server](/docs/en/mcp) gets the same text with the
`get_api_setup_guide` tool.

**Not for you if** your site runs on WordPress (use the plugin), Shopify or
Webflow (native integrations in **Integrations**): those channels need none of
this.

## What you get at the end

* A `/blog` listing and one page per article, built from the articles Daily
  Blog Post generated and you published.
* Articles in your sitemap.
* Optional: your site rebuilds the moment you publish (steps 3).
* Optional: each article counted as **delivered** in your dashboard, with its
  public URL (step 4).

## Step 0 — Pick your path

| You want                                                             | Do steps      | Effort        |
| -------------------------------------------------------------------- | ------------- | ------------- |
| A blog that reads Daily Blog Post and rebuilds on a schedule         | 1 → 2         | one afternoon |
| …that also rebuilds the moment you publish                           | 1 → 2 → 3     | + 30 minutes  |
| …and a dashboard that shows every article as delivered, with its URL | 1 → 2 → 3 → 4 | + 30 minutes  |

Steps 1 and 2 are enough for a working blog. Steps 3 and 4 are independent of
each other.

## Step 1 — Get an API token

1. In the app, open **Integrations**, find the **Public API** card and click
   **Configure Public API**.
2. Create a token. Give it the name of the site (e.g. "Site Next.js
   production"). Tick the permission `articles:read`. Tick `publish:write`
   only if you plan to do step 4.
3. Copy the token: it is shown **once**.

Rules for the token:

* Store it as an environment variable on the server or in the build, e.g.
  `DBP_API_TOKEN`. **Never ship it in browser code.**
* Send it in the `X-API-Key` header. `Authorization: Bearer` is **not**
  accepted on the public API (it is the MCP server's header).
* Base URL: `https://dailyblogpost.app`.

```bash
curl -H "X-API-Key: $DBP_API_TOKEN" \
  "https://dailyblogpost.app/api/public/articles?limit=100&page=1"
```

A `401` means the header is missing or the token was revoked.

## Step 2 — Read the articles and build the pages

### 2a. List

`GET /api/public/articles?limit=100&page=1` (then `page=2`… until
`page > totalPages`).

```json
{
  "articles": [
    {
      "id": "7f1c…",
      "slug": "how-to-choose-a-standing-desk",
      "title": "How to choose a standing desk",
      "metaTitle": "How to choose a standing desk (2026 guide)",
      "metaDescription": "Height range, stability, budget: the five criteria…",
      "excerpt": "Height range, stability, budget: the five criteria…",
      "content": "<h2>…</h2><p>…</p>",
      "coverImageUrl": "https://…/cover.webp",
      "keywords": ["standing desk", "ergonomic desk"],
      "locale": "en",
      "status": "published",
      "publishedAt": "2026-10-02T08:00:00.000Z",
      "updatedAt": "2026-10-03T09:12:41.000Z",
      "createdAt": "2026-10-01T17:40:12.000Z"
    }
  ],
  "page": 1,
  "limit": 100,
  "total": 1,
  "totalPages": 1
}
```

Other useful parameters: `locale=fr` (one language), `updated_since=<ISO
date>` (only what changed, for incremental builds), `sort=publishedAt&order=asc`.

### 2b. What the default list contains — read this twice

Without a `status` parameter you receive:

* the articles **published** from Daily Blog Post;
* plus the articles **already sent to your site** by the API integration of
  step 3 that your site has not confirmed yet. Those carry their real
  `status` (usually `generated`).

So: **do not filter on `status === "published"` on your side.** If you do,
an article sent to your site is never rendered, so it is never confirmed, so
it never becomes published: a loop with no exit. Render everything the
default list returns.

Only the **sitemap** should use `?status=published` (see 2e). Any other
`status` value answers `400`: this API serves what you published, it never
previews an article that is still being written.

### 2c. One page per article

| Page element                | Field                      | Rule                                                                                                                    |
| --------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| URL                         | `slug`                     | `https://your-site.com/blog/{slug}`. Keep this pattern stable: it is what you will confirm in step 4.                   |
| `<title>`                   | `metaTitle`                | falls back to `title` server-side, always present                                                                       |
| `<meta name="description">` | `metaDescription`          | may be empty                                                                                                            |
| H1                          | `title`                    |                                                                                                                         |
| Body                        | `content`                  | **HTML** produced by Daily Blog Post (headings, paragraphs, lists, links, images). Render it as HTML, do not escape it. |
| Cover image                 | `coverImageUrl`            | may be `null`; `alt` = `title`                                                                                          |
| Dates                       | `publishedAt`, `updatedAt` | ISO strings; `publishedAt` is `null` for an article sent to your site and not confirmed yet — show `createdAt` then     |
| Language                    | `locale`                   | set `<html lang>` or the page language accordingly                                                                      |

Store or key everything on `id`: a re-publish of the same article must
**update** the existing page, never create a second one.

### 2d. Single article

`GET /api/public/articles/{id}` or `GET /api/public/articles/by-slug/{slug}`.
Same content rule as the list: published, or sent to your site and not
confirmed yet. `404` otherwise.

### 2e. Sitemap

Expose a sub-sitemap built from `GET
/api/public/articles?status=published&sort=publishedAt&order=asc&limit=100`
and reference it from your sitemap index:

```xml
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://your-site.com/sitemap-main.xml</loc></sitemap>
  <sitemap><loc>https://your-site.com/sitemap-blog.xml</loc></sitemap>
</sitemapindex>
```

One `<url>` per article: `<loc>` from `slug`, `<lastmod>` from `updatedAt`.

### 2f. Rebuild on a schedule

A static site shows a new article only after its next build. Rebuild at least
every hour (CI schedule, host cron). On each build, fetch the **full** default
list and compare its `id`s with the pages you have: a new `id` is a page to
create, a missing one a page to remove (archived on our side). Do **not**
rely on `updated_since` alone to decide whether anything changed: an article
handed to your site by step 3 keeps its old `updatedAt`, and an archived
article leaves the list without ever appearing in `updated_since`. Use
`updated_since` only to limit how much content you re-download.

**Checkpoint before step 3:** `curl` with the token returns `200` and JSON; a
page exists for every article of the list; the token is server-side only;
the sitemap lists the published articles.

## Step 3 (optional) — Rebuild the moment you publish

Instead of waiting for the next scheduled build, let Daily Blog Post call your
site when you click **Publish**.

1. Have an HTTPS URL that triggers a rebuild: your host's **deploy hook**
   (Vercel, Netlify, Cloudflare Pages, Coolify…), a CI dispatch URL, or a
   revalidation route you write.
2. In the app, **Integrations → API** card: paste that URL as **Endpoint
   URL**, add an authentication header if your hook needs one, **Save**, then
   click **Test**. The Test sends `{ "type": "dailyblogpost.test" }`; any
   `2xx` passes.
3. From now on every Publish sends a JSON `POST` to that URL:

```json
{
  "type": "dailyblogpost.publish",
  "sent_at": "2026-10-09T10:00:00.000Z",
  "article": {
    "id": "7f1c…",
    "title": "…",
    "content": "<h2>…</h2>",
    "seo_title": "…",
    "seo_description": "…",
    "seo_slug": "how-to-choose-a-standing-desk",
    "keywords": ["…"],
    "cover_image_url": "https://…",
    "cover_image_alt": "…"
  }
}
```

Rules:

* Answer `2xx` quickly; do the rebuild asynchronously if it is slow.
* Handle both `type` values. On `dailyblogpost.test`, return `200` and stop.
* Point the URL at the **final host** (`www.` or apex, whichever answers).
  The push follows a redirect only between the apex and its `www.` variant,
  and only on `307`/`308`: a `301` or `302` on a `POST` is refused and the
  publication is recorded as failed.
* You do not need to store the payload: the article is already in the
  default list of step 2 the moment it is sent. Your rebuild reads it from
  there.

After a bare `2xx` the dashboard shows the article as **received, not
confirmed**, and the article stays unpublished on our side until step 4
confirms it. If you skip step 4, that is the normal, stable state for a
read-only site.

## Step 4 (optional) — Confirm each article with its URL

Confirmation is what turns "received" into **delivered** in the dashboard,
publishes the article on our side and fires the `article.published` signal.
It always carries the **public http(s) URL** of the article on your site.
Pick one of the three ways:

| Way                                        | When it fits                                                                           | How                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------------------------------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **A. Answer the push with the URL**        | your endpoint of step 3 creates the page synchronously (a CMS, a database-backed site) | respond to `dailyblogpost.publish` with `{ "remote_url": "https://your-site.com/blog/{seo_slug}", "remote_id": "…" }`. Any of the keys `remote_url`, `url`, `permalink`, `link`, `remote_id`, `id`, `itemId`, `item_id` in your answer counts as a confirmation, so a deploy-hook or revalidation endpoint must answer **without** them (an empty body is fine) until the page is really online. |
| **B. Acknowledge after the build**         | static sites: the page exists only once the build finished                             | at the end of the build, for each article rendered for the first time, call the acknowledgement below (needs `publish:write`)                                                                                                                                                                                                                                                                    |
| **C. Give us a Read URL and let us check** | you do not want to write any call                                                      | in **Integrations → API**, fill **Read URL**: an endpoint on your site that lists the articles online (contract in [Custom API — Read endpoint](/docs/en/custom-api-read)). Daily Blog Post checks it weekly and confirms what it finds.                                                                                                                                                         |

Acknowledgement call (way B):

```bash
curl -X POST "https://dailyblogpost.app/api/public/articles/{id}/publish" \
  -H "X-API-Key: $DBP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "api", "remote_url": "https://your-site.com/blog/how-to-choose-a-standing-desk" }'
```

* `remote_url` is **required** and must be `http(s)`: without it the call
  answers `422` with `code: "remote_url_required"` and records nothing.
* The call goes through the same checks as the Publish button. `422` with
  `issues` tells you why: `invalid_status` (archived or still generating),
  `missing_content`, `missing_seo_slug`, `language_mismatch`.
* Acknowledge each article **once**, after its page is really online, and
  only the articles whose `publishedAt` is still `null`. Acknowledging an
  already published article again resets its publication date and fires an
  `article.updated` event.

## Acceptance checklist

Tick everything that applies to the path chosen in step 0.

* [ ] `GET /api/public/articles` with the token answers `200` JSON (step 1).
* [ ] The token lives in an environment variable, server or build side only.
* [ ] One page per article at a stable URL built from `slug`; title, meta
  description, HTML content and cover image rendered (step 2c).
* [ ] **No client-side filter on `status`** (step 2b).
* [ ] Re-publishing an article updates the existing page (key on `id`).
* [ ] Sitemap built from `?status=published` and referenced in the index.
* [ ] Scheduled rebuild in place, driven by the full list of `id`s (not by
  `updated_since` alone).
* [ ] Step 3: the **Test** button in Integrations → API passes; the endpoint
  points at the final host.
* [ ] Step 4: each live article is confirmed with its URL; the dashboard
  shows it as delivered.

## Prompt for an AI coding agent

Paste this in your agent, in the repository of your site:

> Read [https://dailyblogpost.app/docs/en/api-setup.mdx](https://dailyblogpost.app/docs/en/api-setup.mdx) and connect this site
> to Daily Blog Post. The API token is in the environment variable
> `DBP_API_TOKEN`; never print it or commit it. Do steps 1 to 4 (or: 1 and 2
> only), following the field rules and the acceptance checklist exactly. Our
> article URL pattern is `https://your-site.com/blog/{slug}`. Stop and ask me
> before any change outside the blog pages, the sitemap and the build
> configuration.

## Reference

* Endpoints, fields, pagination, error codes: [Public API — Articles](/docs/en/public-api-articles).
* Read endpoint contract (step 4, way C): [Custom API — Read endpoint](/docs/en/custom-api-read).
* Driving Daily Blog Post itself from an agent: [MCP server](/docs/en/mcp).
