# Quickstart: the inbox on your site in 10 minutes

> Create keys, add a token endpoint to your server and put <nexafon-inbox> on a page, from nothing to a working embedded inbox.

Source: https://docs.nexafone.so/v1/quickstart/

By the end of this page a page on your own site shows the Nexafon inbox, signed in as one of your users, with no
Nexafon sign-in screen. You need:

- a Nexafon workspace whose plan includes SDK embeds, with its WhatsApp number connected (the inbox works without
  one, it is just empty);
- Node.js 22.18 or later, which runs TypeScript files directly (the server helpers also exist for
  [PHP, Python and Go](/v1/server/));
- about 10 minutes.

## 1. Create the keys

In the dashboard (https://app.nexafone.so), open **Developers → API keys** and create two keys in **test mode**:

1. A **publishable key** (`pk_test_…`). Under **Allowed websites** add the origin your page runs on, here
   `http://localhost:3000`. A publishable key is safe in a browser: it only starts sessions, and only from those
   origins.
2. A **secret key** (`sk_test_…`). Pick the permissions the embedded users may use, for the inbox at least
   `inbox:reply`. Copy it now: it is shown once. It stays on your server.

Then open **Developers → Embed domains** and add the same origin, `http://localhost:3000`. Nexafon’s frames refuse to
load on any other site.

:::note[Which user signs in?]
The token your server signs names a user by your own id and, the first time, by email. Nexafon links that email to
the workspace **member** with the same email. Owners never sign in through the SDK, so use a teammate: invite yourself
under a second address (**Staff → Members → Invite**, role **Agent**), pick at least one team under **Teams they work
in** (agents see only their teams’ conversations; create a team under **Teams** first if there is none), and accept
the invitation.
:::

## 2. Add the token endpoint

Your server tells Nexafon who is signed in by signing a short-lived **user token** with the secret key, using the
Node helper `@nexafon/node`. The SDK packages are not on npm yet, so `npm install @nexafon/node` fails for now; build
the helper from a checkout of the Nexafon repo and install the tarball
([install from a checkout](#install-from-a-checkout) has the same route for every package). In the repo:

```sh
pnpm install
pnpm --filter @nexafon/node build
pnpm --filter @nexafon/node pack --pack-destination /tmp
```

Then, in a new folder for this quickstart (`"type": "module"` lets Node run `server.ts` as a module without a
warning):

```sh
npm init -y
npm pkg set type=module
npm install /tmp/nexafon-node-*.tgz
```

Save this server as `server.ts`:

```ts title="server.ts"
import { readFileSync } from 'node:fs'
import { createServer, type IncomingMessage } from 'node:http'
import { fileURLToPath } from 'node:url'
import { createUserToken, verifyWebhook, WebhookVerificationError } from '@nexafon/node'

interface SignedInUser {
  id: string
  name: string
  email: string
  emailVerified: boolean
  isStaff: boolean
}

// Your app's own session decides who is signed in, and its own records whether it has confirmed their email.
// This sample has no sign-in: its user is the teammate in NEXAFON_USER_EMAIL, whose address you confirmed by
// inviting it to your workspace.
function signedInUser(_req: IncomingMessage): SignedInUser | null {
  const email = process.env.NEXAFON_USER_EMAIL ?? ''
  if (email === '') return null
  return {
    // Nexafon keeps the member it first linked to this id, so it must be the same person's id on every sign-in and
    // never another person's. Use your app's user id; the sample has no user records, so the email stands in.
    id: email,
    name: 'Amina Ali',
    email,
    emailVerified: true,
    isStaff: true,
  }
}

// Everyone who gets a token may reach your workspace's inbox and calls: sign only for your staff, never for customers
// who signed up. The sample's user is the teammate you invited.
function mayUseNexafon(user: SignedInUser): boolean {
  return user.isStaff
}

async function rawBody(req: IncomingMessage): Promise<Buffer> {
  const chunks: Buffer[] = []
  for await (const chunk of req) chunks.push(Buffer.from(chunk))
  return Buffer.concat(chunks)
}

const page = readFileSync(new URL('./quickstart.html', import.meta.url), 'utf8')
  .replaceAll('%NEXAFON_EMBED_CDN_URL%', process.env.NEXAFON_EMBED_CDN_URL ?? '')
  .replaceAll('%NEXAFON_PUBLISHABLE_KEY%', process.env.NEXAFON_PUBLISHABLE_KEY ?? '')

export const server = createServer(async (req, res) => {
  // #region token
  // The SDK calls getUserToken whenever it needs a session; answer with a fresh token for the signed-in user.
  if (req.method === 'POST' && req.url === '/nexafon-token') {
    const user = signedInUser(req)
    if (!user) {
      res.writeHead(401).end()
      return
    }
    if (!mayUseNexafon(user)) {
      res.writeHead(403).end()
      return
    }
    // Signed with NEXAFON_SECRET_KEY (sk_test_… or sk_live_…), which never leaves this server.
    const token = createUserToken({
      userId: user.id,
      name: user.name,
      role: 'agent',
      email: user.email,
      // Nexafon links a first sign-in to the member with this email only when it is verified: never send true for
      // an address your app has not confirmed, or whoever sets a member's email on your app acts as that member.
      emailVerified: user.emailVerified,
    })
    res.writeHead(200, { 'content-type': 'text/plain', 'cache-control': 'no-store' }).end(token)
    return
  }
  // #endregion
  // #region webhook
  if (req.method === 'POST' && req.url === '/hooks/nexafon') {
    try {
      // The raw body, exactly as received: parsing it first changes the bytes the signature covers.
      const event = verifyWebhook(
        req.headers,
        await rawBody(req),
        process.env.NEXAFON_WEBHOOK_SECRET ?? '',
      )
      // Deliveries can repeat: skip an event.id you have already handled.
      console.log(event.type, event.id)
      res.writeHead(204).end()
    } catch (err) {
      if (!(err instanceof WebhookVerificationError)) throw err
      res.writeHead(400).end()
    }
    return
  }
  // #endregion
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(page)
})

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  server.listen(3000, '127.0.0.1', () => console.log('listening on port 3000'))
}
```

`createUserToken` signs a JWT that lives 5 minutes and works once. The `role` caps what the session may do, below the
member’s own role and the key’s permissions. [User tokens](/v1/server/user-tokens/) has every claim.

:::caution[Send `emailVerified` as true only for a confirmed address]
Nexafon links a user’s first sign-in to the workspace member with the token’s email only when it is verified. If your
app lets users set an email it has not confirmed, someone could register with a member’s address and act as that
member. Send `true` only when your app has confirmed the address, from your own user record, never as a constant.
:::

The sample’s user is the teammate you invited, so its address is confirmed. In your app, pass the flag your own user
record keeps.

The `userId` must be stable per person: Nexafon keeps the member it first linked to an id, so send the same id for the
same person every time and never one person’s id for another. Use your app’s own user id; the sample has no user
records, so it uses the email. To try a second teammate, invite them too and set `NEXAFON_USER_EMAIL` to their
address.

## 3. Put the inbox on a page

Save the page next to the server as `quickstart.html`. It loads the SDK, starts it with your publishable key and a
function that fetches a fresh token (and throws when the server answers an error, such as 401 when no one is signed
in), and shows `<nexafon-inbox>`:

```html title="quickstart.html"
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Support</title>
    <style>
      nexafon-inbox { display: block; height: 640px; }
    </style>
  </head>
  <body>
    <script src="%NEXAFON_EMBED_CDN_URL%/v1/nexafon.js"></script>
    <script>
      Nexafon.init({
        publishableKey: '%NEXAFON_PUBLISHABLE_KEY%',
        // A 401 (no one signed in) must not reach the SDK as a token.
        getUserToken: async () => {
          const r = await fetch('/nexafon-token', { method: 'POST' })
          if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`)
          return r.text()
        },
      })
    </script>
    <nexafon-inbox></nexafon-inbox>
  </body>
