Quickstart: the inbox on your site in 10 minutes
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);
- about 10 minutes.
1. Create the keys
Section titled “1. Create the keys”In the dashboard (https://app.nexafone.so), open Developers → API keys and create two keys in test mode:
- A publishable key (
pk_test_…). Under Allowed websites add the origin your page runs on, herehttp://localhost:3000. A publishable key is safe in a browser: it only starts sessions, and only from those origins. - A secret key (
sk_test_…). Pick the permissions the embedded users may use, for the inbox at leastinbox: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.
2. Add the token endpoint
Section titled “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 has the same route for every package). In the repo:
pnpm installpnpm --filter @nexafon/node buildpnpm --filter @nexafon/node pack --pack-destination /tmpThen, in a new folder for this quickstart ("type": "module" lets Node run server.ts as a module without a
warning):
npm init -ynpm pkg set type=modulenpm install /tmp/nexafon-node-*.tgzSave this server as 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 has every claim.
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
Section titled “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>:
<!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
Section titled “4. Run it”NEXAFON_SECRET_KEY=sk_test_… \NEXAFON_PUBLISHABLE_KEY=pk_test_… \NEXAFON_EMBED_CDN_URL=https://app.nexafone.so/embed \node server.tsOpen 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
Section titled “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 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
Section titled “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:
pnpm installpnpm --filter @nexafon/js --filter @nexafon/react buildpnpm --filter @nexafon/js --filter @nexafon/react pack --pack-destination /tmpThen install the tarballs in your app, all in one command:
npm install /tmp/nexafon-js-*.tgz /tmp/nexafon-react-*.tgzPHP (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:
composer config repositories.nexafon '{"type": "path", "url": "/path/to/nexafon/packages/server-php", "options": {"symlink": false}}'composer require nexafon/nexafon-php:@devPython (nexafon): install the package folder. Building it from source needs Python 3.9 or later:
pip install /path/to/nexafon/packages/server-pythonGo (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:
-replace=github.com/Keyd-Solutions/nexafon/packages/server-go=/path/to/nexafon/packages/server-go- Add the softphone and call buttons and listen to events.
- Use your framework: React, Next.js, Vue, Nuxt, Angular, Svelte or WordPress.
- Match your brand with appearance, then work through the go-live checklist.