API Documentation
Everything you need to fetch your blog content via the Blotd API.
Getting Started
- Sign up - Create a free account at
blotd.com/login - Get your API key - Go to Dashboard > API Keys and copy your key
- Make your first request - See the example below
Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer blotd_xxxxxxxxxxxx
Keep your API key safe
Never expose your API key in client-side code, public repositories, or browser requests. Store it as an environment variable (e.g. BLOTD_API_KEY) and only use it in server-side code such as API routes, server components, or build scripts. If you believe a key has been compromised, revoke it immediately from your dashboard and create a new one.
API Key Scoping
Each API key only returns articles assigned to it. This lets you manage content for multiple websites or clients from a single Blotd account.
How it works
- Create separate API keys - Go to Dashboard > API Keys and create one key per website or client (e.g. "Marketing Site", "Partner Portal", "Mobile App").
- Assign articles to keys - When creating or editing an article, use the "API Key Access" panel in the sidebar to select which keys can access that article.
- Fetch with the right key - Each key only returns its assigned articles. An article assigned to "Marketing Site" won't appear in API responses for "Partner Portal".
Default behavior
If you leave all keys unchecked when creating an article, that article will be accessible by all of your API keys. This preserves backward compatibility and is the default for existing articles.
// Marketing site — uses its own key, only gets marketing articles
const marketing = await fetch(
"https://api.blotd.com/v1/articles",
{
headers: {
Authorization: `Bearer ${process.env.MARKETING_BLOTD_KEY}`,
},
}
);
// Partner portal — different key, different articles
const partner = await fetch(
"https://api.blotd.com/v1/articles",
{
headers: {
Authorization: `Bearer ${process.env.PARTNER_BLOTD_KEY}`,
},
}
);Base URL
https://api.blotd.com/v1Endpoints
/articlesList all published articles, paginated.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number |
| limit | number | 10 | Items per page (max 50) |
| sort | string | newest | newest or oldest |
| tag | string | - | Filter by tag |
Example
const res = await fetch(
"https://api.blotd.com/v1/articles?page=1&limit=10",
{
headers: {
Authorization: "Bearer blotd_xxxxxxxxxxxx",
},
}
);
const { data, pagination } = await res.json();/articles/:slugGet a single article by its slug.
const res = await fetch(
"https://api.blotd.com/v1/articles/my-first-post",
{
headers: {
Authorization: "Bearer blotd_xxxxxxxxxxxx",
},
}
);
const { data } = await res.json();/articles/tag/:tagFilter articles by tag. Supports the same pagination params as /articles.
/articles/search?q=querySearch articles by title, content, or tags.
Track Article Views
Pro Feature
Article view tracking is available on the Pro plan. Analytics data (total views, unique visitors, referrers) appears in your dashboard. Free users can still embed the pixel — it simply won't record data until they upgrade.
Because we recommend caching API responses, you can't rely on API call logs alone to count page views. The tracking pixel is a separate lightweight endpoint that fires on every page load, regardless of content caching.
/v1/t/:slugReturns a 1×1 transparent GIF and records a view event. Tracking requests do not count against your API quota.
Authentication
This endpoint supports two authentication methods so you can track views from both client-side HTML and server-side code:
| Method | Use Case | Example |
|---|---|---|
| ?tid=<tracking_id> | HTML <img> embeds (client-side) | /v1/t/my-post?tid=tid_abc123 |
| Authorization: Bearer | Server-side tracking | Bearer blotd_xxx... |
Tracking ID vs API Key: Your tracking ID (starts with tid_) is a public identifier safe to embed in HTML. It is not your secret API key — it can only be used for the tracking pixel, not to fetch content. Find it in Dashboard > API Keys.
Zero-Config Setup
Every article response includes a tracking.pixelUrl field with the full pixel URL ready to use. No need to look up or configure your tracking ID separately — just render the URL from the API response.
HTML Pixel Embed
<!-- Use tracking.pixelUrl from the API response -->
<img
src={article.tracking.pixelUrl}
width="1"
height="1"
alt=""
style="position:absolute;opacity:0"
/>Server-Side Tracking
// Fire-and-forget — use the pixelUrl from the article response fetch(article.tracking.pixelUrl);
Privacy
Visitor IPs are hashed with SHA-256 before storage — we never store raw IP addresses. Unique visitor counts are based on these anonymous hashes.
Response Format
All responses follow this shape:
{
"success": true,
"data": {
"id": "...",
"title": "My First Post",
"slug": "my-first-post",
"content": "<p>...</p>",
"tags": ["tutorial"],
"tracking": {
"pixelUrl": "https://api.blotd.com/v1/t/my-first-post?tid=tid_xxxx"
},
...
},
"pagination": {
"page": 1,
"limit": 10,
"total": 42,
"totalPages": 5
}
}Rate Limits
| Plan | Monthly Limit | Exceeded |
|---|---|---|
| Free | 1,000 requests | 429 Too Many Requests |
| Pro | 25,000 requests | 429 Too Many Requests |
Code Examples
Next.js (App Router)
async function getArticles() {
const res = await fetch(
"https://api.blotd.com/v1/articles",
{
headers: {
Authorization: `Bearer ${process.env.BLOTD_API_KEY}`,
},
next: { revalidate: 60 },
}
);
return res.json();
}
export default async function BlogPage() {
const { data } = await getArticles();
return (
<div>
{data.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.readingTime}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Track views — pixelUrl included in API response */}
<img src={post.tracking.pixelUrl} width={1} height={1} alt="" />
</article>
))}
</div>
);
}Astro
---
const res = await fetch(
"https://api.blotd.com/v1/articles",
{
headers: {
Authorization: `Bearer ${import.meta.env.BLOTD_API_KEY}`,
},
}
);
const { data: posts } = await res.json();
---
{posts.map((post) => (
<article>
<h2>{post.title}</h2>
<Fragment set:html={post.content} />
</article>
))}Plain JavaScript
fetch("https://api.blotd.com/v1/articles", {
headers: {
Authorization: "Bearer blotd_xxxxxxxxxxxx",
},
})
.then((res) => res.json())
.then(({ data }) => {
data.forEach((post) => {
console.log(post.title, post.slug);
});
});Best Practices
Cache responses server-side
Blog content changes infrequently. Cache API responses so one request serves thousands of visitors. In Next.js, use revalidate for time-based caching:
await fetch(url, { next: { revalidate: 60 } })Use on-demand revalidation for zero waste
Even better than time-based caching: tag your fetches and only invalidate when content actually changes. Your pages stay fully cached until you publish, update, or delete an article.
// In your page (cache indefinitely)
await fetch(url, { next: { tags: ["blog"] } })
// In your publish/update handler (bust the cache)
import { revalidateTag } from "next/cache"
revalidateTag("blog")Fetch only what you need
Use ?limit=5 if your homepage only shows 5 posts. Filter by tag with ?tag=tutorials for specific sections. Smaller responses mean faster loads and fewer wasted API calls.
Handle errors gracefully
Always handle 401, 404, and 429 responses. Show fallback UI when the API is unreachable, a friendly message for missing articles, and a retry prompt when rate-limited. Never let an API error crash your page.
Error Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request (missing params) |
| 401 | Unauthorized (missing or invalid API key) |
| 404 | Article not found |
| 429 | Rate limit exceeded |
| 500 | Server error |