OpinlySDK

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

MethodPathDescriptionParams
GET/v1/content/postsPublished posts (cursor-paginated, filterable).limit (1–100, default 12), cursor, category, author, sort (newest|oldest)
GET/v1/content/postA single post by slug (404 if none).slug (the post's slug, e.g. my-post)
GET/v1/content/categoriesCategories, each with up to 5 latest posts.
GET/v1/content/authorsAll authors with sample posts.
GET/v1/content/authors/{slug}A single author page.slug (path)
GET/v1/content/routesAll addressable routes (sitemap + static generation): typed { type, slug, lastModified }, bare slugs.
GET/v1/content/rssRSS 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…"
}
  • code is a stable, machine-readable identifier (UNAUTHORIZED, VALIDATION_ERROR, INVALID_CURSOR, NOT_FOUND, INTERNAL_ERROR).
  • request_id is echoed in the X-Request-Id response header on every request — quote it when contacting support. Send your own X-Request-Id to correlate requests end to end.