Brandfine Docs
SDK

createBrandfineClient

The SDK's entry point. Typed HTTP client over the /external/* surface.

import { createBrandfineClient } from '@brandfine/client'

const bf = createBrandfineClient({
  baseUrl: 'https://api.brandfine.co',
  apiKey: process.env.BRANDFINE_API_KEY!,
})

Returns a stateless, multi-instance-safe handle. Construct one per (baseUrl, apiKey) pair — multi-tenant consumers create several side-by-side.

Options

OptionTypeRequiredNotes
baseUrlstringNo trailing slash (the client trims one if present).
apiKeystringWorkspace-scoped key.
fetchtypeof fetchOverride for tests / edge runtimes / tracing wrappers.
userAgentstringDefaults to @brandfine/client.

Empty baseUrl or apiKey throws at construction — fail loud, not silent.

Methods

bf.posts.list({ type, locale, forceLimit })       // paginated, returns flat array
bf.posts.getBySlug(slug)                          // null on 404
bf.workspace.get<TCustom, TSchema>()              // narrow customConfig + schemaOrg
bf.navigations.get(key)                           // null on 404
bf.categories.list({ locale })
bf.analytics.install(opts?)                       // inject the tracker, see /docs/sdk/analytics
bf.submissions.create(input)                      // POST a contact-form submission
bf.get<T>(path, opts?)                            // escape hatch for new endpoints

Error handling

Non-2xx responses (except 404 on nullable404 endpoints) throw BrandfineApiError:

import { BrandfineApiError } from '@brandfine/client'

try {
  await bf.posts.list()
} catch (err) {
  if (err instanceof BrandfineApiError) {
    console.error(err.status, err.statusText, err.body, err.url)
    if (err.status === 429) await sleep(1000) // backoff
  }
  throw err
}

publishedAt for sitemap <lastmod>

Every BrandfinePost carries a publishedAt ISO timestamp. It's the time of the last publish that changed content — re-stamped on every republish with a new contentHash, untouched on no-op republishes. Use it directly for sitemap entries:

const posts = await bf.posts.list({ type: 'services', locale: 'en' })

const sitemap = posts.map((p) => ({
  url: `https://example.com/services/${p.slug}`,
  lastmod: p.publishedAt,
}))

The singular bf.posts.getBySlug(slug) response includes a translations[] array with per-locale publishedAt, so you can compute a cross-locale lastmod when one URL represents the whole article family:

const post = await bf.posts.getBySlug('schengen-visa')
if (post) {
  const familyLastmod = post.translations
    ?.reduce((max, t) => (t.publishedAt > max ? t.publishedAt : max),
             post.publishedAt) ?? post.publishedAt
}

Draft edits don't affect publishedAt — only publishes do — so this is also the right field for "fresh content" badges or chronological feeds, not just sitemaps.

Typed customConfig

When you have a known shape for customConfig (per post type), parameterize the methods:

type ServiceConfig = {
  apply: { visaSlug: string; destinationCode: string } | null
  faq: Array<{ question: string; answer: string }>
}

const services = await bf.posts.list<ServiceConfig>({ type: 'services' })
// services[0].customConfig is now typed as ServiceConfig | null

The default is unknown — opt into typing incrementally as the consumer's needs solidify.

On this page