Capabilities allow your extension to interact with the host platform—querying data, reading context, sending messages, and subscribing to events. This page walks you through adding a new capability to your extension.

## 1. Determine the capability

Identify which capability you need:

- `data.query` — host-mediated data requests (action name + params → host returns data)
- `data.fetch` — direct HTTP requests from the sandbox (requires allowedDomains)
- `context.read` — read host-provided context (customerId, customerEmail, messaging.conversationId, etc.)
- `actions.toast` — show toast notifications (success, error, info, warning)
- `actions.invoke` — invoke host actions (e.g., open new conversation)
- `messaging.send` — send messages into the active conversation bound to this Instance (text/image/file/carousel + reply/link/postback actions)
- `identity.extend` — enrich identity JWT claims before signing
- `events:identity` — subscribe to identity events (login, logout, refresh, expired)
- `events:messaging` — subscribe to messaging events (postback button clicks)
- `events:activity` — subscribe to activity events (page views, clicks, purchases)

## 2. Add permission to manifest.json

Add the corresponding permission to `packages/extension/public/manifest.json`:

- `data.query` → `"data:query"`
- `data.fetch` → `"data:fetch"`
- `context.read` → `"context:read"`
- `actions.toast` → `"actions:toast"`
- `actions.invoke` → `"actions:invoke"`
- `messaging.send` → `"messaging:send"`
- `identity.extend` → `"identity:extend"`
- `events:identity` → `"events:identity"` (also add entries to `events` array, e.g. `["identity:login", "identity:logout"]`)
- `events:messaging` → `"events:messaging"` (also add entries to `events` array, e.g. `["messaging:postback:Buy Now"]`)
- `events:activity` → `"events:activity"` (also add entries to `events` array, e.g. `["activity:product_view"]`)

Only add if not already declared.

**Note:** Identity state is available via `context.read()` → `identity` field (requires `context:read`, no separate permission).

## 3. If data.fetch — add allowedDomains

Provide the API domain(s) and add them to the `allowedDomains` array in manifest.json.

## 4. If data.fetch — create API wrapper

Create `packages/extension/src/lib/api.ts` with the API wrapper pattern:

```tsx
type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>

export function createApi(fetch: FetchFn) {
  return {
    async getData(): Promise<DataType> {
      const result = await fetch('https://api.example.com/data', { method: 'GET' })
      if (!result.ok) throw new Error(`Request failed: ${result.status}`)
      return result.data as DataType
    },
  }
}
```

## 5. Use the capability in a surface

### For data.*, context.*, and actions.* capabilities — use `useCapabilities()`:

```tsx
import { useCapabilities } from '@stackable-labs/sdk-extension-react'

const capabilities = useCapabilities()
// data.query:    capabilities.data.query({ action: 'getItems', ... })
// data.fetch:    const api = createApi(capabilities.data.fetch)
// context.read:  capabilities.context.read()  (or use useContextData() hook)
// actions.toast: capabilities.actions.toast({ message: 'Done!', type: 'success' })
// actions.invoke: capabilities.actions.invoke('newConversation', { tags: ['order'], fields: [{ id: 'field_id', value: 'val' }] })
```

### Special handling (events + identity.extend + messaging.send):

#### For events — use dedicated hooks (INSTEAD of useCapabilities direct):

Events are subscribed via `useIdentityEvent` / `useMessagingEvent` / `useActivityEvent` — never use `capabilities.events.*` directly (events are not part of the `capabilities` object).

#### For messaging.send — use the `useMessaging` hook:

`useMessaging` returns a `[send, { enabled, loading, error }]` tuple. The `send` function is the imperative call wrapped with React state + typed errors. Always use the hook — do not call `capabilities.messaging.send(...)` directly from `useCapabilities()`. The hook is the canonical pattern: state management (`enabled`/`loading`) + typed errors (`error`) + correct payload shape (`{ kind, body }` with the `kind` discriminator). Wire send buttons to `disabled={!enabled || loading}` — the `enabled` flag pre-empts the silent `no_conversation` case without declaring `context:read`. See the snippet below for the full error-handling surface (actionable codes vs. host-handled codes).

#### For identity.extend — choose the correct option:

- **`useExtendIdentity(handler)`** — synchronous hook, fires only once at initial login. Always use for known-at-login enrichment.
- **`capabilities.identity.extend(patch)`** — imperative call via `useCapabilities()`, fires post-login (after async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and broadcasts `identity:refresh`.

```tsx
// events:identity — use useIdentityEvent hook
import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'

// 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)
})
```

```tsx
// events:messaging — use useMessagingEvent hook
import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'

useMessagingEvent('postback:Buy Now', (event) => {
  console.log('Postback:', event.data.actionName, event.data.conversationId)
})
```

```tsx
// events:activity — use useActivityEvent hook
import { useActivityEvent } from '@stackable-labs/sdk-extension-react'

useActivityEvent('product_view', (event) => {
  console.log('Activity:', event.eventName, event.data)
})
```

```tsx
// messaging.send — use useMessaging hook (returns [send, { enabled, loading, error }] tuple)
import { useMessaging } from '@stackable-labs/sdk-extension-react'

// manifest.json: { "permissions": ["messaging:send"] }
const [send, { enabled, loading, error }] = useMessaging()
await send({ kind: 'text', body: 'Hello from the extension' })
```

```tsx
// identity.extend — login-time useExtendIdentity hook OR post-login imperative capabilities.identity.extend
// Enrich identity JWT claims before signing.
// The host sends base claims (external_id, email, name); your handler returns
// additional claims to merge. Returned keys are filtered against manifest.identityClaims:
//   - Standard JWT claims (external_id, email, name) are exempt — always allowed
//   - Custom claims must be declared in manifest.identityClaims
//   - Undeclared keys are dropped with a console.warn
// Merged claims land in BOTH identityState.user.metadata (for sibling extensions)
// AND the signed JWT's custom_claims (for downstream JWT consumers).
// Pattern: name the handler with useCallback<ExtendIdentityHandler> for stable
// reference + clean separation; pass the named const to useExtendIdentity.
//
// useExtendIdentity 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 for the imperative path.

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)

// ── For post-login async push (e.g., after async verification completes): ─────
const capabilities = useCapabilities()
await capabilities.identity.extend({ verified: true })

// Consumer side — same or sibling extension reacts via identity:refresh
useIdentityEvent('refresh', (event) => {
  console.log('verified =>', event.data.state.user?.metadata?.verified)
})

```

## 6. Verify

- Confirm the permission is in manifest.json
- If data.fetch, confirm the domain is in allowedDomains
- For data/context/actions: confirm accessed via `useCapabilities()` hook
- For events: confirm using `useIdentityEvent`, `useMessagingEvent`, or `useActivityEvent` hooks
- For identity.extend: confirm using `useExtendIdentity` hook (login-time) and/or `capabilities.identity.extend(patch)` (imperative post-login)
- For messaging.send: confirm using `useMessaging` hook