</html>
```

The server above fills in `%NEXAFON_EMBED_CDN_URL%` and `%NEXAFON_PUBLISHABLE_KEY%`; in your own page write the values
in directly.

## 4. Run it

```sh
NEXAFON_SECRET_KEY=sk_test_… \
NEXAFON_PUBLISHABLE_KEY=pk_test_… \
NEXAFON_EMBED_CDN_URL=https://app.nexafone.so/embed \
NEXAFON_USER_EMAIL=you+agent@yourcompany.com \
node server.ts
```

Open `http://localhost:3000`. The inbox loads already signed in as the teammate whose email you set, and shows the
conversations of their teams.

## If it does not load

| What you see | Why | Fix |
| --- | --- | --- |
| The frame is blank and the console reports `frame-ancestors` | The page’s origin is not an embed domain | Add it under **Developers → Embed domains** |
| “This can’t open here” | The session exchange failed | Open the browser’s network tab: `POST /api/v1/sdk/sessions` answers with a [Problem Details](/v1/errors/) `code` |
| `client_not_allowed` | The origin is not on the publishable key | Add it to the key’s **Allowed websites** |
| `sdk_member_not_found` | No member has that email | Invite the user, or check `NEXAFON_USER_EMAIL` |
| The inbox is empty | The teammate is in no team, or their teams have no conversations | Add them to a team under **Staff → Members** (**Edit teams**) |
| `sdk_role_not_allowed` | The email belongs to the workspace owner | Sign in as a teammate |
| `user_token_replayed` | A token was reused | Fetch a new token on every `getUserToken` call |

