List & read posts
Two routes read the public archive of the site that owns the key: a paginated listing and a single-post detail.
- Auth: Bearer API key — scope
posts:read. These are the only two routes that also accept a public read key (pk_…), including as a?key=query parameter. See Keys, scopes & restrictions. - Rate limit: 600 req/min per key, plus 60 req/min per (key, IP) when the
caller used a
pk_key.
GET /v1/posts
Section titled “GET /v1/posts”Paginated archive, newest first.
- URL:
https://api.canverly.com/v1/posts - Method:
GET
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Notes |
|---|---|---|---|
limit |
integer | 20 |
1–50. Out of range returns 400 — the value is never silently clamped. |
cursor |
string | — | The next_cursor from the previous page. An invalid cursor returns 400. |
category |
string | — | Category slug. Comma-separate for match-any (category=news,opinion). |
tag |
string | — | Tag slug. Comma-separate for match-any. |
q |
string | — | Full-text search over title and excerpt. |
since |
string | — | RFC-3339 timestamp. Returns only posts with updated_at >= since — the incremental-sync hook. |
language |
string | — | BCP-47 tag, e.g. pt-BR. |
key |
string | — | A pk_… public read key, for browser callers. A ck_ key here returns 400. Ignored when an Authorization header is present. |
Headers
Section titled “Headers”| Header | Required | Value |
|---|---|---|
Authorization |
yes¹ | Bearer ck_… or Bearer pk_… |
If-None-Match |
no | A previously returned ETag. A match returns 304 with no body. |
¹ Unless the request carries ?key=pk_… instead.
Example
Section titled “Example”curl -s "https://api.canverly.com/v1/posts?limit=5&category=news" \ -H "Authorization: Bearer ck_live_xxx"Paginate by following the cursor until it is absent or null:
curl -s "https://api.canverly.com/v1/posts?limit=50&cursor=eyJwIjoi…" \ -H "Authorization: Bearer ck_live_xxx"Incremental sync — poll only what changed since your last run:
curl -s "https://api.canverly.com/v1/posts?since=2026-08-01T00:00:00Z" \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”{ "items": [ { "id": "01J8ZQ7X4K2N5M9P0R3T6V8W1Y", "slug": "como-fazer-x", "title": "Como fazer X", "excerpt": "A one-sentence summary.", "published_at": "2026-08-01T12:00:00Z", "updated_at": "2026-08-02T09:30:00Z", "author": { "name": "Ana Prado", "slug": "ana-prado", "avatar_url": "https://cdn.example.com/a.png" }, "categories": [{ "slug": "financas", "name": "Finanças" }], "tags": [{ "slug": "juros", "name": "Juros" }], "cover_url": "https://cdn.example.com/cover.jpg", "url": "https://blog.example.com/como-fazer-x", "reading_time": 4, "language": "pt-BR", "post_type": "post" } ], "next_cursor": "eyJwIjoi…"}| Field | Type | Notes |
|---|---|---|
id |
string | 26-character Crockford base32 ULID. |
slug |
string | URL slug. |
title |
string | |
excerpt |
string | |
published_at / updated_at |
string | RFC-3339. |
author |
object | null | Public byline (a persona), not a platform user. null when no persona is assigned — handle that case. |
categories / tags |
array | { slug, name }. name is never empty: an unregistered slug is humanized (juros-compostos → Juros Compostos), so you never need two display paths. |
cover_url |
string | null | Absolute CDN URL; null/absent when there is no resolvable image. |
url |
string | Canonical public URL, already carrying the language prefix when the post is not in the site’s default language. |
reading_time |
integer | Minutes, max(1, ceil(words / 200)) — the same formula the site uses. |
language |
string | BCP-47. |
post_type |
string | e.g. post, page. |
next_cursor |
string | null | Opaque cursor for the next page. Absent or null means the end. There is no total and no has_more. |
The listing body does not include the post body. Fetch the detail route for that.
Caching
Section titled “Caching”Responses carry a strong ETag and Cache-Control: public, max-age=60, stale-while-revalidate=300.
Send the ETag back as If-None-Match and you get a 304 with no body when
nothing changed — the cheapest possible poll.
GET /v1/posts/{reference}
Section titled “GET /v1/posts/{reference}”One published post, with its rendered body.
- URL:
https://api.canverly.com/v1/posts/{reference} - Method:
GET
| Path segment | Notes |
|---|---|
reference |
A ULID (26 chars), a UUID (36 chars), or a slug. Anything that is not id-shaped is treated as a slug. |
Same query/header options as the listing for key and If-None-Match.
Cache-Control here is public, max-age=300, stale-while-revalidate=600.
# By slugcurl -s https://api.canverly.com/v1/posts/como-fazer-x \ -H "Authorization: Bearer ck_live_xxx"
# By idcurl -s https://api.canverly.com/v1/posts/01J8ZQ7X4K2N5M9P0R3T6V8W1Y \ -H "Authorization: Bearer ck_live_xxx"200 OK
Section titled “200 OK”Every field of the listing item, plus:
| Field | Type | Notes |
|---|---|---|
content_html |
string | The rendered body, allowlist-sanitized on the way out. |
What sanitization means concretely, so you can plan around it:
- No
<script>,<iframe>,<form>and no event-handler attributes. Ever. - Ad blocks, product cards and story pages are omitted from the output.
- Video embeds are downgraded to a link.
- An image stored as a library id (
attrs.media_id) comes out with its URL already resolved. If the asset was deleted or is unavailable, the block is omitted rather than emitted as a broken<img>.
Errors
Section titled “Errors”Envelope: { "error": { "code", "message" } }. Full table in Errors & rate limits.
| Status | error.code |
When |
|---|---|---|
304 |
— | If-None-Match matched; no body. |
400 |
bad_request |
Invalid limit or cursor; or a ck_ key passed in ?key=. |
401 |
unauthorized |
Missing/invalid/revoked key. |
403 |
forbidden |
Scope posts:read missing — or, for a pk_ key, the request Origin is not in the key’s allowed origins. The key is valid; the page it runs on is not. Add the origin under Configurações → Integrações API. |
404 |
not_found |
Detail route: no such post, or not public. |
429 |
rate_limited |
Over 600 req/min per key, or 60 req/min per (key, IP) on a pk_ key. Honour Retry-After. |
502 |
upstream |
Transient upstream error; retry with backoff. |