REST API (/v1)
The versioned HTTP contract behind @opinly/backend — endpoints, auth, pagination, and error shapes.
@opinly/backend is a thin client over a versioned REST API. You can call it directly from any
language or runtime if you prefer.
- Base URL:
https://sdk.opinly.ai - All routes are under
/v1/content - OpenAPI spec:
/v1/openapi.json - Interactive docs:
/v1/docs(Scalar)
Authentication
Send your API key as a bearer token in the Authorization header on every request:
curl https://sdk.opinly.ai/v1/content/posts \
-H "Authorization: Bearer sk-…"Your key is scoped to your company, so every request returns only your content. Keep it server-side.
A missing or invalid key returns 401 as an application/problem+json body (see
Errors):
{
"type": "https://opinly.ai/docs/reference/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"code": "UNAUTHORIZED",
"request_id": "9f1c…"
}Endpoints
| Method | Path | Description | Params |
|---|---|---|---|
GET | /v1/content/posts | Published posts (cursor-paginated, filterable). | limit (1–100, default 12), cursor, category, author, sort (newest|oldest) |
GET | /v1/content/post | A single post by slug (404 if none). | slug (the post's slug, e.g. my-post) |
GET | /v1/content/categories | Categories, each with up to 5 latest posts. | — |
GET | /v1/content/authors | All authors with sample posts. | — |
GET | /v1/content/authors/{slug} | A single author page. | slug (path) |
GET | /v1/content/routes | All addressable routes (sitemap + static generation): typed { type, slug, lastModified }, bare slugs. | — |
GET | /v1/content/rss | RSS feed items. | limit (1–100, default 20) |
There is no single "resolve everything" endpoint — you route by URL and call the matching typed
endpoint. Categories and authors are taxonomy-prefixed (/blog/category/…, /blog/authors/…), so
a category archive is just GET /v1/content/posts?category=<slug>, and a single post is
GET /v1/content/post?slug=<post> (returns the post, or 404 if there's none).
Pagination
GET /v1/content/posts is cursor-paginated. The response is an envelope:
interface PostList {
data: Post[]
has_more: boolean
next_cursor: string | null // opaque; pass as ?cursor= for the next page
}Fetch the next page by passing the previous response's next_cursor:
# first page
curl "https://sdk.opinly.ai/v1/content/posts?limit=12" -H "Authorization: Bearer sk-…"
# next page
curl "https://sdk.opinly.ai/v1/content/posts?limit=12&cursor=MTcwMDA…" -H "Authorization: Bearer sk-…"When there are more results, the response also carries an RFC 8288
Link: <…>; rel="next" header. Cursors are opaque — don't parse or construct them.
Response shapes
GET /v1/content/post returns a FullPost (200) or a problem document (404).
FullPost carries the body and everything needed to render a post page:
interface FullPost {
content: object // Tiptap JSON (OpinlyNode)
title: string
slug: string
description: string
metaTitle: string | null
metaDescription: string | null
titleFile: { fileKey: string | null; altText: string | null; title: string | null; caption: string | null } | null
images: { fileKey: string | null; altText: string | null; title: string | null; caption: string | null }[]
firstPublishedAt: string // ISO 8601
modifiedAt: string
author: { name: string; slug: string; fileKey: string | null; bio: string | null } | null
faqs: { question: string; answer: string }[] | null
category: { slug: string; name: string; description: string } | null
}Post (the lightweight "card" used in lists) and Category, Sitemap, Rss shapes are all
defined in the OpenAPI spec — the client's TypeScript types are generated from it, so they can't
drift. See the client reference for the typed methods.
Errors
Every non-2xx response is RFC 9457 application/problem+json:
{
"type": "https://opinly.ai/docs/reference/errors/validation-error",
"title": "Invalid request",
"status": 400,
"detail": "limit: must be less than or equal to 100",
"instance": "/v1/content/posts",
"code": "VALIDATION_ERROR",
"request_id": "9f1c…"
}codeis a stable, machine-readable identifier (UNAUTHORIZED,VALIDATION_ERROR,INVALID_CURSOR,NOT_FOUND,INTERNAL_ERROR).request_idis echoed in theX-Request-Idresponse header on every request — quote it when contacting support. Send your ownX-Request-Idto correlate requests end to end.