Everything a host app needs to embed the checklist: one script tag, the whole
attribute and event surface, and every HTTP call the widget makes — with the
payloads each one takes and returns.
Base URL https://gettingstarted.marthaia.comREST prefix /api/gs/v1Bundle /sdk/v1/gs.min.js
Quick start
Three things: load the bundle, mount the element, and tell the platform when a
step is really finished. The checklist itself — steps, their texts, the theme —
lives in the console, not in your code.
1. Load the bundle and mount the widget
Paste this into the layout your authenticated app already has. The element
boots itself: it reads the project config, generates a visitor id for people you
don't know yet, fetches their progress, and renders.
project-key is the project's publishable key (pk_…).
It ships to browsers by design; the secret key never does.
user-id is optional. Omit it while you don't know who is looking and
the widget tracks an anonymous visitor instead; hand it over later and the
anonymous history is merged into that user once.
disabled on the element renders nothing — a kill switch you can
ship ahead of a rollout.
2. Report a step from your app
The SDK tells you when a step is clicked, but almost nothing is finished by a
click. When the real event happens in your own code, say so and the widget
re-renders:
Your app's JavaScript
const gs = document.querySelector('getting-started')
await gs.complete('first_project') // POST /complete → the new state
console.log(gs.state.progress.percent)
3. Or report it from your backend
For events your browser never sees — a payment settled, an integration
connected — use the secret key server-to-server. It never leaves your server.
Both keys are in the console at /preview/keys — every section
has its own URL (/preview/steps, /preview/users,
/preview/stats, …), so a page can be bookmarked, shared and reloaded.
A key is handed over when it is created, and it can be shown again at any
time from that screen: the platform keeps an encrypted copy beside the hash
it authenticates with, and only your account can read it. When a credential
has to be replaced, rotate it rather than
revoking it and starting over.
Embedding
The widget is one custom element, <getting-started>, built
from a plain script tag with no bundler and no framework, and a thin React
wrapper for hosts that would rather pass props.
Attributes
Every attribute is observed. Values in the Default column marked
"theme" come from the project config fetched at boot, not from the element.
The attributes of the getting-started element
Attribute
Type
Default
What it does
endpoint
string
— required
The platform base URL. The SDK appends /api/gs/v1. Changing it reboots the widget.
project-key
string
— required
The project's publishable key, pk_…. Optional only when a host injects configOverride.
user-id
string
empty
Your opaque id for the signed-in user. Enables progress across devices. Going from empty to a value calls /identify exactly once.
visitor-id
string
generated: v_ + 24 base62 chars, kept in localStorage['gs_<projectId>_visitor_id']
Overrides the auto-generated visitor id. Sent on every call.
locale
string
browser language → project defaultLocale
Forces one of the project's locales.
meta
JSON object
absent
Small, host-supplied facts about this person — a plan, a role, a cohort. Stored with the subject and sent on every call; see Metadata.
theme
auto | light | dark
theme mode (default auto)
Sets color-scheme and data-mode on the widget root. The palette itself always comes from the project theme.
position
right | left
theme position (default right)
Which edge the desktop panel docks to.
layout
auto | panel | drawer | peek | none
auto
auto picks desktopLayout or mobileLayout for the current viewport; none renders nothing.
z-index
number
theme zIndex (default 2147483000)
Stacking order, for hosts that already own the top of the page.
disabled
boolean (presence)
absent
Present, whatever its value: the element renders nothing and never boots.
Host behaviour
What reboots the widget:endpoint,
project-key, locale and visitor-id — the
config and state are read again. theme, position,
layout and z-index re-render in place.
Two bundles, one element:/sdk/v1/gs.min.js (IIFE —
registers the tag and exposes window.GS) and
/sdk/v1/gs.esm.js (ESM). Load one of them, never both: each carries
its own copy of the class and the first registration wins.
npm, if you prefer:import '@gs/web' registers
<getting-started>; defineGettingStarted('my-checklist')
picks a different tag name.
SSR-safe: importing @gs/web on the server touches no
window and no document; the React wrapper renders the
element tag and hydrates on the client.
Shadow DOM: the widget owns its styles, so it cannot collide with
yours. Hosts style it through the --gs-* custom properties and the
part="…" hooks — see Theming.
Accessibility: the panel and the sheet are labelled dialogs, the
sheet traps focus, Esc closes both, the mobile peek is a
role="button" operable with Enter/Space,
and prefers-reduced-motion disables the entry animations.
No third-party requests: the bundle has zero runtime
dependencies and calls nothing but your endpoint.
Metadata
A checklist is rarely just a checklist: which plan someone is on, whether they are
an admin, which cohort they signed up in. Pass those facts as meta and
they are stored with the subject, sent back on every read, and visible in the
console's Users tab. The server never interprets them.
Any call may carry metadata, and what arrives is merged into what is stored. From
the imperative API:
// Identifying is the natural moment to say who this user is.
await widget.identify('u_4821', { plan: 'team', seats: 12 })
// Or later, when the host learns something new — this persists immediately.
await widget.setMeta({ plan: 'enterprise' })
// Read back what every call currently carries.
widget.meta
Shape
A flat JSON object of strings, numbers, booleans or null. Nested objects and arrays are refused.
Size
At most 1 KB of serialised JSON, 24 keys, 40-character key names and 200-character string values. A bigger payload is refused with meta_too_large, never truncated.
Merging
Keys merge over what is already stored. Sending null for a key deletes it.
Never sent
Omit meta and nothing changes: existing metadata is kept, and a subject that never had any has no meta field at all.
Not for secrets
This is not a place for personal data or credentials — it is handed to the widget in plain sight and rendered in the console.
A malformed meta attribute is reported through the
gs:error event rather than silently ignored.
React
The wrapper maps props to attributes (projectKey →
project-key), events to callbacks, and exposes the imperative API
through a ref. React is a peer dependency; it is never bundled.
App.tsx
import { GettingStarted, useGettingStarted } from '@gs/react'
export function App({ me }) {
return (
<GettingStarted
endpoint="https://gettingstarted.marthaia.com"
projectKey="pk_your_publishable_key"
userId={me?.id}
locale="en"
onStepCompleted={(key, state) => console.log(key, state.progress)}
onCompletedAll={() => celebrate()}
onError={(error) => console.warn('gs', error.code, error.message)}
/>
)
}
// imperative API + live state
function Progress() {
const gs = useGettingStarted()
return (
<>
<getting-started ref={gs.ref} endpoint="https://gettingstarted.marthaia.com"
project-key="pk_your_publishable_key" user-id="u_1234" />
<p>{gs.state?.progress.percent}%</p>
<button onClick={() => gs.complete('first_project')}>I shipped it</button>
</>
)
}
React props and callbacks
Prop
Type
Maps to
endpoint
string (required)
attribute endpoint
projectKey
string (required)
attribute project-key
userId
string | null
attribute user-id
visitorId
string | null
attribute visitor-id
locale
string | null
attribute locale
theme
auto | light | dark | null
attribute theme
position
right | left | null
attribute position
layout
auto | panel | drawer | peek | none | null
attribute layout
zIndex
number | string | null
attribute z-index
disabled
boolean
attribute disabled
configOverride
config object | null
element property (draft mode)
className, style
string / object
the element itself
onLoaded
(detail) => void
gs:loaded
onStepClicked
(detail) => void
gs:step-clicked
onStepCompleted
(key, state) => void
gs:step-completed
onOpened, onClosed, onDismissed
() => void
the matching events
onCompletedAll
(state) => void
gs:completed-all
onError
(error) => void
gs:error
onStateChange
(state) => void
every state the wrapper observes
The wrapper's ref (and useGettingStarted()) exposes
element, state, complete,
identify, refresh, open,
close, dismiss, reset,
getState, getConfig and setState.
Events
Every event is a bubbling, composed CustomEvent dispatched on the
element, so a listener anywhere in your app can hear it.
SDK events and their payloads
Event
detail
Fired when
gs:loaded
{ subjectId, state }
The widget has a config and a state: at boot, and after refresh().
gs:step-clicked
{ key, actionUrl }
A pending step was clicked. The link still navigates — the SDK does not complete the step for you.
gs:step-completed
{ key, state }
complete(key) succeeded, with the state it returned.
gs:opened / gs:closed
{}
The panel or sheet opened or closed.
gs:dismiss-requested
{ percent }
The visitor pressed dismiss on an unfinished checklist, so the confirmation is on screen.
gs:dismissed
{ scope }
A dismissal happened. scope: "visit" is the confirmation's "later" answer — hidden until the next visit, nothing stored; scope: "permanent" is dismiss() or "never", and is stored on the subject.
gs:completed-all
{ state }
The last required step was completed.
gs:error
{ code, message, details?, status? }
Any failed call. code is an API error code, or one the SDK raises itself.
The SDK's own error codes, which never come from the API, are
network_error (the request failed or timed out),
invalid_response (a response missing its data envelope),
not_configured (no config, no endpoint or no key),
unsupported and sdk_error.
JavaScript API
Everything the widget does is available to the host on the element itself.
Methods
The element's methods
Signature
Returns
Does
complete(stepKey: string)
Promise<State | null>
POST /complete for this subject, then re-renders. Idempotent: completing a step twice returns the same state.
identify(userId: string, meta?: object)
Promise<State | null>
POST /identify — links the anonymous visitor to this user id and merges the history. The optional meta is merged into what the subject already carries.
setMeta(meta: object)
Promise<State | null>
Merges metadata into what every subsequent call sends, and persists it straight away. Use it when the host learns something mid-session that the snippet's meta attribute could not know.
refresh()
Promise<State | null>
Re-fetches the state and emits gs:loaded. Useful after a server-side completion.
open()
void
Opens the panel or the bottom sheet, and emits gs:opened.
close()
void
Closes it and emits gs:closed.
dismiss()
Promise<State | null>
POST /dismiss — hides the mobile peek, persisted on the subject.
reset()
Promise<State | null>
POST /reset — clears every step this subject completed.
The methods that talk to the API reject with the error they got, and
emit gs:error. Await them in a try/catch if a failure
needs to change what your app does next.
Properties
The element's properties
Property
Type
What it is
state
State | null (read-only)
The current state, in the shape of GET /state. null before boot.
config
Config | null (read-only)
The resolved config the widget is rendering from.
subjectId
string | null (read-only)
This visitor's subject on the server. Store it if you need to look the person up later.
visitorId
string (read-only)
The visitor id actually in use — the generated one, or the visitor-id attribute.
locale
string (read-only)
The locale in use after resolution.
draft
boolean (read-only)
True when the config was injected rather than fetched.
version
string (read-only)
The running SDK bundle version — not the config version, which is state.version.
configOverride
Config | null (settable)
A whole config object to render instead of fetching one. Setting it marks draft.
setState(next)
(Partial<State> | null) => void
Replaces the local state and re-renders, with no request.
Draft previews
The console's Preview tab builds the widget with unsaved changes by injecting the
config as a property. With no endpoint and no
project-key, nothing ever leaves the browser:
Draft mode, no network
const el = document.createElement('getting-started')
el.configOverride = myConfig // a whole /config payload
el.setAttribute('locale', 'en')
document.body.appendChild(el)
el.setState({ completed: ['first_project'] }) // drive the UI locally
These two members are additive: with both endpoint and
project-key set, state calls still reach the server; without them,
complete, dismiss and reset change only the
local state.
REST API
Everything the widget does is a documented HTTP call you can make yourself —
from a browser with the publishable key, from your backend with the secret key,
and from the console with your account session.
Conventions
API conventions
Concern
How it works
Base URL
https://gettingstarted.marthaia.com/api/gs/v1
Content type
application/json; charset=utf-8 on every POST body.
X-GS-Key: pk_…, or ?key=pk_… for callers that cannot set headers (an EventSource). A pk_ key reaches every public endpoint and nothing else.
Secret key
X-GS-Secret: sk_… only. A secret key in a URL is rejected — URLs leak into logs, history and referrers.
Account session
The /admin/* endpoints take your console sign-in token in the Authorization header. The account must own the project; anything else answers 404 project_not_found rather than admitting the project exists. Operator (superuser) tokens are rejected here — operations go through the PocketBase dashboard.
Identity
A request must carry userId and/or visitorId; the pair resolves to one subject, and a new one is created on first sight. userId wins when both are known.
Origins
Browser calls are checked against the project's origin allowlist (Origin header). Server-to-server calls are exempt. Every response varies on Origin.
Caching
Only /config is cacheable: it returns an ETag and Cache-Control: private, max-age=60, and answers 304 to a matching If-None-Match. State is never cached.
Preflight
OPTIONS on an API path is answered 204 with the CORS headers, once the request has passed the same key and origin checks as the call it precedes.
Rate limits
Exceeding a budget answers 429 with a Retry-After header
(seconds) and the rate_limited code.
Per-route request budgets
Route
Budget
Counted per
GET /config
300 / minute
key + IP
GET /state
120 / minute
key + IP
GET /stream
shares the /state budget
key + IP
POST /complete
30 / minute
key + subject
POST /dismiss
10 / minute
key + subject
POST /reset
10 / minute
key + subject
POST /identify
20 / minute
key + IP
POST /server/complete, POST /server/state
600 / minute
key
14 endpoints
Publishable key — the widget's own calls
These seven are what the widget calls, and what you may call with a
publishable key from a browser. A secret key works here too.
GET/api/gs/v1/config
publishable or secret key
The assembled project configuration: which terminal the panel docks to, every
step with its texts resolved for one locale, and the theme. Served from an
in-memory cache, so a cache hit touches the database zero times.
Query
Type
Meaning
locale
string, optional
One of the project's locales. An unknown locale falls back to defaultLocale.
enabled is always present; eventName appears only when
the step sets it. Editing a project, a step or a translation bumps
version and therefore the ETag, so a cached copy costs one cheap
304.
GET/api/gs/v1/state
publishable or secret key
One subject's progress: what they finished, what is next, and how far along
they are. This is the object every mutating endpoint returns.
Query
Type
Meaning
userId
string, optional
Your opaque id for the signed-in user.
visitorId
string, optional
The anonymous visitor id. At least one of the two is required.
locale
string, optional
Locale for the state that comes back — its locale, and the labels the widget will render from the config.
The subject this identity resolves to. Opaque and stable; it is a record id, not a prefixed token.
locale
The locale actually served, after resolution.
meta
The host metadata stored for this subject (§9), omitted entirely when the client has never sent any. See Metadata.
dismissed
True once the mobile peek was dismissed for this subject.
completed
The step keys this subject has finished.
steps
Every enabled step with its completion flag, in configured order.
progress.completed / progress.total
Finished vs. total required, enabled steps.
progress.nextKey
The first enabled step still pending, or "" when there is none. This is what the mobile peek shows.
progress.percent
round(completed / total × 100); 0 when the project has no required steps.
version
The project's config version at the time of the read.
disabled
True when an operator switched this person off from the console's Users tab. Not part of the spec. While it is true the widget renders nothing for them; identity, aliases and completed steps are untouched, and clearing the flag restores them exactly as they were.
POST/api/gs/v1/complete
publishable or secret key
Marks one step complete for this subject. Idempotent: a second call for the
same step returns the same state and records nothing new.
Body field
Type
Meaning
stepKey
string — required
The step's stable key. It must belong to the project and be enabled, otherwise 400 invalid_step.
userId
string, optional
Your opaque user id.
visitorId
string, optional
The visitor id. At least one of the two is required.
Finishing the last required step stamps completed_at on the subject
and publishes an event on the stream.
POST/api/gs/v1/dismiss
publishable or secret key
Hides the mobile peek for this subject, stored on the subject — so it survives
a reload and a new device. The desktop trigger stays available as a floating
action button in the bottom-right corner of the page; on
mobile, reopen it with el.open() from your own trigger.
Body field
Type
Meaning
userId
string, optional
Your opaque user id.
visitorId
string, optional
The visitor id. At least one of the two is required.
Clears every step this subject completed, and with it the dismissal and the
completion stamp. Meant for "start over" affordances and for tests — not for
your users' logout.
Body field
Type
Meaning
userId
string, optional
Your opaque user id.
visitorId
string, optional
The visitor id. At least one of the two is required.
Links an anonymous visitor to a real user id, so someone who started before
signing up keeps what they finished. The visitor's progress moves to the user's
subject, the earliest completion and dismissal win on duplicates, and the
emptied subject is deleted — all in one transaction.
Body field
Type
Meaning
visitorId
string — required
The anonymous id to absorb. Both identities are required here; one alone answers 400 missing_identity.
The widget calls this for you the moment user-id goes from empty to
a value.
GET/api/gs/v1/stream
publishable or secret key
Server-sent events for one subject, so a step completed in your backend moves
the checklist in an open browser tab. One event carries the whole completion
list, not a delta.
Query
Type
Meaning
userId / visitorId
string, optional
The subject to follow. At least one is required.
locale
string, optional
Locale for the opening state.
key
string
Required in practice: EventSource cannot set the X-GS-Key header.
A heartbeat comment arrives every 25 seconds; the stream closes when the client
disconnects. The bus behind it lives in the process, so this is a
single-instance feature. The SDK does not consume the stream: the widget
refreshes on its own state-changing calls, and refresh() is the
supported way to pull a server-side change in.
GET/api/gs/v1/themes
No key required.
The ready-made themes, so a host can start from a complete look instead of
designing one. Each preset is a whole theme — colours, radius, font, texts and
icons — and can be copied into a project's theme as it stands.
The console offers the same list as swatches on its Theme tab.
martha is drawn from marthaia.com's own palette (accent
#EF562F over #FFF5F2, Instrument Sans);
mono, indigo, forest,
midnight and sunset cover the other common
directions. Applying a preset replaces the look and leaves your texts and
icons alone.
Secret key — server-to-server
For your backend. A publishable key is refused here with
401 secret_required, and these calls skip the origin allowlist
because the caller is not a browser.
POST/api/gs/v1/server/complete
secret key only
The same completion as POST /complete, recorded with source
server so you can tell backend-driven progress from clicks. No
locale is needed: title, texts and the browser are none of a backend's business.
Header / body
Type
Meaning
X-GS-Secret
header, required
The project's secret key, sk_….
stepKey
string — required
The step's stable key. Unknown or disabled → 400 invalid_step.
What the console calls on your behalf. Every path parameter is a record id, and
the project must belong to the account you signed in with.
POST/api/gs/v1/admin/projects/{id}/keys
account session
Mints a key for a project. This is the only time the full key is ever
returned — the platform stores a SHA-256 hash and shows the first 12 characters
from then on.
Body field
Type
Meaning
type
publishable | secret, optional
Defaults to publishable. Anything else is 400 bad_request.
label
string, optional
A note for the keys list — "production", "staging".
Revokes a key for good. It stops resolving immediately, everywhere — the key
cache is dropped the moment the revocation is stored. A revoked key answers
401 revoked_key.
Returns the full key again, for a key that belongs to one of your projects.
Every key is stored twice — a hash to authenticate with, and a copy sealed
with the instance's GS_KEY_SECRET for exactly this call. It is a
read: revealing changes nothing, and the key stays valid. A key that cannot
be produced answers with a code saying so rather than a guess, which is what
the three 409s below mean.
Replaces a key in one step: the replacement is minted first, then the old
record is revoked, and both facts come back in the same response. This is the
way out of a key that cannot be revealed and the right answer to a credential
that has leaked, because there is never a moment with no working key. The
replacement carries the same type and label, and is revealable the same way.
A key that is already revoked cannot be rotated: it answers
409 revoked_key.
Completion rows, i.e. completed steps summed over subjects.
dismissed / dismissRate
Subjects that hid the peek, and their share of subjects.
completedAll
Subjects that finished every required step.
perStep
Completions per step key, counted from the stored progress rows.
GET/api/gs/v1/admin/projects/{id}/subjects
account session
The people this project has seen, most recently active first — the data behind the console's
Users tab. end_users carries no PocketBase rules, so this and the
console are the only ways to read it.
search
Optional. Matches a user id, a visitor id or a subject id, as a substring.
Switches one person on or off. A disabled subject keeps their identity, aliases and progress —
the widget simply renders nothing for them, which is what you want for a test account or to take
the checklist away from somebody without erasing their history. They still appear in
/stats.
Resets one subject's progress, for support cases — a returned widget, a demo
account. Same effect as POST /reset, reached by subject id instead
of by identity.
A subject of another project answers 404 subject_not_found; a project
of another account answers 404 project_not_found, so neither leaks
that the record exists.
No endpoint matches that — try a path, a method, a field name, or clear the box.
Theming
The theme lives on the project and arrives inside the config. The widget renders in
a shadow root, so your page's CSS cannot reach in, and only the properties below
reach out. A project that has never been themed still renders: every missing field
falls back to the default theme.
Ready-made themes
You do not have to design a theme to have one. Six presets ship with the product and
are also served at GET /api/gs/v1/themes. Each is a complete theme —
colours, radius, font, texts and icons — so applying one cannot leave a field behind.
The console's Theme tab shows them as swatches; pick one, then adjust
anything you like.
The ready-made themes
Preset
Accent
Mood
martha — Martha IA
#EF562F on #FFF5F2
Warm orange on light surfaces, from marthaia.com's palette, with Instrument Sans. The one to copy if you are embedding this on a Martha IA page.
indigo
#4F46E5 on white
The familiar SaaS look: calm, unopinionated, suits a product page.
forest
#15803D on off-white
Deep green that reads as growth without shouting.
midnight
#5B8DEF on #0E1116
Dark slate with a cool accent, for products that already live in the dark.
sunset
#FB923C on #1B1310
Amber on warm charcoal — dark, but still warm.
mono
#111827, radius 6
Near-black and square: quiet enough for documentation and internal tools.
Theme fields
The theme object's fields
Field
Type
Default
Controls
mode
auto | light | dark
auto
Whether the widget reports light or dark to the browser. auto follows the operating system unless your page sets data-theme="light|dark".
accentColor
CSS colour
#E5352B
The progress bar fill, the completed check, a pending step's CTA and hover border, the peek's arrow, focus outlines, and the drawer launcher.
accentTextColor
CSS colour
#FFFFFF
Text on the accent fill — the drawer launcher's label.
backgroundColor
CSS colour
#111111
The panel, the sheet and the peek bar.
surfaceColor
CSS colour
rgba(255,255,255,0.06)
Step rows and other raised blocks inside the panel.
textColor
CSS colour
#FFFFFF
Step labels and the panel title.
mutedTextColor
CSS colour
rgba(255,255,255,0.45)
Descriptions, done-text and the subtitle.
borderColor
CSS colour
rgba(255,255,255,0.08)
Hairlines between rows, and the panel's own edge.
radius
number (px)
16
Corner radius of the panel, sheet, rows and controls.
fontFamily
CSS font list
DM Sans, system-ui, sans-serif
The widget's type. Set a stack, never a single family.
panelWidth
number (px)
360
The desktop panel's width.
peekOffset
number (px)
74
How far the mobile peek sits above the bottom edge — set it to clear your bottom navigation.
progressStyle
bar
bar
The progress indicator in the panel header.
zIndex
number
2147483000
Stacking order of everything the widget draws.
breakpoint
number (px)
1024
The desktop/mobile switch. A host can also override it with --gs-breakpoint.
position
right | left
right
Which edge the desktop panel docks to. The position attribute wins over it.
icons
{ peek, close, arrow }
👋 / × / →
The three glyphs in the widget chrome.
texts
object of 9 strings
French defaults — see below
The widget's own copy, in the project's default locale.
textsByLocale
{ [locale]: texts }
absent
Per-locale overrides for those same strings. Non-empty fields win over texts.
Texts, and where each one appears
The shipped defaults are French, because the product's first project was — override
them for your own language. Step labels and descriptions are not here: they come from
each step's translations.
Widget chrome strings
Text field
Default
Where it shows
title
Bien démarrer
Panel header, the dialog's accessible name, the reopen handle
subtitle
{count, plural, =0 {Parcours terminé.} one {# action restante.} other {# actions restantes.}}
Under the progress bar; count is the number of steps still to do
nextCta
Prochaine étape
The mobile peek's leading line, above the next step's label
noteTitle
Vous n'avez pas besoin de suivre un ordre.
The note at the foot of the checklist
noteBody
Complétez les actions qui vous intéressent.
Under the note title
continue
Continuer
Reserved. It is part of the theme contract and the config carries it, but the shipped widget renders no element from it.
dismiss
Masquer
The peek's dismiss button and its label
close
Fermer
The panel's and sheet's close control
reopen
Bien démarrer
Label of the floating action button — bottom-right on desktop — that brings a collapsed panel back
dismissTitle
Masquer la checklist ?
Heading of the dismissal confirmation
dismissBody
{count, plural, =0 {Tout est terminé.} one {Il reste # action.} other {Il reste # actions.}} Votre avancement est enregistré.
Under it: the same ICU plural, plus the reassurance that nothing is lost
dismissLater
Revenir à ma prochaine visite
"Later": hidden for this visit, back at the next one, nothing written server-side
dismissNever
Ne plus l'afficher
"Never": the permanent dismissal, the same call dismiss() makes
dismissCancel
Annuler
Keeps the checklist open, with progress untouched
The subtitle is an ICU plural: {count, plural, =0 {…} one {# …}
other {# …}}, with count, completed,
total and percent available to it. The dismissal copy
takes the same variables.
Dismissing a checklist that is not finished asks first: it used to be one
silent click with no way back, so a visitor could lose their checklist without
meaning to. A checklist at 100% is still dismissed in one click, and
dismiss() — the host's own call — stays immediate either way.
A theme, in full
The project's theme field
{
"mode": "auto",
"accentColor": "#E5352B",
"accentTextColor": "#FFFFFF",
"backgroundColor": "#111111",
"surfaceColor": "rgba(255,255,255,0.06)",
"textColor": "#FFFFFF",
"mutedTextColor": "rgba(255,255,255,0.45)",
"borderColor": "rgba(255,255,255,0.08)",
"radius": 16,
"fontFamily": "DM Sans, system-ui, sans-serif",
"panelWidth": 360,
"peekOffset": 74,
"progressStyle": "bar",
"zIndex": 2147483000,
"breakpoint": 1024,
"position": "right",
"icons": { "peek": "👋", "close": "×", "arrow": "→" },
"texts": {
"title": "Getting started",
"subtitle": "{count, plural, =0 {All done.} one {# action left.} other {# actions left.}}",
"nextCta": "Next step",
"noteTitle": "You don't have to follow an order.",
"noteBody": "Complete the actions that matter to you.",
"continue": "Continue",
"dismiss": "Hide",
"close": "Close",
"reopen": "Getting started",
"dismissTitle": "Hide the checklist?",
"dismissBody": "{count, plural, =0 {All done.} one {# action left.} other {# actions left.}} Your progress is saved.",
"dismissLater": "Back at my next visit",
"dismissNever": "Don't show it again",
"dismissCancel": "Cancel"
},
"textsByLocale": {
"fr": { "title": "Bien démarrer", "nextCta": "Prochaine étape" }
}
}
Styling from the host
The theme is applied to the element as CSS custom properties, so a host can override
any of them from its own stylesheet. Values you set before the widget's first render
win over the config.
Your app's stylesheet
getting-started {
--gs-accent: #7C3AED;
--gs-panel-width: 400px;
--gs-peek-offset: 88px; /* clear your bottom nav */
--gs-breakpoint: 900px; /* host-only: where the peek takes over */
}
The properties are --gs-accent, --gs-accent-text,
--gs-bg, --gs-surface, --gs-text,
--gs-muted, --gs-border, --gs-radius,
--gs-font, --gs-z-index, --gs-panel-width
and --gs-peek-offset. --gs-breakpoint exists only for the
host — it is not part of the theme.
customCss on the project is injected into the shadow root after the
built-in stylesheet, for anything the properties cannot reach.
Structural parts carry part names — panel,
sheet, peek, handle, launcher,
step, backdrop — so you can style them with
::part() from your own CSS.
Errors
Every failure — validation, key, origin, limit — uses one envelope. The HTTP status
says how to react; code says what happened, and is stable enough to
branch on.
details appears only when there is something structured to say —
invalid_step carries stepKey, origin_not_allowed
carries origin, and rate_limited carries
retryAfterSeconds in addition to the Retry-After header.
Error codes, their statuses and their causes
Status
code
Raised when
400
bad_request
The body is not JSON, or a required field is empty — stepKey, most often, or a key type that is neither publishable nor secret.
400
invalid_step
stepKey is not a step of this project, or the step is disabled. Carries details.stepKey.
400
missing_identity
Neither userId nor visitorId was supplied — or, on /identify, only one of them was.
401
invalid_key
No key, an unknown key, or a secret key sent in the query string.
401
revoked_key
The key was revoked in the console.
401
secret_required
A publishable key called /server/*.
401
unauthorized
An /admin/* call without an account session.
403
origin_not_allowed
The browser's Origin is not in the project's allowlist. Carries details.origin.
403
project_paused
The project's status is paused: every public endpoint answers this until it is resumed.
403
forbidden
Account creation while public signup is switched off on the instance.
404
project_not_found
Admin: the project does not exist, or belongs to another account.
404
subject_not_found
Admin: the subject does not exist, or belongs to another project.
404
not_found
Admin: the key does not exist.
409
reveal_disabled
Reveal: this instance was started without GS_KEY_SECRET, so no key material is retained for any key.
409
key_not_revealable
Reveal: the key was minted before key material was retained, so the plaintext was never kept. Rotate it to get a key you can reveal.
409
key_material_unreadable
Reveal: the stored copy cannot be decrypted — GS_KEY_SECRET changed since this key was minted. Rotate the key.
409
revoked_key
Rotate: the key is already revoked. Rotation replaces a live credential; there is nothing to replace here.
429
rate_limited
A rate budget was exhausted. The Retry-After header carries the wait in seconds.
500
internal_error
Something on our side failed — a config that cannot be assembled, a write that would not commit. Safe to retry with backoff.
Two statuses are not errors: 304 from /config when your
If-None-Match still matches, and 204 from the CORS
preflight. One documented code, 409 conflict, is reserved but no v1
endpoint returns it.
AI prompt
The fastest installation is to hand the job to a coding assistant. Each
prompt below is self-contained — it names the endpoint, the file to edit
and the attributes to set — so the assistant answers with a diff instead
of a guess. Replace pk_… and sk_… with your own
keys from the console, and the step key with one of
the steps you configured there. These are the same three prompts the
landing page shows, from one file, so the site and this reference cannot
disagree about how the widget is installed.
Any stack — HTML, Rails, Django, Laravel
Add the Getting Started onboarding checklist to this project. Two edits, no package, no wrapper component.
1. Load the widget once, just before </body> in the app's main layout:
<script src="https://gettingstarted.marthaia.com/sdk/v1/gs.min.js" defer></script>
2. Render it once, in the layout that signed-in users see:
<getting-started
endpoint="https://gettingstarted.marthaia.com"
project-key="pk_REPLACE_WITH_MY_KEY"
user-id="REPLACE_WITH_MY_USER_ID"
locale="en"></getting-started>
Rules: user-id must be my app's stable id for the signed-in user, because that is what progress is stored against; if nobody is signed in yet, omit user-id and the widget keeps progress per browser. locale should follow the user's language. Do not restyle the widget with CSS — it renders inside a shadow root and takes its colours from the project theme.
Next.js / React
Wire the Getting Started checklist into this Next.js app (App Router).
1. Load the script once, in app/layout.tsx:
import Script from 'next/script'
<Script src="https://gettingstarted.marthaia.com/sdk/v1/gs.min.js" strategy="afterInteractive" />
2. Render the widget in the authenticated layout only, e.g. app/(app)/layout.tsx:
<getting-started
endpoint="https://gettingstarted.marthaia.com"
project-key="pk_REPLACE_WITH_MY_KEY"
user-id={session.user.id}
locale={locale} />
3. It is a custom element, so declare it once for TypeScript:
declare global {
namespace JSX {
interface IntrinsicElements {
'getting-started': { endpoint?: string; 'project-key'?: string; 'user-id'?: string; locale?: string }
}
}
}
Do not write a React wrapper or a context provider: the element fetches its own config and keeps its own state.
Server-side step completion
Mark a checklist step complete from my backend when the user actually does the thing, instead of waiting for them to click it.
POST https://gettingstarted.marthaia.com/api/gs/v1/server/complete
X-GS-Secret: sk_REPLACE_WITH_MY_SECRET_KEY
Content-Type: application/json
{"userId":"REPLACE_WITH_MY_USER_ID","stepKey":"REPLACE_WITH_MY_STEP_KEY"}
Call this after the transaction that performs the action commits. Treat a non-2xx as a logged warning, never as a reason to fail the user's action. Keep the secret key on the server — it must never reach the browser. The widget shows the change on its next state fetch.
Replace pk_… and sk_… with your own keys from the
console — and the step key with one of the steps you
configured there. Nothing in these prompts is tied to a framework version, and
none of them asks an assistant to invent an API: every endpoint they name is
documented in full in the reference.