name: hooks-api description: "Extension hooks and API reference: createExtension, Surface, useCapabilities, createStore, useContextData. Use when writing extension runtime code."
Hooks & API Reference
All imports from @stackable-labs/sdk-extension-react.
createExtension(factory, options?)
Bootstrap the extension runtime. Call once in src/index.tsx.
factory: () => React.ReactElement— render function returning all surfacesoptions?: { extensionId?: string }— extension identifier
import { createExtension } from '@stackable-labs/sdk-extension-react'
import { Header } from './surfaces/Header'
import { Content } from './surfaces/Content'
import { Footer } from './surfaces/Footer'
const Extension = () => (
<>
<Header />
<Content />
<Footer />
</>
)
// NOTE: extensionId is optional — used when connected to a registered extension
createExtension(() => <Extension />, { extensionId: 'my-extension' })
useCapabilities()
Returns the capabilities object for calling host-mediated APIs.
const capabilities = useCapabilities()
// capabilities.context.read() — includes identity state in response
// capabilities.data.query(payload)
// capabilities.data.fetch(url, init?)
// capabilities.actions.toast(payload)
// capabilities.actions.invoke(action, payload?) — actions: newConversation, setConversationTags, setConversationFields, open, close, show, hide
// capabilities.identity.extend(patch) — push enrichment claims to user.metadata + JWT custom_claims (imperative; for handler-style at login, use useExtendIdentity hook). Each patch key MUST be declared in manifest.identityClaims or the host filter drops it.
// capabilities.messaging.send(payload) — post a message into the active conversation bound to this Instance. Prefer the useMessaging hook instead. Requires messaging:send permission. Author label is admin-set per-Instance (instance.config.settings.messagingDisplayName) with extension.manifest.name fallback.
useStore(store, selector?)
Subscribe to a shared store. Re-renders when the selected state changes.
const viewState = useStore(appStore, (s) => s.viewState)
createStore(initialState)
Create a shared store for cross-surface state coordination.
const appStore = createStore<AppState>({ viewState: { type: 'menu' } })
Store<T> interface
get(): T— read current stateset(partial: Partial<T>): void— merge partial state updatesubscribe(listener: (state: T) => void): () => void— subscribe, returns unsubscribe fn
useContextData()
Reads host-provided context including extension settings. Returns { loading, customerId, customerEmail, messaging, settings, ... }. messaging.conversationId is the active Messaging conversation ID (or null until one exists).
const { loading, customerId, customerEmail, messaging, settings } = useContextData()
const conversationId = messaging?.conversationId
useSettings()
Convenience hook for reading extension settings. Returns non-secret settings scoped to this extension on this instance.
const settings = useSettings()
const apiBaseUrl = settings.baseUrl as string
useSurfaceContext()
Returns host-provided context specific to the current surface slot.
const surfaceContext = useSurfaceContext()
useExtension()
Returns extension-level context.
const { extensionId } = useExtension()
useEvent(eventType, handler)
Generic cross-domain event hook (should not be used unless absolutely required). Subscribe to any event using fully-qualified event types.
eventType: EventType— fully-qualified (e.g.,'activity:product_view','identity:login','messaging:postback')- Domain wildcard (e.g.,
'activity') receives all events in that domain handler: (event: BaseEvent) => void
useEvent('activity', (event) => {
console.log('Activity:', event.data)
})
useMessaging()
Send messages into the active conversation bound to this Instance. Wraps the messaging.send capability with React state, tracking enabled / loading / error / data. Returns a tuple [send, state] so callers can rename send when the hook is used multiple times in one component. Requires messaging:send permission. The author label rendered above outbound messages is set per-Instance by the admin (Instance settings messagingDisplayName); falls back to extension.manifest.name when blank.
- Returns:
readonly [send, { enabled, loading, error, data }] send(payload: SendMessagePayload): Promise<SendMessageResponse | null>— returns the response on success; throws on actionable errors; resolves tonullon host-handled errors (SDK logs a breadcrumb, host surfaces remediation)enabled: boolean— advisory;truewhen an active conversation exists. Permission-free (nocontext:readneeded). Wire to buttondisabledto pre-empt the silentno_conversationcase. Startsfalse; flips reactively via host-pushed availability events.loading: boolean— true while a call is in flight (matchesuseContextData'sloadingfor SDK-wide consistency)data: SendMessageResponse | null—{ messageId, receivedAt }from last successful senderror: SendMessageActionableErrorCode | null— one ofinvalid_message/rate_limited/upstream_errorafter a failed send. Host-handled codes (no_conversation/reauth_required/forbidden) never surface here.
Payload is discriminated by kind: 'text' / 'image' / 'file' / 'carousel'. See the messaging.send capability section for the full payload table + action types.
import { useMessaging } from '@stackable-labs/sdk-extension-react'
const [send, { enabled, loading, error }] = useMessaging()
const onApprove = async () => {
try {
await send({ kind: 'text', body: 'Approved ✓' })
} catch {
// error holds the typed SendMessageActionableErrorCode
}
}
return (
<button disabled={!enabled || loading} onClick={onApprove}>
Approve
</button>
)
useMessagingEvent(eventType, handler)
Subscribe to messaging events (e.g. postback button clicks) pushed from the host widget. Requires events:messaging permission and matching entries in manifest events array.
eventType: 'postback' | 'postback:<actionName>'handler: MessagingEventHandler—(event: MessagingEvent) => voidMessagingPostbackEvent: { eventName: 'postback', data: { actionName: string, conversationId: string, timestamp: string } }'postback'receives ALL postback events (requires elevated marketplace review)'postback:<actionName>'receives only events matching the specific actionName
useMessagingEvent('postback:Buy Now', (event) => {
console.log('Postback:', event.data.actionName, event.data.conversationId)
})
With useCallback (for memoized handlers):
import { useCallback } from 'react'
import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
const handlePostback = useCallback<MessagingEventHandler>((event) => {
console.log('Postback:', event.data.actionName, event.data.conversationId)
}, [])
useMessagingEvent('postback:Buy Now', handlePostback)
useActivityEvent(eventType, handler)
Subscribe to activity events pushed from the host via the framework. Requires events:activity permission and matching entries in manifest events array.
eventType: 'click' | 'page_view' | 'form_submit' | 'product_view' | 'add_to_cart' | 'purchase' | 'search' | '*'(domain-stripped)handler: ActivityEventHandler—(event: ActivityEvent) => voidActivityEvent: { eventName: string, data: Record<string, unknown> }'*'receives ALL activity events
useActivityEvent('product_view', (event) => {
console.log('Activity:', event.eventName, event.data)
})
useIdentityEvent(eventType, handler)
Subscribe to identity events pushed from the host via the framework. Requires events:identity permission and matching entries in manifest events array.
eventType: 'login' | 'logout' | 'refresh' | 'expired'handler: (event: IdentityEvent) => voidIdentityEvent: { eventName: IdentityEventType, data: { state: IdentityState, timestamp: string } }IdentityState: { authenticated: boolean, user: UserIdentity | null, expiresAt?: string }
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
useIdentityEvent('login', (event) => {
// event.data.state.user.metadata is populated with any enrichment from sibling
// extensions with identity:extend (declared in their manifest.identityClaims)
console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
})
useIdentityEvent('logout', () => {
console.log('User logged out')
})
// identity:refresh fires after any extension calls capabilities.identity.extend({...}).
// Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
useIdentityEvent('refresh', (event) => {
console.log('Identity refreshed — metadata:', event.data.state.user?.metadata)
})
useExtendIdentity(handler)
Register a handler to enrich identity JWT claims before signing. Requires identity:extend permission.
handler: ExtendIdentityHandler—(claims: IdentityBaseClaims) => Record<string, unknown> | Promise<Record<string, unknown>>IdentityBaseClaims: { external_id: string, email?: string, name?: string, [key: string]: unknown }
// manifest.json:
// {
// "permissions": ["identity:extend"],
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
// }
// Standard JWT claims (external_id, email, name) are exempt from declaration.
// Custom claims must be in manifest.identityClaims or they're dropped with a warn.
//
// Fires ONCE at initial login — return what's known synchronously. For post-login
// async updates (e.g., after verification completes via a webhook or polling),
// use capabilities.identity.extend(patch) — see the 'identity.extend' capability.
useExtendIdentity((claims) => ({
external_id: `custom_${claims.external_id}`, // standard claim override (exempt)
loyalty_tier: 'bronze', // custom — sync, known at login
verified: false, // custom — default; updated async post-verification
}))
With useCallback (for memoized handlers):
import { useCallback } from 'react'
import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
import type { ExtendIdentityHandler } from '@stackable-labs/sdk-extension-contracts'
// manifest.json:
// {
// "permissions": ["identity:extend"],
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
// }
const handleExtend = useCallback<ExtendIdentityHandler>((claims) => ({
external_id: `custom_${claims.external_id}`, // standard claim override (exempt)
loyalty_tier: 'bronze', // custom — sync, known at login
verified: false, // custom — default; updated async post-verification
}), [])
useExtendIdentity(handleExtend)
Identity via context.read()
Identity state is available in the context.read() response as an identity field. Requires context:read permission (no separate identity permission needed).
const context = await capabilities.context.read()
// context.identity — { authenticated, user, expiresAt? }