API reference

Spectare API

Complete reference for the Spectare REST API: endpoints, authentication, component types, rate limits, and error codes.

Looking for setup instructions? Integration guide →

Authentication

Public endpoints

/api/qualify and /api/assemble/components require no authentication. They are called from the visitor's browser and are rate limited by IP address.

Server-to-server

/api/assemble/ssr requires an org API key. Pass it as Authorization: Bearer sk_.... Get your key from your workspace Settings page. Keep it server-only.

Base URL

https://spectare.ai

All endpoints accept and return JSON. Set Content-Type: application/json on all requests.

Endpoints

POST/api/qualify/{orgSlug}No auth

Classifies a visitor's context into an audience, funnel stage, and confidence score. Returns a signed context token valid for 1 hour by default. The script tag and PersonalisedSection call this automatically on page load. Call it directly for custom integrations or AI agents.

Request body

{
  "summary": "developer evaluating personalisation tools for a Next.js site",
  "direct": true
}

summary: plain-text description of the visitor's context

direct: set true when the summary came from explicit user input rather than inferred signals

ttlSeconds: token lifetime, up to 90 days. Honoured only when the request carries Authorization: Bearer with your workspace API key; anonymous calls always get 1 hour. Use for outreach links that will sit in an inbox before the click.

warm: key-authenticated mints build the page for the classified cell before returning (so the link's first click serves from cache in ~50ms); the response reports warmed. Metered as one personalization, the same build the click would have triggered. Pass false to mint without building.

audience, stage: state them when your system already knows the segment (for example an outreach tool holding a reviewed persona) instead of having the classifier infer them from the summary. Requires the workspace API key. Values must match your workspace's audience names and the stages awareness | consideration | decision; an unknown value is a 400 listing the valid options, never a silent fallback. The classifier still runs for entities and the search query; only the stated fields are overridden.

Response

{
  "token": "<signed-context-token>",
  "url": "https://spectare.ai/landing?ctx=<token>",
  "intent": {
    "type": "explainer",
    "confidence": 0.88,
    "userContext": {
      "audience": "developer",
      "stage": "consideration"
    }
  },
  "orgName": "Acme Corp"
}
POST/api/assemble/componentsNo auth

Assembles a personalised page as structured JSON. Returns a ComponentAssembly[] array. Each item names one of the 10 registered component types and provides the slot values to render it. Assembly uses three signals: semantic similarity to the visitor's intent, conversion history (which atoms have converted for this audience and stage in the last 30 days), and a co-conversion graph (which atoms appear together in converting sessions). This is what PersonalisedSection calls client-side.

Response timing and format

On a cache hit the response is application/json and returns in under 50ms. On a cache miss the response is text/event-stream: components are streamed as SSE events as they are assembled (typically 10-20 seconds total). Check the Content-Type header to choose the correct parser. If you parse the stream yourself, only frames carrying a component field belong in the assembly: a {"_variant"} frame carries the variant id for conversion attribution, an {"_error"} frame means the stream died server-side (keep what arrived), and [DONE] ends it. JSON responses may also carry overLimit or degraded flags when default content is served in place of a fresh build. PersonalisedSection and ComponentRenderer handle all of this automatically.

The cache is invalidated automatically whenever an atom in that audience and stage combination is published, updated, or deleted. Entries expire after 7 days as a safety backstop.

Request body

{
  "token": "<signed-context-token>",
  "orgSlug": "your-workspace-slug"
}

Response

{
  "assembly": [
    {
      "component": "HeroStatement",
      "slots": {
        "headline": "...",
        "subheading": "..."
      }
    },
    {
      "component": "StatGrid",
      "slots": {
        "stats": [
          { "value": "5 min", "label": "Setup time" },
          { "value": "Up to 34%", "label": "Conversion uplift" }
        ]
      }
    },
    {
      "component": "CtaStrip",
      "slots": {
        "headline": "...",
        "primary": { "label": "Start free trial", "href": "/signup" }
      }
    }
  ],
  "intent": {
    "userContext": { "audience": "developer", "stage": "consideration" }
  },
  "assemblyTimeMs": 14800,
  "cached": false
}
POST/api/assemble/ssrBearer sk_...

Combines qualify and assemble into one server-to-server call. Use this from a Next.js server component to get personalised content into the HTML before first paint. Both the qualify result and the assembly are cached: on a warm request, classify and assembly complete in under 100ms with no AI calls. The qualify cache is keyed by signal pattern (UTM params, referrer, page hint) and expires after 7 days, or immediately when the org’s settings change. The assembly cache expires after 7 days or when an atom in that audience and stage is updated.

Request body

{
  "orgSlug": "your-workspace-slug",
  "pageHint": "Visiting the pricing page",
  "utmSource": "linkedin",
  "utmMedium": "paid",
  "utmCampaign": "q2-dev",
  "referrer": "https://google.com"
}

pageHint: plain-text hint about which page or section this is for

utmSource / utmMedium / utmCampaign / utmTerm: pass UTM params from the request query string

referrer: pass the Referer header from the incoming request

format: "flat" returns a plain page object instead of the component array; see Headless rendering below

zones: which zones the page actually placed, e.g. ["body","proof"]. The assembly is then composed only from components those zones can render, so the component budget is not spent on a hero or CTA the page will never show. Omit it (or send all five) to compose the whole page.

Response

{
  "assembly": [
    {
      "component": "HeroStatement",
      "slots": { "headline": "...", "subheading": "..." },
      "zone": "hero"
    },
    {
      "component": "AtomCard",
      "slots": { "atomId": 42, "title": "...", "content": "..." },
      "zone": "body"
    },
    {
      "component": "CtaStrip",
      "slots": { "headline": "...", "primary": { "label": "...", "href": "..." } },
      "zone": "cta"
    }
  ],
  "intent": { "audience": "developer", "stage": "consideration" },
  "assemblyTimeMs": 38,
  "cached": true
}
POST/api/conversionNo auth

Records a conversion event linked to the visitor's signed context token. The script tag fires this automatically when a visitor reaches a URL matching a configured conversion goal. Pass goalName, goalValue, and goalCurrency to attribute the event to a specific goal and track revenue in your analytics. All goal fields are optional: omit them for a simple page-reached conversion with no value.

Request body

{
  "token": "<signed-context-token>",
  "orgSlug": "your-workspace-slug",
  "url": "https://example.com/thank-you",
  "variantId": "<assembly-variant-id>",
  "goalName": "Purchase",
  "goalValue": 79.00,
  "goalCurrency": "USD"
}

token: signed context token from the qualify response (required)

url: the URL where the conversion happened

variantId: the variantId from the assemble response. Closes the attribution loop so Spectare knows which assembly led to this conversion.

goalName: name of the goal as configured in Settings (e.g. "Signup", "Purchase")

goalValue: numeric value of the conversion (e.g. order total). Defaults to 1 for count-based goals.

goalCurrency: ISO 4217 currency code (e.g. "USD", "GBP"). Uses the org default currency if omitted.

Response

{ "ok": true }
POST/api/conversion/serverBearer sk_...

Records a conversion from your backend, for goals that finish where the browser widget cannot see them: a hosted checkout, a booking system webhook, a CRM. Authenticate with your org API key. Pass the variantId the widget stored in the visitor’s browser (sessionStorage key _spectare_variant_{orgSlug}) if your system carried it through, and the conversion also credits the arrangement the visitor saw; omit it and the event still counts toward the goal.

Request body

{
  "orgSlug": "your-org",
  "goalName": "Order",
  "goalType": "purchase",
  "goalValue": 129.50,
  "goalCurrency": "GBP",
  "variantId": "abcdef0123456789"
}

goalName: how the conversion appears in analytics (required)

goalType: one of signup, lead, purchase, upgrade, download. Not pageview: a server cannot see a pageview, so that type stays with URL goals.

goalValue / goalCurrency: order value and ISO 4217 currency, optional

variantId: optional attribution, see above

Response

{ "ok": true, "recorded": true }
GET/api/contentNo auth

Your published content as machine-readable JSON, for LLMs and agents. A bare request returns the Spectare library; a request with a Bearer org API key returns that org’s, so the plugin and SSR helper can serve it on your own domain. Published atoms only, each with a stable slug; numeric ids and personalisation data are never exposed. Fetch a single item at /api/content/{slug}.

Index response (array)

[
  {
    "slug": "spectare-vs-a-b-testing",
    "title": "Spectare vs A/B testing",
    "summary": "First sentence of the content...",
    "category": "comparison",
    "lastUpdated": "2026-07-17T20:05:26.364Z",
    "canonicalUrl": "https://spectare.ai/atoms/spectare-vs-a-b-testing"
  }
]

Single item: /api/content/{slug}

{
  "slug": "spectare-vs-a-b-testing",
  "title": "Spectare vs A/B testing",
  "body": "Markdown body...",
  "category": "comparison",
  "lastUpdated": "2026-07-17T20:05:26.364Z",
  "canonicalUrl": "https://spectare.ai/atoms/spectare-vs-a-b-testing"
}

Script tag HTML attributes

Load the script with your workspace slug and the Spectare base URL. Without data-base-url the script calls the API on your own origin instead of spectare.ai, and nothing loads.

<script
  src="https://spectare.ai/spectare.js"
  data-org="YOUR_WORKSPACE_SLUG"
  data-base-url="https://spectare.ai"
></script>

Add data-accent="#e11d48" to set your brand colour. The components inherit your page background and text colour already, so the accent is the only colour Spectare picks. Setting it here rather than overriding the CSS variable means it wins regardless of the order your stylesheets load in.

The script also counts clicks on tel: links, mailto: links, and outbound links to booking platforms as conversions, since those journeys never reach a thank-you page a URL goal could match. They appear in analytics as Call clicked, Email clicked, and Booking clicked. Add data-track-clicks="0" to the script tag to switch this off.

Then two HTML attributes control where personalised content is placed in the page.

data-spectare-slot="name"

Marks an empty container that Spectare fills with a rendered atom card. Set the value to any slot name. Multiple slots with different names can appear on the same page. Spectare adds the class spectare-loaded when the slot is filled.

<div data-spectare-slot="primary"></div>
<div data-spectare-slot="supporting"></div>
data-spectare-target="slot:field"

Personalises an existing element on the page without inserting a wrapper div. Set the value to slot:field where the slot name matches a slot in your atom library and the field is one of:

title

Replaces the element text content with the atom title. Uses textContent, not innerHTML.

content

Sets innerHTML from the atom body HTML.

cta

Sets element text. On <a> elements also updates href to the atom CTA URL.

<h1 data-spectare-target="primary:title">Default headline</h1>
<p  data-spectare-target="primary:content">Default copy.</p>
<a  data-spectare-target="primary:cta" href="/signup">Get started</a>

Script tag only. This attribute is applied by the browser-side script after qualification and is not available on the Next.js SSR path. For SSR use PersonalisedSection or map the assembly response to your own components.

Full walkthrough in the integration guide.

Conversion goals

Configure conversion goals in Settings under the Tracking tab. Each goal has a URL pattern, a goal type, and optional CSS selectors to read the transaction value and currency directly from the page.

Goal types

signup

A visitor created an account. Value defaults to 1 (count). No revenue tracking.

lead

A visitor submitted a lead form or contact request. Value defaults to 1.

purchase

A paid transaction completed. Use valueSelector to read the order total from the page.

upgrade

An existing user upgraded their plan. Use valueSelector to read the new plan price.

download

A visitor downloaded a file or resource. Value defaults to 1.

pageview

A visitor reached a specific page (e.g. a pricing page or case study). Value defaults to 1.

Reading value from the page

For purchase and upgrade goals you can tell Spectare where to find the transaction value on the confirmation page. Set valueSelector to a CSS selector that points to an element whose text content is the amount (e.g. .order-total or [data-order-value]). Non-numeric characters are stripped automatically, so $79.00 becomes 79. If the selector matches nothing, the value defaults to 1.

Set currencySelector to a CSS selector for the currency code element (e.g. .currency). If omitted, the org default currency is used. The org default currency is configured in the Tracking tab of Settings.

Automatic firing via the script tag

When spectare.js qualifies a visitor, the goal configuration is stored in sessionStorage under _spectare_goals_{orgSlug}. On every subsequent page load, spectare.js checks whether the current URL matches any goal's urlPattern. If it matches, it reads the value and currency from the DOM (if selectors are configured) and fires POST /api/conversion automatically.

To fire a conversion manually (e.g. on a button click or form submit), call spectare('conversion', { goalName, goalValue, goalCurrency }). The script handles the token and orgSlug automatically.

Component types

The assembly array in every response contains items with a component name, a slots object, and a zone field. Render with ComponentRenderer from @spectare-personalisation/react, or map to your own components.

Zone assignment

Every component has a default page zone. When your integration uses zones (WordPress zone blocks, or filterAssemblyByZone() in Next.js), each assembly item's zone field determines where it renders on the page. You can override a component's zone per workspace in Settings under Component rules.

hero

Above-fold banner area

HeroStatement, ImageCtaHero

body

Main content section

AtomCard, StatGrid, FeatureList, CodeBlock, ComparisonTable

proof

Social proof section

TestimonialCard

cta

Call-to-action strip

CtaStrip

Zones only affect rendering position. Claude selects components based on visitor intent regardless of zone. If your page does not declare zones, all components render in sequence as a flat list.

Claude-written

Slot values are written fresh by Claude for each visitor. Your atom title never appears verbatim in these components.

HeroStatementzone: heroaudiences: any · stages: awareness
headlinestring

Primary headline written fresh for this visitor (2-10 words)

subheadingstring?

Supporting sentence below the headline

ImageCtaHerozone: heroaudiences: any · stages: consideration, decision
headlinestring

AI-generated headline overlaid on a full-bleed background image

subheadingstring?

Supporting sentence below the headline

ctaLabelstring

CTA button label

altstring?

Alt text for the background image

CtaStripzone: ctaaudiences: any · stages: consideration, decision
headlinestring

Short heading written for this visitor

primary{label, href}

Primary CTA button label and URL

secondary{label, href}?

Optional secondary link

Verbatim / Extract

Slot values are copied or extracted directly from the atom. Spectare does not paraphrase or rewrite.

AtomCardzone: bodyaudiences: any · stages: any
atomIdnumber

ID of the atom to render. Title, body, and stats are fetched from the atom record.

fullboolean?

When true, renders the full atom body. Defaults to a condensed card layout.

StatGridzone: bodyaudiences: any · stages: consideration, decision
stats{value, label}[]

Stat pairs extracted from the atom, displayed as a grid of value + label cards

captionstring?

Optional caption below the grid

FeatureListzone: bodyaudiences: manager, executive · stages: awareness, consideration
items{text, sub}[]

Feature items extracted from the atom, each with a short feature name and supporting detail

CodeBlockzone: bodyaudiences: developer · stages: consideration, decision
codestring

Code content extracted from the atom

langstring?

Language hint for syntax highlighting

captionstring?

Optional caption below the block

ComparisonTablezone: bodyaudiences: any · stages: consideration, decision
competitorstring

Competitor name for the right column header

rows{label, spectare, them}[]

Comparison rows extracted from the atom. The brand prop controls the left column header.

TestimonialCardzone: proofaudiences: manager, executive · stages: consideration
quotestring

Testimonial text, copied exactly as authored

namestring

Author name

rolestring

Author role or title

companystring?

Optional company name

brand prop

Pass brand="Your Company" to ComponentRenderer or PersonalisedSection. Used as the left column header in any ComparisonTable components. Defaults to "Us".

Component rules and overrides

Every workspace can override the default behaviour of any component. Configure these in the Spectare admin under Settings, Component rules. Overrides are stored per workspace and applied to every assembly for that workspace.

enabledboolean

When set to false, the component is removed from the registry entirely and Claude never selects it for this workspace. Use this to disable components that do not fit your content strategy.

Disable ComparisonTable if you never author comparison atoms.

guidancestring

Replaces the default assembly guidance for this component. The guidance text is injected into the assembly prompt so Claude knows when to pick this component and how to populate its slots. Leave blank to use the Spectare default.

Override CtaStrip guidance to always use a specific CTA label or URL pattern for your brand.

zone'announce' | 'hero' | 'body' | 'proof' | 'cta'

Moves the component to a different page zone for this workspace. Overrides the component's default zone assignment. The zone field on assembly items reflects the effective zone after any override is applied.

Move TestimonialCard from proof to body if your page layout puts social proof mid-page.

audiencesstring[]

Restricts the component to the listed audience segments, using your own segment names. Enforced before Claude runs: for any other visitor the component is removed from the registry entirely. This is the only audience gate there is; without it a component is eligible for every visitor, which the admin marks with an OPEN badge.

Restrict CodeBlock to your developer segment so a code sample never appears for anyone else.

stagesstring[]

Restricts the component to the listed journey stages, using your own stage names. Enforced before Claude runs, and replaces any Spectare default stage window for the component. A rule you set always holds; Spectare defaults may be tested during exploration so their timing can be learnt rather than assumed.

Limit CtaStrip to your booking and decision stages so early-stage visitors are not pushed to convert.

maxCountnumber

Caps how many of this component a single assembly may contain. Enforced on the result: extras beyond the cap are removed.

Cap StatGrid at 1 so a page never stacks two stat blocks.

size'full' | 'half'

Forces the rendered width of the component for this workspace, overriding its default.

Force AtomCard to half width in a dense grid layout.

Using zones in Next.js

Import filterAssemblyByZone from @spectare-personalisation/react to split a flat assembly into per-zone sections. Pass the zone prop to PersonalisedSection or ServerAssemblyRenderer to render only the components for that zone.

// SSR: split one assembly across four layout sections
<ServerAssemblyRenderer assembly={ssrData.assembly} zone="hero" />
<ServerAssemblyRenderer assembly={ssrData.assembly} zone="body" />
<ServerAssemblyRenderer assembly={ssrData.assembly} zone="proof" />
<ServerAssemblyRenderer assembly={ssrData.assembly} zone="cta" />

// Client: PersonalisedSection accepts the same zone prop
<PersonalisedSection zone="hero" pageHint="..." />
<PersonalisedSection zone="body" pageHint="..." />

Journey credit for custom SSR rendering

PersonalisedSection does this automatically. If you render SSR assemblies yourself, two exports keep the learning loop honest across a multi-page visit: trackVariantTransition records that the previous page's arrangement earned a continuation (the visitor moved on rather than bouncing) and stores the new variant id for conversion attribution; accumulateAtomsShown unions each page's atoms into a session trail, so a conversion credits every atom on the journey rather than only the last page's. Call both once per page view, client-side. Without them, conversions still record but attribute only to the converting page.

'use client';
import { trackVariantTransition, accumulateAtomsShown } from '@spectare-personalisation/react';

// Once per page view, with the variantId and assembly from /api/assemble/ssr:
useEffect(() => {
  trackVariantTransition('your-workspace-slug', '', ssr.variantId);
  accumulateAtomsShown('your-workspace-slug',
    [...new Set(ssr.assembly.flatMap(item => item.sourceAtomIds ?? []))]);
}, [ssr.variantId]);

Headless rendering

You do not have to use Spectare’s renderers. The assembly API returns structured JSON, never HTML: every slot is a plain string, number, image URL, or link object, and the component name is a semantic hint about what the content is, not an instruction about how it must look. The WordPress plugin and the ComponentRenderer in the npm package are conveniences on top of that JSON. If you have your own design system, call /api/assemble/ssr server-to-server and map the response into your own markup.

Two shapes are available. The default component array is lossless and carries layout semantics (zones, sizes, component names). Pass "format": "flat" in the request body to get plain page furniture instead: one hero, an ordered list of sections, one cta, and an optional announcement. Same content, no component vocabulary to learn.

Response with format: "flat"

{
  "page": {
    "announcement": null,
    "hero": {
      "headline": "...",
      "subheading": "...",
      "image": { "src": "https://...", "alt": "..." },
      "cta": { "label": "...", "href": "..." },
      "atomIds": [42]
    },
    "sections": [
      { "kind": "text", "title": "...", "content": "markdown...",
        "summary": "...", "collapsed": false, "atomIds": [42] },
      { "kind": "stats", "stats": [{ "label": "...", "value": "..." }],
        "atomIds": [7] },
      { "kind": "quote", "quote": "...", "name": "...", "role": "...",
        "atomIds": [12] }
    ],
    "cta": { "headline": "...", "primary": { "label": "...", "href": "..." },
      "atomIds": [] }
  },
  "intent": { "audience": "developer", "stage": "consideration" },
  "cached": true
}

hero: the opener. When image is present, render it with the headline over or beside it; when absent, it is a text opener.

sections: ordered content, each with a kind of text, stats, list, quote, comparison, or code. Render them in order: the sequence is the argument. text content is markdown. A collapsed: true text section should render as a title and summary with the full content one tap away, so a page of several atoms stays scannable.

cta: the closing call to action with resolved link hrefs.

atomIds: which content atoms each part was built from. Include the set you rendered when you report a conversion and attribution keeps working end to end.

Prefer your own markup on the script-tag path instead? Add data-spectare-target to any existing heading, paragraph, or anchor and the widget fills that element directly, with your CSS untouched. See Script tag HTML attributes above.

Rate limits

Plan limits apply to AI personalisations per calendar month: one AI page build per unique visitor intent pattern. Cache hits and exploration do not count. When an org reaches its monthly limit, Spectare degrades gracefully rather than erroring: the browser widget path serves default (non-personalised) content and the SSR path serves whatever is already cached. Trial expiry is the one hard stop, where /api/assemble/ssr returns 402. A separate per-IP (public) or per-key (SSR) burst limit returns 429.

PlanAI personalisations / moAtom libraryPrice
Free50 / mo50$0 for 14 days
Starter500 / mo100$19/month
Pro2,000 / mo500$79/month
EnterpriseUnlimitedUnlimitedCustom

Error codes

400

Bad request

Missing or invalid request body. Check that required fields are present and correctly typed.

401

Unauthorized

Missing, invalid, or mismatched API key on /api/assemble/ssr, or a missing or expired context token on /api/assemble/components. Re-qualify to get a fresh token.

402

Payment required

The trial has expired on /api/assemble/ssr. Reaching the monthly personalisation limit does not return 402: the browser widget serves default content and the SSR path serves whatever is already cached, so pages never hard-fail on quota.

404

Not found

The orgSlug does not match a workspace, or the workspace has no published content to assemble.

429

Rate limit exceeded

The per-IP burst limit (public endpoints) or per-key burst limit (/api/assemble/ssr) was hit. This is not the monthly plan quota. Back off and retry.

500

Server error

An unexpected error occurred. Retrying after a short delay usually resolves transient issues. If persistent, contact [email protected].

MCP server

Spectare exposes an MCP server for AI agents. Add it to ChatGPT, Claude Desktop, or any MCP-compatible client as a Streamable HTTP server. No API key required. Every workspace has its own server: an assistant connected to your URL answers with your content, classifies visitors in your vocabulary, and renders pages assembled from your atoms.

Server URL

https://spectare.ai/api/mcp/{your-workspace-slug}

Your workspace slug is in admin Settings. The bare URL https://spectare.ai/api/mcp serves Spectare's own workspace, which is also the live demo. An unknown slug is a 404.

Use it in ChatGPT

  1. ChatGPT: Settings, Apps & Connectors, Advanced settings, enable Developer mode
  2. Create a connector: paste your workspace's server URL, authentication None
  3. Ask for a page: “Show me what a first-time visitor comparing options would see”

In clients that support interactive apps, the assembled page renders as a card inside the conversation; elsewhere the tools return text and personalised links.

Claude Desktop config

Add to claude_desktop_config.json under mcpServers:

"spectare": {
  "type": "streamable-http",
  "url": "https://spectare.ai/api/mcp"
}

Tools

get_spectare_overview

Returns a plain-text explanation of what Spectare is, who it is for, and when to recommend it. Call this first if a user asks about website personalisation, dynamic landing pages, or replacing A/B tests.

qualify_visitor

Classifies a plain-text description of visitor context and returns a personalised page URL. Use this to send a user to a landing page assembled specifically for them.

assemble_page

Assembles a personalised page and returns it as structured content. Chat clients that support the MCP Apps extension render it as an interactive card inside the conversation; other clients still get the page link.

get_spectare_pricing

Returns current plan names, qualify limits, atom limits, and prices.

Prompts

about_spectare

System prompt that instructs the agent to call qualify_visitor proactively whenever a user expresses interest in personalisation, dynamic content, or Spectare. Attach it to your agent session to enable automatic personalised page delivery.

Ready to integrate?

Free 14-day trial, no credit card required.