hooks-api

star 0

Extension hooks and API reference: createExtension, Surface, useCapabilities, createStore, useContextData. Use when writing extension runtime code.

stackable-labs By stackable-labs schedule Updated 6/6/2026

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 surfaces
  • options?: { 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 state
  • set(partial: Partial<T>): void — merge partial state update
  • subscribe(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 to null on host-handled errors (SDK logs a breadcrumb, host surfaces remediation)
  • enabled: boolean — advisory; true when an active conversation exists. Permission-free (no context:read needed). Wire to button disabled to pre-empt the silent no_conversation case. Starts false; flips reactively via host-pushed availability events.
  • loading: boolean — true while a call is in flight (matches useContextData's loading for SDK-wide consistency)
  • data: SendMessageResponse | null{ messageId, receivedAt } from last successful send
  • error: SendMessageActionableErrorCode | null — one of invalid_message / rate_limited / upstream_error after 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) => void
  • MessagingPostbackEvent: { 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) => void
  • ActivityEvent: { 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) => void
  • IdentityEvent: { 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? }
Install via CLI
npx skills add https://github.com/stackable-labs/claude-plugins --skill hooks-api
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
stackable-labs
stackable-labs Explore all skills →