# Partner user tokens and the full app

> Sign a partner user token that lists a user's stores and roles, and embed the whole Nexafon dashboard with <nexafon-app>.

Source: https://docs.nexafone.so/v1/partners/user-tokens/

Partner staff sign in to Nexafon only through your platform. Your server vouches for them with a **partner user
token**, and your page shows Nexafon with `<nexafon-app>`: the whole dashboard, with Nexafon’s side menu, for all of
their stores.

## The token

A JWT (HS256) signed with a partner secret key (`psk_…`) that has the `partner.stores:manage` scope, of the same mode
as your publishable partner key:

| Claim | Required | |
| --- | --- | --- |
| `sub` | yes | Your id for the user |
| `name` | yes | Their display name |
| `jti`, `exp` | yes | A new id per token; at most 10 minutes ahead. Each token works once |
| `email` | no | Their email |
| `locale` | no | `en`, `so` or `ar` |
| `stores` | yes | 1 to 50 of your stores: `{ externalRef, role, teams? }` |

`role` is one of `owner`, `admin`, `supervisor`, `agent` or `viewer`. `teams`, when present, are the store’s team ids
the user is in; leaving it out keeps their teams as they are, and an empty list takes them out of every team.

```ts title="partner.ts"
// For the staff member your session has signed in: one token covers every store your records say they work in, each
// with its own role. Never take the user, stores or roles from the request.
export function partnerToken(user: {
  id: string
  name: string
  email: string
  stores: { externalRef: string; role: string }[]
}) {
  return createPartnerUserToken({
    userId: user.id,
    name: user.name,
    email: user.email,
    locale: 'so',
    stores: user.stores,
  })
}
```

On each sign-in Nexafon creates or updates the user’s membership of every listed store (role, name, teams) and
re-enables it if it was disabled. Each membership takes a **seat** in its store; a store without a free seat answers
`no_free_seat` naming the store, unless its owner turned on adding seats automatically (see [billing](/v1/billing/)).
Users exist per partner: they have no password and cannot sign in to Nexafon directly.

## The page

```html
<script src="https://app.nexafone.so/embed/v1/nexafon.js"></script>
<script>
  Nexafon.init({
    publishableKey: 'ppk_live_…',
    getUserToken: async () => {
      const r = await fetch('/nexafon/partner-token', { method: 'POST' })
      if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`)
      return r.text()
    },
  })
</script>
<nexafon-app style="height: 100vh"></nexafon-app>
```

A partner session exchanges only on your domains (`client_not_allowed` elsewhere). One session covers all the user’s
stores, and calls from any of them ring the user.

The `examples/html` partner page:

```html title="examples/html/public/partner.html"
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Tocktify · Stores</title>
    <style>
      body { margin: 0; font: 15px/1.5 system-ui, sans-serif; }
      header { padding: 10px 16px; border-bottom: 1px solid #e5e7eb; }
      nexafon-app { height: calc(100vh - 46px); }
    </style>
  </head>
  <body>
    <header><strong>Tocktify</strong> · Seller centre</header>
    <!-- A partner runs all of Nexafon inside its own platform: one session covers every store of the user. -->
    <script src="%NEXAFON_EMBED_CDN_URL%/v1/nexafon.js"></script>
    <script>Nexafon.init({ publishableKey: '%NEXAFON_PARTNER_PUBLISHABLE_KEY%', getUserToken: () => fetch('/nexafon/partner-token', { method: 'POST' }).then((r) => { if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`); return r.text() }) })</script>
    <nexafon-app></nexafon-app>
  </body>
</html>
```

and the React version from `examples/partner`:

```tsx title="examples/partner/src/App.tsx"
import { NexafonApp, NexafonProvider } from '@nexafon/react'
import { useEffect, useState } from 'react'
import type { ReceivedEvent } from '../server.ts'

const getUserToken = async () => {
  const r = await fetch('/api/nexafon-token', { method: 'POST' })
  if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`)
  return r.text()
}

// One partner session covers every store of the user; the embed's own switcher moves between them.
export function App() {
  return (
    <NexafonProvider
      publishableKey={import.meta.env.VITE_NEXAFON_PUBLISHABLE_KEY}
      getUserToken={getUserToken}
    >
      <NexafonApp style={{ height: '100%' }} />
    </NexafonProvider>
  )
}

export function WebhookList() {
  const [events, setEvents] = useState<ReceivedEvent[]>([])
  useEffect(() => {
    const load = () =>
      fetch('/api/webhook-events')
        .then((r) => r.json() as Promise<ReceivedEvent[]>)
        .then(setEvents, () => undefined)
    load()
    const timer = setInterval(load, 2000)
    return () => clearInterval(timer)
  }, [])
  return events.map((e) => (
    <li key={e.id} data-type={e.type} data-store={e.store}>
      {e.type} {e.store}
    </li>
  ))
}
```

Both examples have no sign-in of their own, so for local runs only their token endpoints sign for one demo user
(`EXAMPLE_DEMO_USER_*` in each example's `.env`) and answer `401` without it. Your server signs only for the user your
own session has signed in, and your platform decides which stores and roles that user has; never take them from the
request.

## What the user sees

`<nexafon-app>` shows every section the user’s role allows in the current store: inbox, calls, team calls and
meetings, team, routing and Shop, templates, recordings, reports, connecting WhatsApp, billing, products, orders and
payments. An agent sees the agent menu only. Nexafon’s own sign-in and sign-out never appear. Smaller pieces still
work alongside it, such as a `<nexafon-call-button phone="…" store="shop-1">` on your order page.
