Public Content API
Use the Nolorem Public Content API to programmatically read your blog posts. Create an API key, choose its scope, authenticate with a bearer token, and sync content to any external system.
Updated Jun 17, 2026
The Public Content API gives you read-only programmatic access to the blog posts in your Nolorem organisation. Use it to pull content into a Drupal site, a custom front end, a mobile app, or any external system that needs your blog data.
The API is available on the Premium plan. All access is read-only; publishing and editing remain inside Nolorem.
Creating an API key
Only organisation admins can create API keys.
- In Settings, scroll to the API Keys section (visible to admins only).
- Click Create API key.
- Enter a descriptive name for the key (for example, "Drupal website" or "Content sync script").
- Choose the scope for the key (see below).
- Copy the key shown on screen. It starts with
nlr_live_.
The key is shown once only. Store it in a secrets manager or environment variable immediately. If you lose it, revoke the key and create a new one.
Choosing a key scope
When you create a key, you choose whether it covers one specific blog or all blogs in your organisation.
Blog-scoped key (recommended): the key is bound to a single blog. It can only access posts, categories, and tags from that blog. If the key is ever leaked or shared, only that one blog's content is exposed.
Org-wide key: the key has access to all blogs in your organisation. Use this only when you have a deliberate reason to access multiple blogs with a single key.
The scope badge on each key in the list shows which blog the key is bound to, or "All blogs" for an org-wide key. Scope is fixed at creation. To change scope, create a new key with the desired scope, update any consumers, then revoke the old key.
Authenticating requests
Pass the key in the Authorization header of every request:
Authorization: Bearer nlr_live_your_key_here
Example with curl:
curl https://nolorem.io/api/v1/posts \
-H "Authorization: Bearer nlr_live_your_key_here"
The API is server-to-server. There are no CORS headers, so browser clients cannot call it directly. Make all API calls from your back-end or a server-side script.
Listing your blogs
GET /api/v1/blogs lists the blogs your key can access. The response shape is the same whether the key is blog-scoped or org-wide.
curl https://nolorem.io/api/v1/blogs \
-H "Authorization: Bearer nlr_live_your_key_here"
Response:
{
"data": [
{ "id": "uuid", "slug": "my-blog", "name": "My Blog" },
{ "id": "uuid", "slug": "company-news", "name": "Company News" }
]
}
A blog-scoped key returns exactly one entry (the bound blog). An org-wide key returns all blogs. Use this endpoint to discover which blogs a key covers before fetching content.
Filtering posts by blog
All content endpoints accept an optional blog parameter. Pass either the blog UUID or its slug:
# Filter by slug
curl "https://nolorem.io/api/v1/posts?blog=my-blog" \
-H "Authorization: Bearer nlr_live_your_key_here"
# Filter by UUID
curl "https://nolorem.io/api/v1/posts?blog=550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer nlr_live_your_key_here"
If you omit blog, the API returns content from all blogs your key can access (the default, non-breaking behaviour).
If you use a blog-scoped key and pass ?blog= pointing to a different blog, the API returns 403 Forbidden. This prevents guessing which blogs exist in an organisation.
The same blog parameter works on /api/v1/categories and /api/v1/tags.
Listing posts
GET /api/v1/posts returns a paginated list of posts for your key's accessible blogs. By default it returns published posts, 20 per page.
curl "https://nolorem.io/api/v1/posts?status=published&per_page=50" \
-H "Authorization: Bearer nlr_live_your_key_here"
Response:
{
"data": [
{
"id": "uuid",
"blog_id": "uuid",
"title": "My blog post",
"slug": "my-blog-post",
"excerpt": "Lead paragraph...",
"language": "en",
"category": "Technology",
"tags": ["AI", "Productivity"],
"featured_image_url": "https://...",
"published_at": "2026-06-01T10:00:00Z",
"updated_at": "2026-06-10T14:30:00Z",
"html": null
}
],
"meta": { "page": 1, "per_page": 50, "total": 123 }
}
The html field is always null in list responses. Use the detail endpoints to get the full post body.
Available filters
| Parameter | Description | Example |
|---|---|---|
blog | Blog UUID or slug (default: all accessible blogs) | blog=my-blog |
status | published, draft, or scheduled (default: published) | status=published |
language | ISO 639-1 language code | language=nl |
category | Category name (case-insensitive) | category=Technology |
tag | Tag name (case-insensitive) | tag=AI |
updated_since | ISO 8601 datetime; only posts modified after this date | updated_since=2026-06-01T00:00:00Z |
page | Page number, starting at 1 | page=2 |
per_page | Items per page, max 100 | per_page=100 |
Fetching a single post
Fetch a post by its UUID to get the full HTML body:
curl "https://nolorem.io/api/v1/posts/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer nlr_live_your_key_here"
Or by its slug:
curl "https://nolorem.io/api/v1/posts/slug/my-blog-post" \
-H "Authorization: Bearer nlr_live_your_key_here"
Both return a single PublicPost object (not wrapped in a data array) with the html field populated.
Incremental sync with updated_since
To sync only new or changed content, store the timestamp of your last successful sync and pass it as updated_since on the next run:
# Initial sync: fetch all published posts
curl "https://nolorem.io/api/v1/posts?per_page=100" \
-H "Authorization: Bearer nlr_live_your_key_here"
# Subsequent syncs: only posts changed since last sync
curl "https://nolorem.io/api/v1/posts?updated_since=2026-06-10T14:30:00Z&per_page=100" \
-H "Authorization: Bearer nlr_live_your_key_here"
Paginate through all results before storing your new sync timestamp.
Detecting unpublished or deleted posts
When you use updated_since for incremental sync, the default response only returns posts that are still published. If a post was unpublished or deleted in Nolorem since your last sync, it simply disappears from the results with no signal to remove it on the destination.
To get that signal, pass include_deleted=true alongside your updated_since timestamp:
curl "https://nolorem.io/api/v1/posts?updated_since=2026-06-10T14:30:00Z&include_deleted=true" \
-H "Authorization: Bearer nlr_live_your_key_here"
The response includes all live posts as normal, plus minimal tombstone entries appended at the end of data. Each tombstone has:
id-- the post UUID you already have from a previous syncstatus-- the current status of the post, or"deleted"if it was permanently removedupdated_at-- when the change happeneddeleted--trueif the post was deleted,falseif it was simply unpublished or moved to draft
{
"data": [
{ "id": "...", "title": "Live post", "updated_at": "..." },
{ "id": "...", "status": "draft", "updated_at": "...", "deleted": false },
{ "id": "...", "status": "deleted", "updated_at": "...", "deleted": true }
],
"meta": { "page": 1, "per_page": 20, "total": 1, "tombstones": 2 }
}
Use meta.tombstones to know how many tombstone entries are appended. meta.total always reflects live posts only.
When your sync tool sees a tombstone, unpublish or remove the corresponding page on the destination (for example, set the matching Drupal node to unpublished or draft).
Without include_deleted=true, behavior is identical to before this change -- only live published posts are returned.
Listing categories and tags
Two endpoints return your blog's full category and tag taxonomy. These are useful for external tools that need to map Nolorem categories or tags to their own taxonomy (for example, a Drupal connector matching Nolorem categories to Drupal vocabulary terms). Both accept the blog parameter.
# All categories for your organisation (or the scoped blog)
curl "https://nolorem.io/api/v1/categories?blog=my-blog" \
-H "Authorization: Bearer nlr_live_your_key_here"
# All tags for your organisation (or the scoped blog)
curl "https://nolorem.io/api/v1/tags?blog=my-blog" \
-H "Authorization: Bearer nlr_live_your_key_here"
Both return the full list in a single response (not paginated):
{ "data": [{ "id": "uuid", "name": "Marketing" }, { "id": "uuid", "name": "Technology" }] }
The same bearer token, Premium plan requirement, and posts:read scope apply as for the posts endpoints.
Per-key usage analytics
In Settings, each key row can be expanded to show usage analytics:
- Requests (last 24h / total) -- how many API calls this key has made.
- Distinct sources -- how many distinct IP addresses and client applications have used this key recently.
- Last used -- the timestamp of the most recent authenticated request.
These analytics help you understand how your keys are being used and spot unexpected patterns.
Key sharing flag
If a key shows a large number of distinct IP addresses in a short period, Nolorem may flag it as "possibly shared or compromised." The flag appears as an amber warning on the key row. The key keeps working, and no automatic action is taken -- this is an advisory signal.
When you see the flag, review your usage logs. If you shared the key intentionally (for example, with multiple servers in a cluster), consider whether the access pattern is expected. If the key may have been leaked or shared with unintended parties, revoke it and create a new one to limit access.
Rate limits
Each API key is subject to these limits:
- 60 requests per minute
- 5 000 requests per day
When you exceed a limit, the API returns HTTP 429 with a Retry-After header indicating how many seconds to wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 43
{ "error": "Rate limit exceeded" }
Implement exponential backoff for reliable long-running sync scripts.
Error codes
| Status | Meaning |
|---|---|
| 400 | Invalid query parameters |
| 401 | Missing or invalid API key |
| 403 | Premium plan required, key lacks posts:read scope, or blog-scoped key used with a mismatching ?blog= value |
| 404 | Post not found |
| 429 | Rate limit exceeded (check Retry-After header) |
Machine-readable spec
The full API specification in OpenAPI 3.1 format is available at:
GET /api/v1/openapi.json
This endpoint is unauthenticated. You can import it into Postman, Insomnia, or any OpenAPI-compatible tool.
Revoking a key
To revoke a key, scroll to the API Keys section in Settings, find the key by name, and click Revoke. The key stops working immediately. Requests in flight that were already authenticated will complete, but no new requests with the revoked key will succeed.