OpinlySDK

@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).

MethodReturnsDescription
posts({ limit?, cursor?, category?, author?, sort? })PostListA cursor-paginated page of published posts. Filter by category/author slug.
post(slug)FullPost | nullA single post by its flat, company-unique slug (string, e.g. 'my-post'); null if none.
author(slug)AuthorPageA single author page (or not-found).
authors()AuthorsAll 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.