@opinly/backend
The typed data client — createOpinlyClient and every method it returns.
@opinly/backend wraps the /v1 REST API in a small, fully-typed
client. Its TypeScript types are generated from the OpenAPI spec, so they always match the API.
createOpinlyClient
import { createOpinlyClient } from '@opinly/backend'
const opinly = createOpinlyClient({
apiKey?: string, // defaults to process.env.OPINLY_API_KEY
url?: string, // defaults to "https://sdk.opinly.ai"
fetch?: typeof fetch, // inject a custom fetch (e.g. for caching)
})The key is sent as Authorization: Bearer <key> on every request. If no apiKey is provided and
OPINLY_API_KEY is not set, the call throws — so always create the client server-side.
// Next.js: cache responses, invalidate via webhooks
const opinly = createOpinlyClient({
fetch: (url, init) => fetch(url, { ...init, cache: 'force-cache' }),
})Methods
Each method maps to one endpoint and returns a typed result. There is no resolve() — route by
URL in your app and call the matching method. Categories and authors are taxonomy-prefixed, so a
category archive is posts({ category }) and a single post is post(slug).
| Method | Returns | Description |
|---|---|---|
posts({ limit?, cursor?, category?, author?, sort? }) | PostList | A cursor-paginated page of published posts. Filter by category/author slug. |
post(slug) | FullPost | null | A single post by its flat, company-unique slug (string, e.g. 'my-post'); null if none. |
author(slug) | AuthorPage | A single author page (or not-found). |
authors() | Authors | All authors with sample posts. |
categories() | CategorySummary[] | Categories, each with up to 5 latest posts. |
routes() | ContentRoute[] | Every addressable route — { type, slug, lastModified } (bare slugs). Feeds both your sitemap and static generation; shape each with sitemapUrl/routeParams from @opinly/shared. |
rss({ limit? }) | RssItem[] | Feed items ({ slug, title, description?, date, categories? }). |
const first = await opinly.posts({ limit: 12 })
const next = await opinly.posts({ cursor: first.next_cursor ?? undefined })
const post = await opinly.post('my-post') // FullPost | null
const feed = await opinly.rss({ limit: 50 })
if (post) {
// post.content is your Tiptap JSON
}post() returns null on a 404; every other method throws on a non-2xx response, and the thrown
Error message includes the problem code and detail from the API (see
Errors).
All domain types (FullPost, Post, CategorySummary, AuthorPage, Authors, PostList,
ContentRoute, RssItem, ContentNode, Problem, …) are exported from the package for use in
your own components:
import type { FullPost, Post } from '@opinly/backend'Webhook types
The package also exports the webhook event type for content invalidation:
import type { OpinlyWebhookEvent } from '@opinly/backend'
// { type: 'content.paths-invalidated'; data: { paths: string[] } }See Webhooks for the full handler.