CookbookCall from a browser

Call the API from a browser

The chat completions endpoint answers browser preflight requests, so a web page can call it directly with fetch. Whether it should depends on who is allowed to spend your credits. This recipe covers the safe default (a backend in between), and the narrower case where a browser key with an origin allowlist is acceptable.

Choose the pattern

PatternKey lives inUse when
Backend relay (recommended)Your serverAnything public-facing, anything with sign-in, anything that costs real money.
Browser key with origin allowlistThe page’s codeInternal tools, prototypes and demos on your own domain, where everyone who can open the page may use the key.
⚠️

Anything shipped to a browser can be read by whoever loads the page. An origin allowlist stops other websites from using your key from their visitors’ browsers; it does not stop a person from copying the key and sending requests with a forged Origin header from a script. Only use a browser key you are prepared to see used outside your page, and give it a low spend limit.

Pattern 1: backend relay

The browser calls your server; your server adds the key and calls Syntic. The server is also the place to check the user is signed in and to limit how much each user can use.

// server.js (Node 18+, Express)
import express from 'express'
const app = express()
app.use(express.json())
 
app.post('/api/chat', async (req, res) => {
  // Authenticate your user here and apply per-user limits.
  const upstream = await fetch('https://app.syntic.ai/v1/chat/completions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.SYNTIC_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'Amara Quick',
      max_tokens: 500,
      messages: [
        { role: 'system', content: 'You are the help assistant for the Acme dashboard.' },
        ...req.body.messages.slice(-20),            // cap history from the client
      ],
    }),
  })
  res.status(upstream.status)
  res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'application/json')
  for await (const chunk of upstream.body) res.write(chunk)   // stream straight through
  res.end()
})
 
app.listen(3000)

The page calls /api/chat on its own origin, so no CORS setup is needed at all.

Pattern 2: a browser key with an origin allowlist

Create a separate key for the browser

In the Console, Settings → API Keys, create a private key used only by this page. Never reuse your server key.

Restrict its origins

Add the sites that may use it, for example https://tools.example.com and, for development, http://localhost:5173. Requests from any other origin, or with no Origin header, are refused with 403 origin_not_allowed. Entry syntax and wildcard rules are on Managing API keys.

Set a spend limit

Give the organization or the key’s owner a spend limit you are comfortable losing. See Billing & credits.

Call the API with plain fetch

<script type="module">
  const KEY = 'sk_live_...'   // origin-restricted browser key; see the warning above
 
  async function ask(question, onText) {
    const res = await fetch('https://app.syntic.ai/v1/chat/completions', {
      method: 'POST',
      headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ model: 'Amara Quick', messages: [{ role: 'user', content: question }] }),
    })
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error}`)
    const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()
    let buffer = ''
    for (let r = await reader.read(); !r.done; r = await reader.read()) {
      buffer += r.value
      const lines = buffer.split('\n')
      buffer = lines.pop()
      for (const line of lines) {
        if (!line.startsWith('data: ') || line === 'data: [DONE]') continue
        const piece = JSON.parse(line.slice(6)).choices?.[0]?.delta?.content
        if (piece) onText(piece)
      }
    }
  }
 
  ask('Suggest a name for an internal wiki.', (t) => (document.body.textContent += t))
</script>

CORS details

ItemValue
PreflightOPTIONS requests are answered without a key.
Allowed methodsPOST, OPTIONS
Allowed request headersAuthorization, Content-Type
Allowed originAny (*). Your key’s allowlist is what restricts origins.
Headers readable from JavaScriptx-syntic-request-id, x-syntic-billing-source

Two consequences:

  • Send only Authorization and Content-Type. Client libraries that add their own headers (telemetry or version headers, for example) fail the preflight in a browser. Use plain fetch in browser code.
  • x-syntic-model-downgraded is not readable from browser JavaScript. If you need to detect model fallbacks, do it on a server.

Errors you may see

StatuserrorCause
403origin_not_allowedThe page’s origin is not on the key’s list (check scheme and port), or the browser sent Origin: null (for example from a file:// page).
403public_key_rejectedA pk_live_ key was used. Public keys cannot call the chat endpoint.
CORS error in the console, no status—A header other than Authorization or Content-Type was sent.
Was this page helpful?