## Install from a checkout

The SDK packages are not published yet: they are not on npm, Packagist or PyPI, and `go get` finds the Go module only
once the Nexafon repository is public and tagged. Until then, install them from a checkout of the repo; below,
`/path/to/nexafon` is that checkout. The guides show the install command to use after release.

**npm** (`@nexafon/js`, `@nexafon/react`, `@nexafon/vue`, `@nexafon/angular`, `@nexafon/node`): build and pack the
packages you use, in the repo. The React, Vue and Angular wrappers depend on `@nexafon/js`, so pack it with them:

```sh
pnpm install
pnpm --filter @nexafon/js --filter @nexafon/react build
pnpm --filter @nexafon/js --filter @nexafon/react pack --pack-destination /tmp
```

Then install the tarballs in your app, all in one command:

```sh
npm install /tmp/nexafon-js-*.tgz /tmp/nexafon-react-*.tgz
```

**PHP** (`nexafon/nexafon-php`): add the package folder as a Composer path repository. With `symlink` off, Composer
copies the package, so your app does not depend on where the checkout lives:

```sh
composer config repositories.nexafon '{"type": "path", "url": "/path/to/nexafon/packages/server-php", "options": {"symlink": false}}'
composer require nexafon/nexafon-php:@dev
```

**Python** (`nexafon`): install the package folder. Building it from source needs Python 3.9 or later:

```sh
pip install /path/to/nexafon/packages/server-python
```

**Go** (`github.com/Keyd-Solutions/nexafon/packages/server-go`): point the module path at the folder with a `replace`
directive. After release, remove the `replace` and run `go get`:

```sh
go mod edit -require=github.com/Keyd-Solutions/nexafon/packages/server-go@v0.0.0 \
  -replace=github.com/Keyd-Solutions/nexafon/packages/server-go=/path/to/nexafon/packages/server-go
```

## Next

- Add the [softphone and call buttons](/v1/web/) and listen to [events](/v1/web/events/).
- Use your framework: [React](/v1/web/react/), [Next.js](/v1/web/nextjs/), [Vue](/v1/web/vue/),
  [Nuxt](/v1/web/nuxt/), [Angular](/v1/web/angular/), [Svelte](/v1/web/svelte/) or
  [WordPress](/v1/web/wordpress/).
- Match your brand with [appearance](/v1/web/appearance/), then work through the [go-live checklist](/v1/go-live/).
