# Nexafon developer docs > Put a WhatsApp Business inbox, WhatsApp and team calls, and WhatsApp commerce inside your own product. Source: https://docs.nexafone.so/v1/ Nexafon runs a business’s WhatsApp: the shared inbox, WhatsApp calls and team calls, and WhatsApp commerce (catalog, Shop menu, orders and payments). These docs show how to put all of it inside **your** product, so your users never leave it. ## What you can build with | Piece | What it does | Start here | | --- | --- | --- | | Web SDK | One script tag or npm package; Web Components for the whole dashboard (``), the inbox, the softphone and a call button, themed with your colours | [Quickstart](/v1/quickstart/) | | Server helpers | Sign user tokens and verify webhooks with one function call in Node, PHP, Python and Go | [Server helpers](/v1/server/) | | REST API (v1) | Conversations, messages, templates, teams, members, recordings, commerce and payments | [API reference](/v1/api/) | | Webhooks | Signed events for messages, calls, meetings, templates, billing and orders | [Webhooks](/v1/webhooks/) | | Partner API | For platforms that run many stores: create stores, sign in their staff, receive every store’s events | [Partners](/v1/partners/) | ## How the pieces fit 1. Your server keeps a **secret key** and signs a short-lived **user token** for whoever is signed in to your product. 2. The page loads the SDK with your **publishable key**. The SDK swaps the user token for an **SDK session** that lives only in memory inside Nexafon’s frames. 3. Nexafon’s screens render inside your page; your code listens to [events](/v1/web/events/) and calls [methods](/v1/web/#methods) such as `call(phone)`. 4. Your server receives [webhooks](/v1/webhooks/) and calls the [API](/v1/api/) with the secret key. [Core concepts](/v1/concepts/) explains workspaces, roles, keys, sessions and test mode in more detail. ## For AI agents and tools Every page is also served as Markdown: add `.md` to its path, such as [/v1/quickstart.md](/v1/quickstart.md). [/llms.txt](/llms.txt) lists every page, [/llms-full.txt](/llms-full.txt) has them all in one file, and the OpenAPI 3.1 spec is at [/v1/api/openapi.json](/v1/api/openapi.json). The [MCP server](/v1/ai/mcp/) gives Claude Code and other MCP clients these docs, the example apps and test-key actions as tools. --- # Quickstart: the inbox on your site in 10 minutes > Create keys, add a token endpoint to your server and put 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 { 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 ``: ```html title="quickstart.html" Support ``` 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/). --- # Core concepts > Workspaces, members and roles, the four kinds of keys, user tokens and SDK sessions, test mode, and partners. Source: https://docs.nexafone.so/v1/concepts/ ## Workspaces, members and roles A **workspace** is one business: its WhatsApp number, inbox, teams, calls and billing. People who work in it are **members**, each with one role: `owner`, `admin`, `supervisor`, `agent` or `viewer`. Teams group members; agents see only their teams’ conversations and calls. Every API operation declares the permission it needs (shown as **Permission** in the [API reference](/v1/api/)), and the server checks it on every request. ## Keys | Key | Prefix | Lives | Used for | | --- | --- | --- | --- | | Secret key | `sk_live_…`, `sk_test_…` | Your server only | Calling the API and signing user tokens. Carries **scopes** (the permissions it acts with) and an optional IP allow-list | | Publishable key | `pk_live_…`, `pk_test_…` | Browser or app | Starting SDK sessions, only from its **allowed origins** (web) or bundle ids and package names (mobile) | | Partner secret key | `psk_live_…`, `psk_test_…` | The partner’s server | The [Partner API](/v1/partners/api/) and the partner’s own stores | | Partner publishable key | `ppk_live_…`, `ppk_test_…` | Browser | SDK sessions on the partner’s domains, for all of a user’s stores | Keys are shown **once**, when they are created; Nexafon keeps only a hash. Revoke a key and it stops working at once. Workspace keys are managed under **Developers → API keys**; partner keys by Nexafon in the console. ## User tokens and SDK sessions Browsers never hold a secret key. Instead: 1. Your server signs a **user token**: a JWT (HS256) signed with a secret key, naming your user, valid for at most 10 minutes and usable once. See [user tokens](/v1/server/user-tokens/). 2. The SDK exchanges it, with the publishable key, at `POST /api/v1/sdk/sessions` for an **SDK session token** (`sdk_…`). The session acts as the workspace member the user maps to, within the scopes of the secret key that signed the token. 3. The session lasts 15 minutes and is refreshed in the background for up to 12 hours; after that the SDK asks your server for a new user token. It is held **in memory** inside Nexafon’s frames, never in `localStorage` or cookies. ## Test mode Test keys (`sk_test_…`, `pk_test_…`) and the sessions they start work against the same workspace data but never reach real customers or move money. Every operation that calls Meta (sending messages; placing, answering, declining, ending, transferring or picking up WhatsApp calls; submitting, deleting or syncing templates; changing the number or the store's catalog) answers `403 test_mode_live` unless the workspace has a WhatsApp **test number** connected. Internal calls between members never reach Meta and work in test mode. Payments, subscription changes and wallet settings (automatic top-up charges a saved card) always answer `403 test_mode_live`, and a test key or session that adds a member when every seat is taken gets `seat_limit_reached` instead of buying a seat. A user token must be signed with a secret key of the same mode as the publishable key. ## Plans and features What a workspace may use comes from its plan: features such as `sdk.embeds` or `commerce.orders`, and limits such as `seats`. An operation whose feature the plan lacks answers `403` with the code `feature_not_in_plan`. See [billing](/v1/billing/) for the full list. ## Partners and stores A **partner** is a platform (a marketplace, a restaurant system) that runs many businesses. Each business on it is a **store**: an ordinary workspace linked to the partner by the partner’s own id for it, its `externalRef`. The partner creates stores, signs in their staff and receives every store’s events. See [partners](/v1/partners/). ## Versions This is version **v1** of the API and SDK. Public operations answer with an `API-Version: v1` header. A deprecated operation keeps working for at least 180 days and announces it with `Deprecation` and `Sunset` headers; see the [API overview](/v1/api/#versioning-and-deprecation). --- # Web SDK > Load the SDK, start it with Nexafon.init, place the Web Components and call methods such as call(phone). Source: https://docs.nexafone.so/v1/web/ The web SDK puts Nexafon’s screens inside any web page as Web Components. Each one renders a frame of Nexafon’s embed pages, so your page needs no UI code and always shows the current version. It works in every framework; the [React](/v1/web/react/), [Vue](/v1/web/vue/) and [Angular](/v1/web/angular/) wrappers add idiomatic components on top. ## Load it With a script tag (the CDN serves the latest v1, with fixes and new features automatically): ```html ``` Or from npm, as a typed loader that adds the same script once: :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/js ``` ```ts import { loadNexafon } from '@nexafon/js' // Resolves to null on the server (Next.js, Nuxt, Angular SSR, SvelteKit): it never touches window there. const Nexafon = await loadNexafon() ``` ## Start it Call `init` once per page, before or after the elements are in the DOM: ```js Nexafon.init({ publishableKey: 'pk_live_…', 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() }, appearance: { primaryColor: '#16A34A', radius: 10, colorMode: 'auto' }, locale: 'en', popOutCalls: false, }) ``` | Option | Type | | | --- | --- | --- | | `publishableKey` | string | `pk_live_…`/`pk_test_…` for one business, or a partner’s `ppk_live_…`/`ppk_test_…` | | `getUserToken` | `() => Promise \| string` | Returns a **fresh** user token from your server each time; a token works once ([user tokens](/v1/server/user-tokens/)). Throw when your server answers an error (such as 401 when no one is signed in), so the SDK never gets an error body as a token | | `appearance` | object | Primary colour, radius, font and light/dark mode ([appearance](/v1/web/appearance/)) | | `locale` | `'en' \| 'so' \| 'ar'` | The embedded screens’ language; Arabic renders right to left | | `popOutCalls` | boolean | On multi-page sites, calls run in a small window so navigating does not end them | `init` loads a hidden **hub** frame at once: it holds the session, the one realtime connection and the call audio for the whole page, and every visible component shares it. So the first component appears in well under a second, and a page with an inbox, a softphone and five call buttons still opens one connection. ## Components | Element | Shows | Attributes | | --- | --- | --- | | `` | The whole dashboard with Nexafon’s side menu: inbox, calls, team, settings, billing, orders… (each section only for users whose role may open it) | `store` | | `` | The shared WhatsApp inbox | `store` | | `` | The softphone: incoming calls, dial pad, call controls | `store` | | `` | A button that calls one customer on WhatsApp | `phone` (E.164, required), `store` | `store` opens a partner’s store first, by its `externalRef` or workspace id ([store switcher](/v1/partners/store-switcher/)). Changing an attribute reloads that component. Size `` and `` yourself (they fill their element, at least 480 px high); the softphone and the call button size themselves to their content. Frames get `allow="microphone; autoplay; clipboard-write"`: the browser asks for the microphone the first time a call starts. Without custom elements you can mount the same screens imperatively: ```js const unmount = Nexafon.mount(document.getElementById('support'), { component: 'inbox' }) // Nexafon.mount(el, { app: true }) for the whole dashboard; unmount() removes it again. ``` ## Methods | Method | Does | | --- | --- | | `openChat(phone)` | Opens (or starts) the chat with that customer in the inbox or app on the page | | `call(phone)` | Calls the customer on WhatsApp from the store the user viewed last; with `popOutCalls` the call opens in the pop-out window | | `startInternalCall(memberIds)` | Starts a team call with those members | | `setAppearance(appearance)` | Changes colours, radius, font or mode at once, for example when your own theme switches | | `on(event, listener)` / `off(event, listener)` | Listen to [events](/v1/web/events/); `on` returns a function that stops listening | | `destroy()` | Removes every frame and ends the session on this page | Methods that act through a frame return a Promise that rejects when the action is not possible, such as a call without a microphone. ## Calls across page navigation On a classic multi-page site a page load would cut an active call. With `popOutCalls: true` a call opens in a small pop-out window that survives navigation; a call already live on the page can move into it and back. If the browser blocks the pop-up, the call stays on the page and the user is asked to allow pop-ups. The [HTML example](/v1/web/html/) shows it across two pages. ## Security model - The publishable key starts sessions only from its allowed origins, and Nexafon’s frames load only on the workspace’s **embed domains** (CSP `frame-ancestors`). - Every message between your page and the frames is origin-checked and versioned. - Session tokens never reach your page: no `localStorage`, `sessionStorage` or cookies on your site. See [security](/v1/security/) for the Content-Security-Policy your own page needs. --- # Plain HTML > Add the inbox, softphone and call buttons to any HTML page with one script tag, including multi-page sites with pop-out calls. Source: https://docs.nexafone.so/v1/web/html/ No build step: one script tag, one `Nexafon.init` call and the `` elements. The complete example is `examples/html` in the Nexafon repository; it runs with Playwright in Chromium, Firefox and WebKit in CI. ## 1. The page ```html title="examples/html/public/index.html" Routa · Orders
RoutaOrders
``` The server fills in `%NEXAFON_EMBED_CDN_URL%` and `%NEXAFON_PUBLISHABLE_KEY%`; in your page write the script URL (`https://app.nexafone.so/embed/v1/nexafon.js`) and your publishable key in directly. ## 2. The token endpoint The page’s `getUserToken` posts to your server, which signs a token for the signed-in user. The example signs it without dependencies, which shows what a [user token](/v1/server/user-tokens/) is: ```ts title="examples/html/token.ts" import { createHmac, randomUUID } from 'node:crypto' // A user token is a JWT (HS256) the site's server signs with its Nexafon secret key: the key string is the HMAC key. // It lives at most 10 minutes and is used once, so every embed load asks for a new one. export interface UserClaims { sub: string name?: string email?: string email_verified?: boolean role?: string locale?: string stores?: { externalRef: string; role: string }[] } const LIFETIME_S = 5 * 60 const b64 = (v: string | Buffer) => Buffer.from(v).toString('base64url') export function signUserToken(secretKey: string, claims: UserClaims, now = Date.now()): string { const iat = Math.floor(now / 1000) const header = b64(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) const payload = b64(JSON.stringify({ ...claims, iat, exp: iat + LIFETIME_S, jti: randomUUID() })) const signature = createHmac('sha256', secretKey).update(`${header}.${payload}`).digest() return `${header}.${payload}.${b64(signature)}` } // "store-1:owner,store-2:agent" → the partner token's stores. export function parseStores(raw: string): { externalRef: string; role: string }[] { return raw .split(',') .map((pair) => pair.trim()) .filter(Boolean) .map((pair) => { const [externalRef = '', role = 'agent'] = pair.split(':') return { externalRef, role } }) } ``` In your own server use a [server helper](/v1/server/) instead: one call in Node, PHP, Python or Go. ## Multi-page sites `examples/html/public/shop.html` and `shop-orders.html` start Nexafon with `popOutCalls: true`. A call started on one page moves to a small pop-out window and keeps going while the user navigates to the other page: ```html title="examples/html/public/shop.html" Routa · Shop

Shop

``` ## Run the example ```sh cp examples/html/.env.example examples/html/.env # then fill in the keys pnpm --filter @nexafon/example-html start ``` Add `http://localhost:5199` to the workspace’s embed domains and to the publishable key’s allowed origins first. For this demo only, set `EXAMPLE_DEMO_USER_EMAIL` to a member’s email: the example has no sign-in, so its token endpoint signs for that one demo user and answers `401` without it. Your server signs only for the user your own session has signed in, answers `403` to signed-in users who are not your staff (a token can make its user a member of your workspace, so never sign for customers who signed up), and sends `email_verified: true` only for an address your site has confirmed. --- # React > Add Nexafon to a React app with NexafonProvider, the NexafonApp, Inbox, Softphone and CallButton components and useNexafonEvent. Source: https://docs.nexafone.so/v1/web/react/ `@nexafon/react` wraps the [web SDK](/v1/web/) in typed React components. Supported: React 19. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/react ``` ## 1. Provide Nexafon once `` takes the `Nexafon.init` options and starts Nexafon once, on mount; use one per page. Put it high in your tree, inside your app’s signed-in area: ```tsx title="examples/react/src/App.tsx" import { CallButton, Inbox, NexafonProvider, Softphone } from '@nexafon/react' 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() } export function App() { return (
) } ``` ## 2. Place components | Component | Renders | Props | | --- | --- | --- | | `` | ``, the whole dashboard | `store?`, `className`, `style`, `id` | | `` | `` | `store?`, `className`, `style`, `id` | | `` | `` | `store?`, `className`, `style`, `id` | | `` | `` | `phone`, `store?`, `className`, `style`, `id` | ## 3. Events and methods ```tsx import { useNexafon, useNexafonEvent } from '@nexafon/react' function OrderHeader({ phone }: { phone: string }) { const nexafon = useNexafon() // null on the server and until Nexafon has started useNexafonEvent('call.ringing', (call) => console.log('ringing', call.callerNumber)) return } ``` `useNexafonEvent` listens while the component is mounted; the events are listed in the [events reference](/v1/web/events/). ## The token endpoint `getUserToken` calls your server, which signs a token with a [server helper](/v1/server/). The example’s `POST /api/nexafon-token` handler: ```ts title="examples/react/server/token.ts" import type { IncomingMessage, ServerResponse } from 'node:http' import { createUserToken } from '@nexafon/node' interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session. function signedInUser(_req: IncomingMessage): SignedInUser | null { const email = process.env.EXAMPLE_DEMO_USER_EMAIL if (!email) return null return { id: 'demo-user-1', name: 'Demo User', role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent', email, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // POST /api/nexafon-token signs a token for the signed-in user with NEXAFON_SECRET_KEY. export function userToken(req: IncomingMessage, res: ServerResponse): void { if (req.method !== 'POST') { res.writeHead(405).end() return } const user = signedInUser(req) if (!user) { res.writeHead(401).end() return } if (!mayUseNexafon(user)) { res.writeHead(403).end() return } const token = createUserToken({ userId: user.id, name: user.name, role: user.role, 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) } ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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 example mounts it as middleware of the Vite dev and preview servers in `examples/react/vite.config.ts`; in production it is a route of your own server, behind your own session check. The full example is `examples/react` (Vite), tested end to end with Playwright. --- # Next.js > Use @nexafon/react in the Next.js app router with a route handler that signs user tokens. Source: https://docs.nexafone.so/v1/web/nextjs/ `@nexafon/react` works in the app router: every export is marked `'use client'` and nothing touches `window` during server rendering. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/react @nexafon/node ``` ## 1. A route handler for tokens The secret key stays on the server, in a route handler (`NEXAFON_SECRET_KEY` in the server’s environment, never a `NEXT_PUBLIC_` variable): ```ts title="examples/next/app/api/nexafon-token/route.ts" import { createUserToken } from '@nexafon/node' interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session. function signedInUser(_req: Request): SignedInUser | null { const email = process.env.EXAMPLE_DEMO_USER_EMAIL if (!email) return null return { id: 'demo-user-1', name: 'Demo User', role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent', email, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // Signs a token for the signed-in user with NEXAFON_SECRET_KEY. export function POST(req: Request) { const user = signedInUser(req) if (!user) return new Response(null, { status: 401 }) if (!mayUseNexafon(user)) return new Response(null, { status: 403 }) const token = createUserToken({ userId: user.id, name: user.name, role: user.role, 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, }) return new Response(token, { headers: { 'content-type': 'text/plain', 'cache-control': 'no-store' }, }) } ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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. ::: ## 2. The provider in a client component `getUserToken` is a function, so render `` from a client component (`'use client'`): ```tsx title="examples/next/app/support.tsx" 'use client' import { CallButton, Inbox, NexafonProvider, Softphone } from '@nexafon/react' // getUserToken is a function, so the provider is rendered from a client component. 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() } export function Support({ publishableKey }: { publishableKey: string }) { return (
) } ``` ## 3. The page The page is a server component. It reads `NEXAFON_PUBLISHABLE_KEY` on each request (`dynamic = 'force-dynamic'`), so one build serves any key, and passes it to the client component: ```tsx title="examples/next/app/page.tsx" import { Support } from './support' // Read on each request, so one build serves any key. export const dynamic = 'force-dynamic' export default function Page() { return } ``` Use the [React components and hooks](/v1/web/react/) inside it as usual. The full example is `examples/next`. --- # Vue > Install the @nexafon/vue plugin and use NexafonApp, Inbox, Softphone and CallButton with useNexafonEvent. Source: https://docs.nexafone.so/v1/web/vue/ `@nexafon/vue` is a Vue plugin with components and composables around the [web SDK](/v1/web/). Supported: Vue 3.5. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/vue ``` ## 1. Install the plugin `createNexafon(options)` takes the `Nexafon.init` options and starts Nexafon once; it stops when the app unmounts. ```ts title="examples/vue/src/main.ts" import { createNexafon } from '@nexafon/vue' import { createApp } from 'vue' import App from './App.vue' 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() } createApp(App) .use( createNexafon({ publishableKey: import.meta.env.VITE_NEXAFON_PUBLISHABLE_KEY, getUserToken }), ) .mount('#app') ``` ## 2. Use the components ```vue title="examples/vue/src/App.vue" ``` | Component | Renders | Props | | --- | --- | --- | | `` | `` | `store?` | | `` | `` | `store?` | | `` | `` | `store?` | | `` | `` | `phone`, `store?` | `class` and `style` fall through to the element. `useNexafonEvent(name, listener)` listens for the component’s lifetime; `useNexafon()` is a ref to the started API (`null` until it has started). Writing the raw `` tags in your own templates needs `compilerOptions.isCustomElement: (tag) => tag.startsWith('nexafon-')`; the components above do not. ## The token endpoint `getUserToken` calls your server, which signs a token with a [server helper](/v1/server/). The example’s `POST /api/nexafon-token` handler: ```ts title="examples/vue/server/token.ts" import type { IncomingMessage, ServerResponse } from 'node:http' import { createUserToken } from '@nexafon/node' interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session. function signedInUser(_req: IncomingMessage): SignedInUser | null { const email = process.env.EXAMPLE_DEMO_USER_EMAIL if (!email) return null return { id: 'demo-user-1', name: 'Demo User', role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent', email, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // POST /api/nexafon-token signs a token for the signed-in user with NEXAFON_SECRET_KEY. export function userToken(req: IncomingMessage, res: ServerResponse): void { if (req.method !== 'POST') { res.writeHead(405).end() return } const user = signedInUser(req) if (!user) { res.writeHead(401).end() return } if (!mayUseNexafon(user)) { res.writeHead(403).end() return } const token = createUserToken({ userId: user.id, name: user.name, role: user.role, 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) } ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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 example mounts it as middleware of the Vite dev and preview servers in `examples/vue/vite.config.ts`; in production it is a route of your own server, behind your own session check. The full example is `examples/vue`, tested with Playwright. --- # Nuxt > Install the @nexafon/vue plugin on the client only, render the components in ClientOnly and sign tokens in a server route. Source: https://docs.nexafone.so/v1/web/nuxt/ Nuxt uses [`@nexafon/vue`](/v1/web/vue/). Importing it on the server never touches `window`; install the plugin on the client only and render the components inside ``. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/vue @nexafon/node ``` ## 1. Runtime config The keys are Nuxt runtime config, set when the server starts: the publishable key is `runtimeConfig.public.nexafonPublishableKey` (from `NUXT_PUBLIC_NEXAFON_PUBLISHABLE_KEY`), and the secret key is `runtimeConfig.nexafonSecretKey` (from `NUXT_NEXAFON_SECRET_KEY`), which stays on the server and is never under `public`. Declare both in `nuxt.config.ts` with empty defaults. ## 2. A client-only plugin The `.client.ts` suffix keeps the plugin out of server rendering. It reads the publishable key from the public runtime config: ```ts title="examples/nuxt/app/plugins/nexafon.client.ts" import { createNexafon } from '@nexafon/vue' export default defineNuxtPlugin((nuxtApp) => { const { nexafonPublishableKey } = useRuntimeConfig().public nuxtApp.vueApp.use( createNexafon({ publishableKey: nexafonPublishableKey, 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() }, }), ) }) ``` ## 3. A server route for tokens `POST /api/nexafon-token` signs a user token with the secret key from the server runtime config: ```ts title="examples/nuxt/server/api/nexafon-token.post.ts" import { createUserToken } from '@nexafon/node' import type { H3Event } from 'h3' interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: NUXT_EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your // session. function signedInUser(event: H3Event): SignedInUser | null { const { exampleDemoUserEmail, exampleDemoUserRole } = useRuntimeConfig(event) if (!exampleDemoUserEmail) return null return { id: 'demo-user-1', name: 'Demo User', role: exampleDemoUserRole || 'agent', email: exampleDemoUserEmail, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // Signs a token for the signed-in user with the workspace secret key. export default defineEventHandler((event) => { const user = signedInUser(event) if (!user) throw createError({ statusCode: 401 }) if (!mayUseNexafon(user)) throw createError({ statusCode: 403 }) const config = useRuntimeConfig(event) setResponseHeaders(event, { 'content-type': 'text/plain', 'cache-control': 'no-store' }) return createUserToken( { userId: user.id, name: user.name, role: user.role, 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, }, { secretKey: config.nexafonSecretKey }, ) }) ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `NUXT_EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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. ::: ## 4. The page ```vue title="examples/nuxt/app/app.vue" ``` The full example is `examples/nuxt`. --- # Angular > Use provideNexafon, the standalone nexafon-ng components and NexafonService, safe under Angular SSR. Source: https://docs.nexafone.so/v1/web/angular/ `@nexafon/angular` has standalone components and a service around the [web SDK](/v1/web/). Supported: Angular 22 with RxJS 7.4 or later. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/angular ``` ## 1. Provide Nexafon `provideNexafon(options)` takes the `Nexafon.init` options and starts Nexafon once, with the app. The example's `appConfig` takes the publishable key as an argument: ```ts title="examples/angular/src/app/app.config.ts" import { type ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core' import { provideClientHydration, withEventReplay } from '@angular/platform-browser' import { provideNexafon } from '@nexafon/angular' 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() } export function appConfig(publishableKey: string): ApplicationConfig { return { providers: [ provideBrowserGlobalErrorListeners(), provideClientHydration(withEventReplay()), provideNexafon({ publishableKey, getUserToken }), ], } } ``` The browser entry point asks the example's server for the key (`GET /api/nexafon-config`) before it starts the app, so one build serves any workspace: ```ts title="examples/angular/src/main.ts" import { bootstrapApplication } from '@angular/platform-browser' import { App } from './app/app' import { appConfig } from './app/app.config' // The publishable key comes from the server when the page loads, so one build serves any workspace. fetch('/api/nexafon-config') .then((r) => r.json() as Promise<{ publishableKey: string }>) .then(({ publishableKey }) => bootstrapApplication(App, appConfig(publishableKey))) .catch((err: unknown) => console.error(err)) ``` ## 2. The server The example's Express server (Angular SSR) answers `GET /api/nexafon-config` with `NEXAFON_PUBLISHABLE_KEY` and signs user tokens at `POST /api/nexafon-token` with `NEXAFON_SECRET_KEY` and the [Node helper](/v1/server/node/), which `getUserToken` calls: ```ts title="examples/angular/src/server.ts" import { join } from 'node:path' import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node' import { createUserToken } from '@nexafon/node' import express, { type Request } from 'express' try { process.loadEnvFile() } catch { // No .env: the variables come from the environment. } const browserDistFolder = join(import.meta.dirname, '../browser') const app = express() const angularApp = new AngularNodeAppEngine() app.get('/api/nexafon-config', (_req, res) => { res.json({ publishableKey: process.env.NEXAFON_PUBLISHABLE_KEY ?? '' }) }) interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session. function signedInUser(_req: Request): SignedInUser | null { const email = process.env.EXAMPLE_DEMO_USER_EMAIL if (!email) return null return { id: 'demo-user-1', name: 'Demo User', role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent', email, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // Signs a token for the signed-in user with NEXAFON_SECRET_KEY. app.post('/api/nexafon-token', (req, res) => { const user = signedInUser(req) if (!user) { res.sendStatus(401) return } if (!mayUseNexafon(user)) { res.sendStatus(403) return } const token = createUserToken({ userId: user.id, name: user.name, role: user.role, 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.type('text/plain').set('cache-control', 'no-store').send(token) }) app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false })) app.use((req, res, next) => { angularApp .handle(req) .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next) }) if (isMainModule(import.meta.url)) { const port = process.env.PORT || 5205 app.listen(Number(port), '127.0.0.1', (error) => { if (error) throw error console.log(`Angular example on port ${port}`) }) } export const reqHandler = createNodeRequestHandler(app) ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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. ::: ## 3. Components and the service ```ts title="examples/angular/src/app/app.ts" import { Component } from '@angular/core' import { CallButtonComponent, InboxComponent, SoftphoneComponent } from '@nexafon/angular' @Component({ selector: 'app-root', imports: [InboxComponent, SoftphoneComponent, CallButtonComponent], template: `
`, styles: `.support { display: grid; gap: 16px; padding: 16px; grid-template-columns: 1fr 340px; }`, }) export class App {} ``` | Component | Renders | Inputs | | --- | --- | --- | | `` | `` | `store` | | `` | `` | `store` | | `` | `` | `store` | | `` | `` | `phone`, `store` | Each renders inside a block host you size. `NexafonService.events(name)` is an Observable of one [event](/v1/web/events/); `openChat`, `call`, `startInternalCall` and `setAppearance` call the started SDK synchronously, so a pop-out window still opens inside the user’s click; `api` is a signal of the SDK (`null` on the server and until it has started). ## SSR On the server (`isPlatformBrowser` is false) the service never loads Nexafon and the components render an empty host; the browser creates the elements after hydration. The full example is `examples/angular`. --- # Svelte and other frameworks > Use the Web Components directly in Svelte, SvelteKit, Solid, Lit or any framework, with loadNexafon from @nexafon/js. Source: https://docs.nexafone.so/v1/web/svelte/ Svelte, Solid, Lit, Qwik and plain JavaScript need no wrapper: load and start Nexafon once in the browser, then write the `` elements in your markup. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/js ``` ```svelte title="examples/svelte/src/App.svelte"
``` `loadNexafon()` resolves to `null` on the server, so the same code is safe in SvelteKit, SolidStart or Astro server code. With TypeScript JSX (Solid), declare the `nexafon-*` tags in `JSX.IntrinsicElements`. ## The token endpoint `getUserToken` calls your server, which signs a token with a [server helper](/v1/server/). The example’s `POST /api/nexafon-token` handler: ```ts title="examples/svelte/server/token.ts" import type { IncomingMessage, ServerResponse } from 'node:http' import { createUserToken } from '@nexafon/node' interface SignedInUser { id: string name: string role: 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. The // example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session. function signedInUser(_req: IncomingMessage): SignedInUser | null { const email = process.env.EXAMPLE_DEMO_USER_EMAIL if (!email) return null return { id: 'demo-user-1', name: 'Demo User', role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent', email, // The demo address is a member you invited, so you have confirmed it. 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 example's only user is the demo teammate. function mayUseNexafon(user: SignedInUser): boolean { return user.isStaff } // POST /api/nexafon-token signs a token for the signed-in user with NEXAFON_SECRET_KEY. export function userToken(req: IncomingMessage, res: ServerResponse): void { if (req.method !== 'POST') { res.writeHead(405).end() return } const user = signedInUser(req) if (!user) { res.writeHead(401).end() return } if (!mayUseNexafon(user)) { res.writeHead(403).end() return } const token = createUserToken({ userId: user.id, name: user.name, role: user.role, 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) } ``` `signedInUser` stands for your app’s own session: answer `401` when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in `EXAMPLE_DEMO_USER_EMAIL` and answers `401` without one; replace it with your session. `mayUseNexafon` answers `403` to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff. :::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 example mounts it as middleware of the Vite dev and preview servers in `examples/svelte/vite.config.ts`; in production it is a route of your own server, behind your own session check. The full example is `examples/svelte`. --- # WordPress > A small WordPress plugin that signs user tokens for the logged-in WordPress user and adds a Nexafon shortcode. Source: https://docs.nexafone.so/v1/web/wordpress/ The WordPress example is a plugin. It adds the REST route `POST /wp-json/nexafon/v1/user-token`, which signs a user token for the logged-in WordPress user with the [PHP helper](/v1/server/php/) (`nexafon/nexafon-php`) and accepts only users with the `nexafon_use` capability sending the REST nonce, and the shortcode `[nexafon_inbox]`, which loads the web SDK and places the inbox and softphone on any page. ```php title="examples/wordpress/nexafon-example/nexafon-example.php" add_cap(NEXAFON_EXAMPLE_CAPABILITY); }); function nexafon_example_can_use(): bool { return current_user_can(NEXAFON_EXAMPLE_CAPABILITY); } add_action('rest_api_init', function (): void { register_rest_route('nexafon/v1', '/user-token', [ 'methods' => 'POST', 'permission_callback' => 'nexafon_example_can_use', 'callback' => function (): WP_REST_Response { $user = wp_get_current_user(); // Nexafon links a new user to the member with this email. WordPress confirms a changed email, but other // plugins (WooCommerce's account page, checkout) and admins set it unconfirmed, so the site has to say so. $token = Tokens::createUserToken([ 'userId' => 'wp-' . $user->ID, 'name' => $user->display_name, 'role' => nexafon_example_setting('NEXAFON_USER_ROLE', 'agent'), 'email' => $user->user_email, 'emailVerified' => filter_var(nexafon_example_setting('NEXAFON_EMAILS_VERIFIED'), FILTER_VALIDATE_BOOLEAN), ], ['secretKey' => nexafon_example_setting('NEXAFON_SECRET_KEY')]); $response = new WP_REST_Response(['token' => $token]); $response->header('Cache-Control', 'no-store'); return $response; }, ]); }); add_shortcode('nexafon_inbox', function (): string { if (!nexafon_example_can_use()) { return ''; } $cdn = rtrim(nexafon_example_setting('NEXAFON_EMBED_CDN_URL'), '/'); wp_enqueue_script('nexafon', $cdn . '/v1/nexafon.js', [], null, ['in_footer' => true]); $config = [ 'publishableKey' => nexafon_example_setting('NEXAFON_PUBLISHABLE_KEY'), 'tokenUrl' => rest_url('nexafon/v1/user-token'), 'nonce' => wp_create_nonce('wp_rest'), ]; wp_add_inline_script('nexafon', sprintf( '(function (c) { Nexafon.init({ publishableKey: c.publishableKey, getUserToken: function () { return fetch(c.tokenUrl, { method: "POST", headers: { "X-WP-Nonce": c.nonce } }).then(function (r) { if (!r.ok) { throw new Error("The token endpoint answered " + r.status) } return r.json() }).then(function (b) { return b.token }) } }) })(%s);', wp_json_encode($config) )); return '
' . '
'; }); ``` Keep the secret key out of the database and the theme: define `NEXAFON_PUBLISHABLE_KEY`, `NEXAFON_SECRET_KEY`, `NEXAFON_EMBED_CDN_URL` and `NEXAFON_USER_ROLE` in `wp-config.php` or the server’s environment. Add the site’s origin to the workspace’s embed domains and the publishable key’s allowed origins. A signed-in WordPress user is not necessarily one of your staff: subscribers and shop customers sign in too. Two rules keep them out of your workspace: - **Gate the token route on a capability, not on being signed in.** The plugin checks `nexafon_use`, which activating it gives to administrators. Give it to your staff’s roles (`wp cap add editor nexafon_use`), never to subscribers or customers. - **Send `emailVerified: true` only when the site has confirmed the address.** Nexafon matches a user to a member by email the first time they sign in, so whoever can set an account’s email to a member’s address can act as that member. WordPress confirms an email change made on the profile page, but an admin or a plugin such as WooCommerce (account details, checkout) changes it without asking. The plugin sends `true` only when `NEXAFON_EMAILS_VERIFIED` is set; without it, Nexafon refuses a user’s first sign-in. To install it on your own site, copy `nexafon-example/` to `wp-content/plugins/` and run `composer require nexafon/nexafon-php` in it. The full plugin is `examples/wordpress`. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: --- # Appearance (theming) > Theme the embedded screens with one brand colour, a corner radius, a font and light, dark or automatic mode. Source: https://docs.nexafone.so/v1/web/appearance/ The embedded screens take your brand from four settings. Layout, spacing and screens never change: what users learn in one product works in every other. ```js Nexafon.init({ publishableKey: 'pk_live_…', getUserToken, appearance: { primaryColor: '#16A34A', radius: 10, font: 'inter', colorMode: 'auto', }, }) ``` | Setting | Values | Default | | --- | --- | --- | | `primaryColor` | A hex colour, `#RGB` or `#RRGGBB` | Nexafon blue | | `radius` | Corner radius in px, 0 to 24 (rounded, clamped) | The kit’s radius | | `font` | `'ibm_plex_sans'`, `'inter'` or `'system'` (your visitors’ system font) | `'ibm_plex_sans'` | | `colorMode` | `'light'`, `'dark'` or `'auto'` (follows the device) | `'auto'` | ## One colour, readable everywhere From `primaryColor` Nexafon derives the whole primary family (buttons, hover and pressed states, subtle backgrounds, focus rings, links) for light and dark mode, and the text colour on each, so text stays readable at WCAG AA contrast whatever colour you pass. ## Changing it at runtime ```js // For example when your own app switches to dark mode. Nexafon.setAppearance({ colorMode: 'dark' }) ``` Settings you leave out keep their current value. Invalid values (a misspelled font, `radius: 'big'`) are ignored rather than breaking the embed. ## Partners A partner’s default appearance, set by Nexafon for the partner, applies to all of its stores’ embeds. Settings passed to `init` or `setAppearance` win over it field by field. ## Language and direction `locale: 'en' | 'so' | 'ar'` sets the language of the embedded screens. Arabic renders right to left; phone numbers, codes and amounts stay left to right inside it. --- # Realtime events reference > The live events your page receives from the embedded screens with Nexafon.on, their payloads, and how they relate to webhooks. Source: https://docs.nexafone.so/v1/web/events/ The hub frame keeps one realtime connection to Nexafon for the page and passes events to your code as they happen: a ringing call shows in the embed within about 150 ms of Nexafon learning of it, and reaches your listeners at the same moment. ```js const stop = Nexafon.on('call.ringing', (call) => { showBanner(`Call from ${call.callerName || call.callerNumber}`) }) // stop() removes the listener; Nexafon.off(name, listener) does the same. ``` The wrappers expose the same events: `useNexafonEvent` in [React](/v1/web/react/) and [Vue](/v1/web/vue/), `NexafonService.events(name)` in [Angular](/v1/web/angular/). ## Events | Event | When | Payload | | --- | --- | --- | | `ready` | A frame finished loading | `{ kind }`: `'hub'`, `'app'`, `'inbox'`, `'softphone'`, `'call-button'` or `'call'` | | `session.ready` | The session started (or was renewed with a new user token) | `{ userId, stores: [{ workspaceId, name }] }` | | `session.error` | The session could not start, such as an invalid token or no free seat | `{ code, message }`: `code` is the API’s [error code](/v1/errors/) | | `call.ringing` | A call to this user started ringing | Call details | | `call.answered` | A call was answered | Call details | | `call.ended` | A call ended | Call details and `reason` | | `message.received` | A customer’s message arrived in a conversation the user can see | `{ workspaceId, conversationId, messageId }` | | `store.changed` | The user switched store in the [store switcher](/v1/partners/store-switcher/) | `{ workspaceId }` | Call details are: | Field | | | --- | --- | | `workspaceId` | The workspace (store) of the call | | `callId` | The call’s id, as in the API and in webhooks | | `kind` | `whatsapp_inbound`, `whatsapp_outbound`, `internal_direct`, `internal_group` or `meeting` | | `callerName` | The caller’s name, when known | | `callerNumber` | The caller’s number (E.164) for WhatsApp calls | ## Browser events or webhooks? Events in the page are for the user in front of it: show a banner, open the order of the caller, update a badge. They only arrive while the page is open and only for what that user may see. For anything your **systems** must not miss (CRM sync, order processing, reports) use [webhooks](/v1/webhooks/): they are signed, retried for three days and cover the whole workspace. ## Protocol Events travel over a versioned, origin-checked `postMessage` bridge between Nexafon’s frames and the loader; your code only ever sees the payloads above. The realtime WebSocket behind it is internal to the SDK and not part of the public v1 API. --- # Mobile SDKs (coming soon) > Native iOS, Android, Flutter and React Native SDKs are in development; this page lists what they will offer and what works today. Source: https://docs.nexafone.so/v1/mobile/ :::caution[Coming soon] The mobile SDKs are not released yet. This page will carry their guides when they ship; nothing below needs code today. ::: | Platform | Package | Status | | --- | --- | --- | | iOS (Swift) | Nexafon iOS SDK | In development | | Android (Kotlin) | Nexafon Android SDK | In development | | Flutter | Nexafon Flutter plugin | In development | | React Native | Nexafon React Native module | In development | ## What is ready for them already The server side of a mobile integration works today and does not change when the SDKs ship: - **Publishable keys** take allowed iOS bundle ids and Android package names (**Developers → API keys**). The session exchange checks them from the `X-Ios-Bundle-Identifier` and `X-Android-Package` headers. - **User tokens** are the same as for the web: your server signs them with a [server helper](/v1/server/). - **SDK sessions** come from `POST /api/v1/sdk/sessions` and are refreshed with `POST /api/v1/sdk/sessions/refresh` ([reference](/v1/api/sdk/)); on mobile they belong in the Keychain or Keystore, never in plain storage. Until the SDKs ship, a mobile app can show the [web SDK](/v1/web/) in a web view for the inbox; calls in the background need the native SDKs and [mobile push](/v1/calling/mobile-push/). --- # Server helpers > Sign user tokens and verify webhooks with one function call in Node, PHP, Python or Go, and drive the Partner API. Source: https://docs.nexafone.so/v1/server/ Your server does three things for a Nexafon integration, and each helper does each in one call, with no dependencies beyond the language’s standard library: | Task | Node | PHP | Python | Go | | --- | --- | --- | --- | --- | | Sign a user token | `createUserToken` | `Tokens::createUserToken` | `create_user_token` | `CreateUserToken` | | Sign a partner user token | `createPartnerUserToken` | `Tokens::createPartnerUserToken` | `create_partner_user_token` | `CreatePartnerUserToken` | | Verify a webhook | `verifyWebhook` | `Webhook::verify` | `verify_webhook` | `VerifyWebhook` | | Partner API client | `PartnerClient` | `PartnerClient` | `PartnerClient` | `NewPartnerClient` | | Package | `@nexafon/node` (npm) | `nexafon/nexafon-php` (Composer) | `nexafon` (PyPI) | `github.com/Keyd-Solutions/nexafon/packages/server-go` | | Runtime | Node 20.19+ | PHP 8.1+ with ext-curl | Python 3.9+ | Go 1.22+ | :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: Tokens from every helper carry the same claims, and the same webhook fixtures verify in all four; Nexafon’s CI checks both against the real API. Guides: [Node](/v1/server/node/), [PHP](/v1/server/php/), [Python](/v1/server/python/), [Go](/v1/server/go/). Another language? A user token is a plain HS256 JWT: [user tokens](/v1/server/user-tokens/) describes it, and [signatures](/v1/webhooks/signatures/) the webhook check. ## Configuration | Variable | Read by | | | --- | --- | --- | | `NEXAFON_SECRET_KEY` | `createUserToken` | A workspace secret key (`sk_live_…`/`sk_test_…`) | | `NEXAFON_PARTNER_SECRET_KEY` | partner tokens, `PartnerClient` | A partner secret key (`psk_live_…`/`psk_test_…`) | | `NEXAFON_API_URL` | `PartnerClient` | The API origin, `https://api.nexafone.so` | Each can also be passed explicitly. Keep secret keys in your server’s environment or secret store; never ship them to a browser or an app. --- # User tokens > The JWT your server signs to tell Nexafon who is signed in, its claims, how users map to members, and how the SDK exchanges it for a session. Source: https://docs.nexafone.so/v1/server/user-tokens/ A **user token** tells Nexafon which of your users is in front of the screen. Your server signs it; the SDK sends it with your publishable key to `POST /api/v1/sdk/sessions` and gets an SDK session in return. The helpers sign it in one call; this page is for understanding it, or for signing it in another language. ## Format A JWT signed with **HS256**. The HMAC key is the full secret key string (`sk_live_…`/`sk_test_…`), of the **same mode** as the publishable key. | Claim | Required | | | --- | --- | --- | | `sub` | yes | Your id for the user, at most 255 characters. After the first sign-in Nexafon links it to the member | | `jti` | yes | A new random id for every token; each token is accepted **once** | | `exp` | yes | Expiry, at most **10 minutes** ahead (the helpers use 5) | | `iat` | no | Issued at | | `name` | no | The name shown in Nexafon; updates the member’s display name | | `email` | first sign-in | Matches the user to the member with that email | | `email_verified` | first sign-in | Must be `true` for Nexafon to trust `email` | | `role` | no | Caps the session to this role’s permissions (`admin`, `supervisor`, `agent`, `viewer`) | ```json { "sub": "user-42", "name": "Amina Ali", "email": "amina@yourcompany.com", "email_verified": true, "role": "agent", "iat": 1790000000, "exp": 1790000300, "jti": "3d1f0c2e-7a51-4c1e-9b8a-0d6c5e4f3a21" } ``` ## Which member the session acts as 1. A `sub` that signed in before maps to the same member again. 2. Otherwise the member with the token’s `email`, only when `email_verified` is `true`: an unverified email would let someone claim another member’s account. 3. Otherwise, if the workspace adds SDK users automatically, a new member with the token’s `role`, which may not be above the role the workspace allows for them. If not, the exchange answers `sdk_member_not_found`. The workspace **owner** never signs in through the SDK (`sdk_role_not_allowed`): whoever holds a secret key could otherwise act as the owner. A disabled member answers `sdk_member_disabled`. ## What the session may do The session acts as that member, with at most: - the member’s own role; - the **scopes of the secret key** that signed the token, so give that key the permissions your embedded users need (see [security](/v1/security/#secret-key-scopes)); - the token’s `role`, when present. ## Lifetime | | | | --- | --- | | User token | Up to 10 minutes; once | | SDK session token (`sdk_…`) | 15 minutes; `POST /api/v1/sdk/sessions/refresh` returns a new one and ends the old | | Refreshing | Up to 12 hours (`refreshUntil`); then the SDK asks `getUserToken` for a new user token | Have `getUserToken` fetch a new token every time; the SDK calls it on start and when a session can no longer be refreshed. `POST /api/v1/sdk/sessions/revoke` ends a session at once. ## Errors | Code | Meaning | | --- | --- | | [`invalid_user_token`](/v1/errors/invalid_user_token/) | Not a valid HS256 JWT of a secret key of this workspace and mode, or a required claim is missing | | [`user_token_expired`](/v1/errors/user_token_expired/) | `exp` passed, or more than 10 minutes ahead | | [`user_token_replayed`](/v1/errors/user_token_replayed/) | The `jti` was used before | | [`sdk_email_required`](/v1/errors/sdk_email_required/) | First sign-in without `email` | | [`sdk_email_not_verified`](/v1/errors/sdk_email_not_verified/) | First sign-in without `email_verified: true` | | [`sdk_member_not_found`](/v1/errors/sdk_member_not_found/) | No member with that email | | [`client_not_allowed`](/v1/errors/client_not_allowed/) | The page’s origin is not allowed for the publishable key | Partners sign a different token that lists stores: see [partner user tokens](/v1/partners/user-tokens/). --- # Node > Sign user tokens and verify webhooks in Node with @nexafon/node. Source: https://docs.nexafone.so/v1/server/node/ `@nexafon/node` needs Node 20.19 or later and has no dependencies. Types are included. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npm install @nexafon/node ``` ## Token endpoint ```ts title="server.ts" // 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 } ``` :::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. ::: Options: `secretKey` (defaults to `NEXAFON_SECRET_KEY`), `expiresIn` in seconds (default 300, at most 600). An invalid input throws a `NexafonTokenError` naming the problem. ## Webhook receiver ```ts title="server.ts" 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 } ``` `verifyWebhook(headers, rawBody, secret)` accepts Node’s `req.headers`, a Fetch `Headers` object or a plain record; `secret` can be a list while you rotate. With Express, read the body with `express.raw({ type: 'application/json' })` so it stays raw. The whole file runs as is (`node server.ts`); it is the [quickstart](/v1/quickstart/) server. For partners, see the [Partner API](/v1/partners/api/). --- # PHP > Sign user tokens and verify webhooks in PHP with nexafon/nexafon-php. Source: https://docs.nexafone.so/v1/server/php/ `nexafon/nexafon-php` needs PHP 8.1 or later with ext-curl, and no framework. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh composer require nexafon/nexafon-php ``` ## Token endpoint ```php title="nexafon-token.php" if (!nexafon_may_use($user)) { http_response_code(403); exit; } // Signed with NEXAFON_SECRET_KEY (sk_test_… or sk_live_…), which never leaves this server. $token = Tokens::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'], ]); header('Content-Type: text/plain'); header('Cache-Control: no-store'); echo $token; ``` :::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. ::: Options (second argument): `secretKey` (defaults to `NEXAFON_SECRET_KEY`) and `expiresIn` (seconds, at most 600). Invalid input throws `Nexafon\TokenException`. ## Webhook receiver ```php title="webhook.php" // The raw body, exactly as received: parsing it first changes the bytes the signature covers. $body = file_get_contents('php://input'); try { $event = Webhook::verify(getallheaders(), $body, getenv('NEXAFON_WEBHOOK_SECRET')); } catch (WebhookVerificationException) { http_response_code(400); exit; } // Deliveries can repeat: skip an $event['id'] you have already handled. error_log($event['type'] . ' ' . $event['id']); http_response_code(204); ``` `Webhook::verify` takes `getallheaders()`, `$_SERVER` or a PSR-7 request’s `getHeaders()`, and a list of secrets while you rotate. In Laravel or Symfony pass `$request->getContent()` as the body. The full files are `docs/snippets/php/nexafon-token.php` and `docs/snippets/php/webhook.php` in the repository. --- # Python > Sign user tokens and verify webhooks in Python with the nexafon package. Source: https://docs.nexafone.so/v1/server/python/ The `nexafon` package needs Python 3.9 or later and has no dependencies. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh pip install nexafon ``` ## Token endpoint ```python title="app.py" def may_use_nexafon(user): # Everyone who gets a token may reach your workspace's inbox and calls: answer 403 unless the user is your staff, # never a customer who signed up. The sample's user is the teammate you invited. return user["is_staff"] def nexafon_token(user): # Signed with NEXAFON_SECRET_KEY (sk_test_… or sk_live_…), which never leaves this server. # 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. return nexafon.create_user_token( user["id"], user["name"], "agent", email=user["email"], email_verified=user["email_verified"] ) ``` :::caution[Send `email_verified` 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. ::: Keyword options: `secret_key` (defaults to `NEXAFON_SECRET_KEY`) and `expires_in` (seconds, at most 600). Invalid input raises `nexafon.NexafonTokenError`. ## Webhook receiver ```python title="app.py" if self.path == "/hooks/nexafon": # The raw body, exactly as received: parsing it first changes the bytes the signature covers. raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) try: event = nexafon.verify_webhook(dict(self.headers), raw, os.environ["NEXAFON_WEBHOOK_SECRET"]) except nexafon.WebhookVerificationError: self.send_response(400) self.end_headers() return # Deliveries can repeat: skip an event["id"] you have already handled. print(event["type"], event["id"]) self.send_response(204) self.end_headers() return ``` With Flask pass `request.headers` and `request.get_data()`; with Django `request.headers` and `request.body`. The sample is a complete standard-library server (`python3 app.py`). --- # Go > Sign user tokens and verify webhooks in Go with the server-go module. Source: https://docs.nexafone.so/v1/server/go/ The module needs Go 1.22 or later and has no dependencies. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh go get github.com/Keyd-Solutions/nexafon/packages/server-go ``` ## Token endpoint ```go title="main.go" func nexafonToken(w http.ResponseWriter, r *http.Request) { u := signedInUser(r) if u == nil { w.WriteHeader(http.StatusUnauthorized) return } if !mayUseNexafon(u) { w.WriteHeader(http.StatusForbidden) return } // "" signs with NEXAFON_SECRET_KEY (sk_test_… or sk_live_…), which never leaves this server. // 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. token, err := nexafon.CreateUserToken("", nexafon.User{ ID: u.ID, Name: u.Name, Role: "agent", Email: u.Email, EmailVerified: u.EmailVerified, }) if err != nil { http.Error(w, "", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") w.Header().Set("Cache-Control", "no-store") _, _ = io.WriteString(w, token) } ``` :::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. ::: `CreateUserToken(secretKey, user, opts...)`: an empty `secretKey` reads `NEXAFON_SECRET_KEY`; `nexafon.ExpiresIn(d)` sets the lifetime (at most 10 minutes). ## Webhook receiver ```go title="main.go" func nexafonWebhook(w http.ResponseWriter, r *http.Request) { // The raw body, exactly as received: parsing it first changes the bytes the signature covers. body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { w.WriteHeader(http.StatusBadRequest) return } ev, err := nexafon.VerifyWebhook(r.Header, body, os.Getenv("NEXAFON_WEBHOOK_SECRET")) if errors.Is(err, nexafon.ErrInvalidWebhook) { w.WriteHeader(http.StatusBadRequest) return } if err != nil { w.WriteHeader(http.StatusInternalServerError) return } // Deliveries can repeat: skip an ev.ID you have already handled. log.Println(ev.Type, ev.ID) w.WriteHeader(http.StatusNoContent) } ``` Pass `nexafon.AlsoSecret(previous)` while you rotate the secret. The sample is a complete `net/http` server (`go run .`). --- # Webhooks > Receive signed events from Nexafon on your server, set up an endpoint, and handle deliveries safely. Source: https://docs.nexafone.so/v1/webhooks/ Webhooks tell your systems what happened in a workspace: a message arrived, a call was missed, an order was paid. Each delivery is an HTTPS `POST` of a JSON event, signed so you can check it came from Nexafon. ## 1. Add an endpoint In the dashboard open **Developers → Webhooks**, add your URL and pick the [events](/v1/webhooks/events/) it should receive. Or create it through the API with a secret key that has `developers:manage`: ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/$WORKSPACE_ID/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/nexafon", "events": ["message.received", "call.missed"]}' ``` The answer carries the endpoint’s **signing secret** (`whsec_…`) once. Store it with your other secrets. The URL must be `https` and reach the public internet: Nexafon resolves the host and refuses private, link-local and cloud metadata addresses, connects to the address it checked, and never follows redirects. A workspace can have up to 20 endpoints, each subscribed to up to 64 event types. ## 2. Receive and verify ```ts title="server.ts" 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 } ``` [Signatures](/v1/webhooks/signatures/) has the receiver in PHP, Python and Go, and the check without a helper. ## 3. Answer fast Answer any `2xx` within **10 seconds**, then do the work: put the event on your own queue first if handling it takes longer. Anything else (a timeout, `3xx`, `4xx`, `5xx`) counts as a failure and is [retried](/v1/webhooks/retries/) for up to 3 days. ## The event ```json { "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c", "type": "message.received", "createdAt": "2026-09-21T14:13:18Z", "workspaceId": "aaaaaaaa-0000-0000-0000-000000000000", "data": { "message": { "id": "…", "conversationId": "…", "direction": "inbound", "type": "text", "text": "Hello" }, "conversation": { "id": "…", "status": "open", "contact": { "phone": "252611234567" } } } } ``` | Field | | | --- | --- | | `id` | Unique per event. Retries, replays and redeliveries send the same `id`: **dedupe on it** | | `type` | The [event type](/v1/webhooks/events/) | | `createdAt` | When it happened (RFC 3339, UTC) | | `workspaceId` | The workspace (store) it happened in | | `partner` | On a partner’s stores only: `{ "storeExternalRef": "…" }` ([partner webhooks](/v1/partners/webhooks/)) | | `data` | The event’s objects, in the same shape as the [API](/v1/api/) returns them | Every delivery also carries these headers: | Header | | | --- | --- | | `Nexafon-Signature` | `t=,v1=` ([signatures](/v1/webhooks/signatures/)) | | `Nexafon-Event-Id` | The event’s `id` | | `Nexafon-Event-Type` | The event’s `type` | | `Nexafon-Delivery-Attempt` | 1 for the first attempt, then 2, 3… | | `User-Agent` | `Nexafon-Webhooks/1.0` | Ordering is best effort per endpoint: use `createdAt` and the objects’ own versions where order matters. --- # Webhook events > Every event type Nexafon delivers to webhook endpoints, when it is sent, and what its data contains. Source: https://docs.nexafone.so/v1/webhooks/events/ `GET /api/v1/workspaces/{workspaceId}/webhook-event-types` lists the same catalog at runtime. This table is generated from the backend’s event catalog: | Event | Sent when | | --- | --- | | `message.received` | A customer's WhatsApp message arrived | | `message.status` | A message the business sent was sent, delivered, read or failed | | `conversation.assigned` | A conversation was assigned to a member or unassigned | | `conversation.closed` | A conversation was closed | | `call.ringing` | A call started ringing | | `call.answered` | A call was answered | | `call.ended` | An answered call ended | | `call.missed` | A call ended without anyone answering | | `meeting.scheduled` | A meeting was scheduled | | `meeting.ended` | A meeting ended | | `template.status_changed` | Meta approved, rejected, paused or disabled a message template, or changed its category | | `contact.created` | A new contact was added | | `billing.low_balance` | The wallet balance fell below its alert threshold | | `invoice.created` | An invoice was issued | | `invoice.paid` | An invoice was paid | | `billing.dunning` | An unpaid invoice's dunning step: a reminder, the workspace going past due or read-only, or reactivated once paid | | `catalog.product_status` | Meta approved or rejected a product of the store's WhatsApp catalog; a rejection carries Meta's reason | | `order.created` | A customer's WhatsApp cart became an order, priced from the store's current prices | | `order.paid` | An order's payment succeeded | | `order.payment_failed` | An order's payment failed; the order still awaits payment | | `order.status_changed` | An order moved to another status; carries the previous status and who changed it | | `order.cancelled` | An order was cancelled | ## Payloads `data` holds the objects the event is about, in the shapes the [API reference](/v1/api/schemas/) documents: | Events | `data` | | --- | --- | | `message.received`, `message.status` | `message`, `conversation` | | `conversation.assigned` | `conversation`, `previousAssigneeMemberId` | | `conversation.closed` | `conversation` | | `contact.created` | `contact` | | `call.ringing`, `call.answered`, `call.ended`, `call.missed` | `call`: kind, status, caller, team, ring settings, participants, `endReason`, timestamps | | `meeting.scheduled` | `meeting`, and `occurrence` when one occurrence of a recurring meeting changed | | `meeting.ended` | `meeting`, `occurrence`, `attendance` | | `template.status_changed` | `template`, `previousStatus`, `previousCategory` | | `billing.low_balance` | `balanceCents`, `thresholdCents`, `currency`, `ledgerEntryId` | | `invoice.created`, `invoice.paid` | `id`, `number`, `status`, `currency`, `totalCents`, `amountDueCents`, `periodStart`, `periodEnd`, `issuedAt`, `dueAt` | | `billing.dunning` | `invoiceId`, `number`, `stage` (`reminder`, `past_due`, `suspended`, `reactivated`), `step`, `amountDueCents`, `currency`, `suspendOn` | | `catalog.product_status` | `retailerId`, `product` (`externalId`, `name`), `variant` (`externalId`, `size`, `color`), `status` (`approved`, `rejected`), `reason`, `previousStatus`, `reviewedAt` | | `order.created`, `order.paid`, `order.cancelled` | `order` | | `order.payment_failed` | `order`, `reason` | | `order.status_changed` | `order`, `previousStatus`, `status`, `changedBy` (`type`, `id`) | An `order` carries `id`, `number`, `status`, the customer, `currency`, `subtotalCents`, `discountCents`, `deliveryFeeCents`, `totalCents`, the discount and coupon, fulfilment and delivery details, `items` and timestamps. Money is always in integer cents (USD). Reactions to messages are not sent as `message.received`. ## Test events **Send test** in the dashboard, or `POST …/webhook-endpoints/{endpointId}/test`, delivers one `webhook.test` event at once, signed like any other. It does not count towards the endpoint’s health; endpoints cannot subscribe to it. --- # Verifying signatures > Check the Nexafon-Signature header with the helpers in Node, PHP, Python and Go, or by hand, and rotate secrets without downtime. Source: https://docs.nexafone.so/v1/webhooks/signatures/ Every delivery carries `Nexafon-Signature: t=,v1=`, where the signature is the hex HMAC-SHA256 of `"."` keyed with the endpoint’s secret (`whsec_…`). Check it before trusting anything in the body. 1. Read the **raw** body, exactly as received. Parsing and re-serialising JSON changes the bytes. 2. Split the header on `,`: take `t` and every `v1`. 3. Reject the delivery if `t` is more than **5 minutes** from your clock (it stops replays of captured deliveries). 4. Compute HMAC-SHA256 of `t + "." + body` with your secret; compare it with each `v1` in constant time. 5. Answer `400` if nothing matches. Then dedupe on the event `id`. ## With the helpers The [server helpers](/v1/server/) do all of this in one call and return the parsed event. ```ts title="Node" 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 } ``` ```php title="PHP" // The raw body, exactly as received: parsing it first changes the bytes the signature covers. $body = file_get_contents('php://input'); try { $event = Webhook::verify(getallheaders(), $body, getenv('NEXAFON_WEBHOOK_SECRET')); } catch (WebhookVerificationException) { http_response_code(400); exit; } // Deliveries can repeat: skip an $event['id'] you have already handled. error_log($event['type'] . ' ' . $event['id']); http_response_code(204); ``` ```python title="Python" if self.path == "/hooks/nexafon": # The raw body, exactly as received: parsing it first changes the bytes the signature covers. raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) try: event = nexafon.verify_webhook(dict(self.headers), raw, os.environ["NEXAFON_WEBHOOK_SECRET"]) except nexafon.WebhookVerificationError: self.send_response(400) self.end_headers() return # Deliveries can repeat: skip an event["id"] you have already handled. print(event["type"], event["id"]) self.send_response(204) self.end_headers() return ``` ```go title="Go" func nexafonWebhook(w http.ResponseWriter, r *http.Request) { // The raw body, exactly as received: parsing it first changes the bytes the signature covers. body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { w.WriteHeader(http.StatusBadRequest) return } ev, err := nexafon.VerifyWebhook(r.Header, body, os.Getenv("NEXAFON_WEBHOOK_SECRET")) if errors.Is(err, nexafon.ErrInvalidWebhook) { w.WriteHeader(http.StatusBadRequest) return } if err != nil { w.WriteHeader(http.StatusInternalServerError) return } // Deliveries can repeat: skip an ev.ID you have already handled. log.Println(ev.Type, ev.ID) w.WriteHeader(http.StatusNoContent) } ``` ## Without a helper Self-contained functions you can copy; Nexafon’s backend tests run each of them against real signed deliveries. ```js title="verify.mjs" import { createHmac, timingSafeEqual } from 'node:crypto' // Pass the raw request body exactly as received, before any JSON parsing. export function verifyNexafonWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) { let timestamp = '' const signatures = [] for (const part of signatureHeader.split(',')) { const [key, value] = part.trim().split('=') if (key === 't') timestamp = value if (key === 'v1') signatures.push(value) } if (!/^\d+$/.test(timestamp)) return false if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest() return signatures.some((signature) => { const given = Buffer.from(signature, 'hex') return given.length === expected.length && timingSafeEqual(given, expected) }) } ``` ```php title="verify.php" $toleranceSeconds) { return false; } $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); foreach ($signatures as $signature) { if (hash_equals($expected, $signature)) { return true; } } return false; } ``` ```python title="verify.py" import hashlib import hmac import time def verify_nexafon_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool: """Pass the raw request body exactly as received, before any JSON parsing.""" timestamp = "" signatures = [] for part in signature_header.split(","): key, _, value = part.strip().partition("=") if key == "t": timestamp = value elif key == "v1": signatures.append(value) if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds: return False expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, signature) for signature in signatures) ``` ```go title="verify.go" package nexafon import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "strings" "time" ) // VerifyWebhook checks the Nexafon-Signature header against the raw request body, exactly as received. func VerifyWebhook(rawBody []byte, signatureHeader, secret string, tolerance time.Duration) bool { var timestamp string var signatures []string for _, part := range strings.Split(signatureHeader, ",") { key, value, _ := strings.Cut(strings.TrimSpace(part), "=") switch key { case "t": timestamp = value case "v1": signatures = append(signatures, value) } } seconds, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } if age := time.Since(time.Unix(seconds, 0)); age > tolerance || age < -tolerance { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := mac.Sum(nil) for _, signature := range signatures { given, err := hex.DecodeString(signature) if err == nil && hmac.Equal(given, expected) { return true } } return false } ``` ## Rotating the secret `POST …/webhook-endpoints/{endpointId}/rotate-secret` returns a new secret. For `keepPreviousHours` (default 24, at most 168) every delivery is signed with **both** secrets: the header then carries two `v1` values, and a receiver holding either one verifies it. Deploy the new secret within that window; the helpers accept a list of secrets meanwhile. --- # Delivery, retries and replays > How Nexafon retries failed webhook deliveries for three days, turns off failing endpoints, and lets you see every attempt and replay events. Source: https://docs.nexafone.so/v1/webhooks/retries/ ## Retries A delivery fails when your endpoint does not answer `2xx` within 10 seconds. Nexafon then retries with exponential backoff: about 30 seconds after the first failure, then doubling (1, 2, 4 minutes…) up to one attempt every 12 hours, with some jitter, for **72 hours** after the first attempt. Each attempt sends the same event `id` with a higher `Nexafon-Delivery-Attempt`. The first attempt starts within about a second of the event (p95). ## Endpoints that keep failing An endpoint that fails for the whole 72-hour window is **turned off** and the workspace’s owners are notified by email (“Webhooks to … are turned off”). Fix the receiver, send a test event, then turn the endpoint back on in **Developers → Webhooks** or with `PATCH …/webhook-endpoints/{endpointId}` and `{"status": "active"}`. ## Delivery logs `GET …/webhook-endpoints/{endpointId}/deliveries` lists every attempt: event, attempt number, status, response code, latency, the start of any error, and the next retry. The dashboard shows the same log. Stored events and attempts contain customer data, so they are deleted after a retention period (a little over two weeks by default). ## Replays `POST …/webhook-endpoints/{endpointId}/events/{eventId}/replay` sends a stored event again, with its original `id`, to an active endpoint (a turned-off endpoint answers `webhook_endpoint_disabled`). Use it after fixing a bug in your receiver. All of these operations are in the [webhook endpoints reference](/v1/api/webhook-endpoints/); partners have the same under `/api/v1/partner/webhook-endpoints` ([partner webhooks](/v1/partners/webhooks/)). --- # WhatsApp calling > How customers call the business on WhatsApp, how the business calls customers, call permissions and WhatsApp’s limits. Source: https://docs.nexafone.so/v1/calling/ Nexafon answers and places WhatsApp voice calls on the business’s own number, in the browser, inside your product. Embedded users take calls in the [softphone](/v1/web/) or ``, and start them with a `` or `Nexafon.call(phone)`. ## Before calls work - **Calling is turned on** for the workspace’s WhatsApp number (in the dashboard’s WhatsApp settings). Meta only allows it on numbers with a messaging limit of at least 2,000; otherwise you get `whatsapp.calling_cannot_be_enabled`. - The member’s role includes `calls:answer`, and for embedded users the signing secret key’s scopes include it too. - The browser has a microphone and the user allows it. The SDK frames carry `allow="microphone; autoplay"`. ## Customers calling the business A call to the business number rings the team its call routing picks, with that team’s ring strategy (everyone at once, in order, or priority on plans that include it). The first member to answer takes it; the others see that someone answered. Unanswered calls end as **missed** and show in the call history. Your page hears `call.ringing`, `call.answered` and `call.ended` ([events](/v1/web/events/)); your server gets `call.ringing`, `call.answered`, `call.ended` and `call.missed` [webhooks](/v1/webhooks/events/). ## The business calling a customer WhatsApp lets a business call a customer only after the customer **allowed** it: | Rule (set by WhatsApp) | | | --- | --- | | Asking for permission | One request a day and two a week per customer, until a call connects (`call_permission_request_limit`) | | Inside the 24-hour window | The request is a message with **Allow** / **Don’t allow** buttons | | Outside the window | Only an approved template with a call permission button can ask (`call_permission_template_missing`) | | How long it lasts | 7 days, unless the customer makes it permanent; they can also refuse | | Without permission | Calling answers `call_permission_missing` | The inbox shows the permission state of each customer and offers the request when it is allowed. Other WhatsApp limits Nexafon passes on: - A daily cap on connected business-initiated calls per number, and per customer (`whatsapp.business_call_limit`), and a cap on simultaneous calls (`whatsapp.call_concurrency_limit`). - Business-initiated calls are not available from business numbers in some countries (currently the United States, Canada, Egypt, Vietnam, Nigeria and Türkiye): `whatsapp_call_country_unsupported`. Customers there can still call the business. - WhatsApp can pause calling on a number for low quality (`whatsapp.calling_disabled_low_quality`). Outbound calls are billed per minute to the workspace wallet; with a low balance or past-due billing they pause (`insufficient_balance`, `billing_past_due`) while customers can still call in. See [billing](/v1/billing/). ## Recording On plans with `call_recording`, calls can be recorded once answered. WhatsApp calls need a recording announcement chosen in the WhatsApp call settings first (`call_recording_consent_missing`). Recordings are listed by the [recordings API](/v1/api/recordings/). ## Prayer times Ringing can pause during prayer times, which Nexafon calculates for the workspace’s location (Umm al-Qura method by default); a call that ends this way has `endReason: "prayer"`. --- # Internal calls and meetings > Calls between staff members inside Nexafon, group calls and scheduled voice meetings, and their plan limits. Source: https://docs.nexafone.so/v1/calling/internal/ Besides WhatsApp calls with customers, members call each other inside Nexafon, in the browser, at no WhatsApp cost. ## Internal calls | Kind | | Permission | | --- | --- | --- | | Direct (`internal_direct`) | One member calls another | `internal_calls:start` | | Group (`internal_group`) | Several members, or a whole team, at once | `group_calls:start` | From your page: ```js await Nexafon.startInternalCall(['member-id-1', 'member-id-2']) ``` Member ids come from the [members API](/v1/api/members/). The callee’s softphone or app rings wherever they are signed in; a missed staff call can be called back. ## Meetings Scheduled voice meetings (`meeting`) have a title, a start and a length, invited members, and optional recurrence. Members with `meetings:create` schedule and host them; `meetings:manage` changes, cancels or ends any meeting and sees attendance. A meeting opens 10 minutes before its start (`meeting_not_open` before that). Invitees get reminders, and your server gets `meeting.scheduled` and `meeting.ended` (with attendance) [webhooks](/v1/webhooks/events/). ## Plan limits | Feature | Limits | | --- | --- | | `meetings.voice` | Whether the plan includes scheduled meetings | | `meetings.max_minutes` | The longest meeting | | `calls.max_participants` | People in one call or meeting | Above a limit, starting or scheduling answers `plan_limit_reached`. --- # Mobile push and call notifications (coming soon) > Ringing calls on iOS with CallKit and on Android with ConnectionService, through the upcoming mobile SDKs. Source: https://docs.nexafone.so/v1/calling/mobile-push/ :::caution[Coming soon] Mobile push ships with the [mobile SDKs](/v1/mobile/), which are not released yet. ::: When they ship, an app using the iOS or Android SDK (or the Flutter and React Native modules) will ring for WhatsApp and team calls even when it is in the background or closed: - **iOS:** VoIP push (PushKit) and CallKit, so calls show on the lock screen like phone calls. - **Android:** high-priority FCM messages and ConnectionService (self-managed), with a full-screen incoming-call notification. The **Developers → Push** page of the dashboard already stores the app’s push credentials: an Apple APNs key (`.p8` file, key id and bundle id) and a Firebase service account. This page will document the app side step by step when the SDKs ship. Until then, calls ring in the browser through the [web SDK](/v1/web/). --- # Partners > How a platform that runs many businesses runs all of Nexafon inside itself, with stores, partner keys, staff sign-in and partner-wide webhooks. Source: https://docs.nexafone.so/v1/partners/ A **partner** is a platform such as a marketplace or a restaurant system. Each business on it is a Nexafon **store**, an ordinary workspace with its own WhatsApp number, inbox, calls, billing and commerce, linked to the partner by the partner’s own id for it (`externalRef`). Store owners and staff never open Nexafon: everything runs inside the partner’s platform. | Step | How | Guide | | --- | --- | --- | | Nexafon sets up the partner | Domains, default store plan and default appearance, and the partner’s keys | Below | | The partner’s server creates stores | `POST /api/v1/partner/stores` with a partner secret key | [Partner API](/v1/partners/api/) | | It keeps products in sync | Products and categories per store | [Catalog sync](/v1/commerce/catalog/) | | Staff open Nexafon inside the platform | A partner user token listing their stores, and `` | [User tokens and the full app](/v1/partners/user-tokens/) | | Users with several stores switch between them | The store switcher inside the embed | [Store switcher](/v1/partners/store-switcher/) | | The platform hears every store’s events | One partner-wide webhook endpoint | [Partner webhooks](/v1/partners/webhooks/) | ## What Nexafon sets up for you Nexafon creates the partner in its console with: - the partner’s **domains**: every store’s embed may be framed on them, and partner sessions start only there; - the **default store plan**: every new store starts on it, with its included seats and features (such as `commerce.orders`); - the **default appearance** of all its stores’ embeds; - the partner’s **keys**: secret keys `psk_live_…`/`psk_test_…` (with scopes and an optional IP allow-list) for the partner’s servers, and publishable keys `ppk_live_…`/`ppk_test_…` for its pages. Each is shown once. A suspended partner’s keys, sessions and store embeds stop working until Nexafon reactivates it; the stores’ data is kept. ## Partner key scopes A partner secret key carries partner permissions for the Partner API: | Permission | Lets the key | Needs plan feature | | --- | --- | --- | | `partner.account:view` | See the partner's profile, status and default store plan | | | `partner.stores:manage` | Create, rename, close and reopen the partner's stores | | | `partner.stores:view` | See the partner's stores | | | `partner.webhooks:manage` | Manage the partner-wide webhook endpoints that receive every store's events | | …and any [workspace permissions](/v1/security/#secret-key-scopes) it acts with inside the partner’s stores. A key that signs [partner user tokens](/v1/partners/user-tokens/) needs `partner.stores:manage`, and the sessions it starts act with no more than its workspace permissions. ## The example `examples/partner` (a Node server and a React page) creates two stores, signs a partner user token for both, embeds `` and verifies an order webhook; it runs in CI with Playwright. --- # Partner API > Create and manage stores, act inside them, manage their staff and save their PayPoint keys with a partner secret key. Source: https://docs.nexafone.so/v1/partners/api/ The Partner API is for the partner’s **servers**: it takes a partner secret key (`psk_live_…`/`psk_test_…`) as `Authorization: Bearer` and refuses browsers (no CORS). The full reference is [Partner API](/v1/api/partner/); the Node, PHP, Python and Go [helpers](/v1/server/) include a client. ## Stores Create a store when a business is created on your platform. The call is **idempotent on `externalRef`**: repeating it returns the same store, so retries are safe. ```ts title="partner.ts" // When a store opens on your platform: idempotent on externalRef, so retrying returns the same store. export async function openStore(shop: { id: string name: string ownerName: string ownerEmail: string }) { const store = await nexafon.stores.create({ externalRef: shop.id, name: shop.name, owner: { name: shop.ownerName, email: shop.ownerEmail }, country: 'SO', timezone: 'Africa/Mogadishu', locale: 'so', }) return store.workspaceId } ``` ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/stores" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"externalRef": "shop-1", "name": "Shop One", "owner": {"name": "Amina Ali", "email": "amina@shop.example"}, "country": "SO", "timezone": "Africa/Mogadishu", "locale": "so"}' ``` The new store starts on your default store plan with its subscription running, and answers with its `workspaceId`. Then: | Operation | | | --- | --- | | `GET /api/v1/partner/stores` | Your stores, paged | | `GET /api/v1/partner/stores/{externalRef}` | One store | | `PATCH /api/v1/partner/stores/{externalRef}` | Rename it, or reopen a closed store with `status: active` | | `POST /api/v1/partner/stores/{externalRef}/close` | Close it: it becomes read-only and keeps its data | | `GET /api/v1/partner` | Your partner profile, status and default plan’s features | ## Acting inside a store The same key calls the workspace operations of the [API](/v1/api/) for **your own** stores, by `workspaceId`: ```bash curl "https://api.nexafone.so/api/v1/workspaces/$WORKSPACE_ID/conversations" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` A workspace that is not one of your stores (another partner’s, or a business outside your platform) answers `404`, exactly like one that does not exist. The key’s workspace scopes limit what it may do there. ## Staff Staff sign in with [partner user tokens](/v1/partners/user-tokens/), which create or update their membership of each store they list. Leaving a store out of a token does **not** remove access; tell Nexafon explicitly: ```ts title="partner.ts" // Staff are managed on your platform: change a role or remove someone from a store when they change there. export async function staffChanged(storeRef: string, userId: string, role: string | null) { if (role === null) await nexafon.members.disable(storeRef, userId) else await nexafon.members.update(storeRef, userId, { role }) } ``` | Operation | | | --- | --- | | `PUT /api/v1/partner/stores/{externalRef}/members/{partnerUserId}` | Change the user’s role (and teams) in that store; their menu updates live | | `DELETE /api/v1/partner/stores/{externalRef}/members/{partnerUserId}` | Disable them in that store; their open sessions end within seconds | ## The store’s PayPoint key Store sales are paid into the store’s own PayPoint account (see [store payments](/v1/commerce/payments/)): | Operation | | | --- | --- | | `PUT /api/v1/workspaces/{workspaceId}/payments/paypoint` | Save the store’s API key and merchant id; answers only “saved” and the last 4 characters | | `POST …/payments/paypoint/test` | Check the key with PayPoint without charging anyone | | `PUT …/payments/paypoint/wallets` | The wallets customers may pay with | | `DELETE …/payments/paypoint` | Remove the key | The key is encrypted at rest and never returned or logged. ## Retries and idempotency Creating operations accept an `Idempotency-Key` header; see the [API overview](/v1/api/#idempotency). Rate limits are per key ([rate limits](/v1/rate-limits/)). --- # 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 . 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 ``: 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 ``` 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" Tocktify · Stores
Tocktify · Seller centre
``` 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 ( ) } export function WebhookList() { const [events, setEvents] = useState([]) useEffect(() => { const load = () => fetch('/api/webhook-events') .then((r) => r.json() as Promise) .then(setEvents, () => undefined) load() const timer = setInterval(load, 2000) return () => clearInterval(timer) }, []) return events.map((e) => (
  • {e.type} {e.store}
  • )) } ``` 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 `` 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 `` on your order page. --- # Store switcher > How users with several stores move between them inside the embed, how to open a given store, and the store.changed event. Source: https://docs.nexafone.so/v1/partners/store-switcher/ A user whose token lists several stores gets a **store switcher** inside ``: each store with its unread chats and ringing calls, so they see at a glance where they are needed. Calls from any of their stores ring them wherever they are. ## Opening a store Pass `store` (the store’s `externalRef` or its workspace id) to open it first: ```html ``` The React, Vue and Angular wrappers take the same `store` prop. Changing it reloads the component on that store. `Nexafon.call(phone)` calls from the store the user viewed most recently. ## Following the switch ```js Nexafon.on('store.changed', ({ workspaceId }) => { // For example, show that store's orders next to the embed. }) ``` `session.ready` lists the session’s stores (`{ workspaceId, name }`) once the session starts. --- # Partner webhooks > Receive every store's events at one signed endpoint, with the store's externalRef on each event. Source: https://docs.nexafone.so/v1/partners/webhooks/ A partner endpoint receives the events of **all** of the partner’s stores, so the platform needs one receiver, not one per store. Manage endpoints with the Partner API (`partner.webhooks:manage`): ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://platform.example/hooks/nexafon", "events": ["order.created", "order.paid", "message.received"]}' ``` Each event carries the store it comes from: ```json { "id": "7c6b5a49-3827-4165-9483-a2b1c0d9e8f7", "type": "order.paid", "createdAt": "2026-09-21T14:13:18Z", "workspaceId": "bbbbbbbb-0000-0000-0000-000000000000", "partner": { "storeExternalRef": "shop-1" }, "data": { "order": { "number": 1042, "status": "paid", "totalCents": 2800 } } } ``` Deliveries are signed, retried and logged exactly like a workspace’s: see [signatures](/v1/webhooks/signatures/), [retries](/v1/webhooks/retries/) and the [event catalog](/v1/webhooks/events/). The helpers’ `verifyWebhook` handles both kinds. ```ts title="partner.ts" // One endpoint receives every store's events; partner.storeExternalRef names the store. export function onDelivery(headers: Record, rawBody: Buffer) { const event = verifyWebhook(headers, rawBody, process.env.NEXAFON_PARTNER_WEBHOOK_SECRET ?? '') if (event.type === 'order.paid') { console.log('paid order in', event.partner?.storeExternalRef, event.id) } return event } ``` Stores can still have their own endpoints too; an event goes to both. --- # WhatsApp commerce > Customers browse a store's products, order and pay without leaving WhatsApp; the store's own platform stays the source of truth. Source: https://docs.nexafone.so/v1/commerce/ With commerce a store sells in WhatsApp: the customer opens the **Shop** from the chat menu, browses categories and products, sends a cart, gives delivery details and pays with mobile money, all inside WhatsApp. The store manages the order in the embedded Orders screen or from its own platform through the API, and the platform hears every step through webhooks. | Part | What happens | Guide | | --- | --- | --- | | Catalog sync | Your platform pushes products; Nexafon mirrors them into the store’s WhatsApp catalog | [Catalog sync](/v1/commerce/catalog/) | | Shop menu | “Shop” in the chat menu opens categories, product lists, sales and the full catalog | [Shop menu](/v1/commerce/shop/) | | Orders | A cart becomes an order priced by the server, with delivery, promotions and status updates | [Orders](/v1/commerce/orders/) | | Payments | The customer pays with PayPoint inside the WhatsApp checkout; the money goes to the store | [PayPoint store payments](/v1/commerce/payments/) | ## What a store needs - A plan with the commerce features: `commerce.catalog` (products), `commerce.orders` (orders) and `commerce.payments` (payments), and the `commerce.products.max` limit. Without them the operations answer `feature_not_in_plan`. Partners usually include them in their default store plan. - A connected WhatsApp number (the catalog is attached to it). - Keys or sessions with the permissions `commerce:view`/`commerce:manage`, `orders:view`/`orders:manage` and `payments:manage`. Money is always integer **cents** in **USD**, and every price, discount and total is computed on Nexafon’s server from the store’s data, never taken from a cart or a client. --- # Catalog sync > Push products, sizes and categories from your platform; Nexafon keeps the store's WhatsApp catalog in sync and reports Meta's reviews. Source: https://docs.nexafone.so/v1/commerce/catalog/ Your platform stays the source of truth: nobody edits products in Nexafon. Whenever a product, price, image, size or stock changes on your side, push it; Nexafon keeps its copy and mirrors it into the store’s WhatsApp catalog, usually within a minute. ## Products ```ts title="partner.ts" // Your platform stays the source of truth: push each change, Nexafon mirrors it into the store's WhatsApp catalog. export async function syncProducts(workspaceId: string) { const store = nexafon.store(workspaceId) await store.categories.upsert([{ externalId: 'shoes', name: 'Shoes' }]) await store.products.batch([ { externalId: 'sneaker-1', name: 'Runner sneaker', categoryExternalId: 'shoes', priceCents: 2700, imageUrls: ['https://cdn.example.com/sneaker-1.jpg'], variants: [ { externalId: 'sneaker-1-42', size: '42', stock: 5 }, { externalId: 'sneaker-1-43', size: '43', stock: 0, availability: 'out_of_stock' }, ], }, ]) } ``` | Operation | | | --- | --- | | `PUT /api/v1/workspaces/{workspaceId}/commerce/products/{externalId}` | Create or update one product (idempotent by your `externalId`) | | `POST …/commerce/products/batch` | Up to 500 products in one call; results come back in the same order | | `DELETE …/commerce/products/{externalId}` | Remove it, also from WhatsApp | | `GET …/commerce/products` | The products with their WhatsApp review status | | `PUT …/commerce/categories` | Create or update categories (`externalId`, `name`), in their display order | | `GET …/commerce/categories` | The categories | A product has a name, description, category, 1 to 10 image URLs, `priceCents`, an optional sale price with a start and end, availability and stock, and optional **variants** (size, colour, each with its own price and stock). Each variant, or the product itself when it has none, is one item in WhatsApp, identified by its `externalId` as WhatsApp’s `retailer_id` (`retailer_id_taken` when another product already uses it). Images are your own public HTTPS URLs; WhatsApp fetches them and Nexafon does not host them. Private or internal addresses are refused. The number of products is limited by the plan’s `commerce.products.max` (`plan_limit_reached`). ## WhatsApp’s review Meta reviews each item. The products list shows each one’s status, and a `catalog.product_status` webhook reports every decision: `approved`, or `rejected` with Meta’s `reason`. Only approved items appear in the Shop. Nexafon also reconciles the whole catalog nightly, so a missed update corrects itself. The store’s **Products** screen in the embed shows the same list, read-only. --- # Shop menu > Shop as an option of the store's WhatsApp team menu, with categories, product lists, sales, the full catalog and opening hours. Source: https://docs.nexafone.so/v1/commerce/shop/ Customers find products from the chat. When the store turns on **Shop** (in the embedded **Routing** settings, with `commerce:manage`), “Shop” appears in the menu new chats get, next to the store’s teams such as Sales and Support. ## What the customer sees 1. Choosing **Shop**, or writing one of the store’s keywords (such as “menu”, “shop” or “dukaan”), opens a list of the store’s categories, plus **Sale** when products are on sale and **View all**. 2. Choosing a category sends its approved products: - 1 product: a single-product message; - 2 to 10: a carousel; - more: a product list in pages of 30, with a **More** button for the next page. 3. **View all** sends the store’s whole WhatsApp catalog. 4. The customer adds items to a cart and sends it; the cart becomes an [order](/v1/commerce/orders/). Outside the shop’s opening hours, the customer gets the store’s closed message with the next opening time. Texts are in the customer’s language (English, Somali or Arabic), else the store’s. ## Settings | Setting | | | --- | --- | | Shop on or off | Shown in the team menu only while the team menu is on and the catalog is active; keywords work either way | | Label per language | The menu row’s text in English, Somali and Arabic | | Position | Where Shop sits among the teams (a menu already showing 10 teams leaves it out) | | Keywords | Words that open the shop | | Order team | The team whose folder receives order conversations | | Opening hours | When the shop answers, using the teams’ working-hours model | Every message is a reply inside WhatsApp’s 24-hour customer window, so the shop costs no template fees. --- # Orders > How WhatsApp carts become server-priced orders with delivery and promotions, the order statuses, the Orders API and order webhooks. Source: https://docs.nexafone.so/v1/commerce/orders/ ## From cart to order 1. The customer sends a cart from the store’s catalog. 2. Nexafon maps each item to the store’s variant, drops unavailable ones with a note, and **prices the order from the store’s current data**, never from the cart: if a price changed, the customer sees the new total before paying. Promotions and the delivery fee apply. 3. The customer gets the order summary and a **Continue** button that opens the checkout: delivery or pickup, area, address, contact number and an optional coupon, with a live total. 4. The order then awaits payment ([PayPoint store payments](/v1/commerce/payments/)). Stores can also ask for a location pin. Each step fires a webhook: `order.created`, then `order.paid` or `order.payment_failed`, `order.status_changed` and `order.cancelled` ([events](/v1/webhooks/events/)). ## Statuses `new` → `awaiting_payment` → `paid` → `preparing` → `out_for_delivery` → `delivered`, plus `cancelled` and `refunded`. Statuses only move forward. Payment moves an order to `paid`; the store (or your platform) sets `preparing`, `out_for_delivery` (delivery orders only), `delivered` and `cancelled`. Each order lists its allowed `nextStatuses`; others answer `order_status_not_allowed`. The customer gets a WhatsApp message on the statuses the store chooses (by default paid, out for delivery, delivered and cancelled): a normal reply inside the 24-hour window, otherwise an approved `order_update` template billed to the wallet. ## The Orders API | Operation | Permission | | --- | --- | | `GET /api/v1/workspaces/{workspaceId}/commerce/orders` | `orders:view` | | `GET …/commerce/orders/{orderId}` (items, delivery, status history) | `orders:view` | | `POST …/commerce/orders/{orderId}/status` with `{"status": "preparing"}` | `orders:manage` | | `POST …/commerce/orders/{orderId}/cancel` with a reason | `orders:manage` | Stock is not reserved: your platform sends stock through [catalog sync](/v1/commerce/catalog/) and can reject an order by cancelling it. ## Delivery `GET`/`PUT …/commerce/delivery` sets delivery and pickup: the delivery **areas** with their fees, free delivery above an amount, the pickup address, whether to ask for a location pin, and which statuses notify the customer. The customer’s address is remembered per number for the next order. ## Promotions `…/commerce/promotions` manages three kinds: | Kind | | | --- | --- | | `flash_sale` | Lowers prices of products in scope inside its window; also shown in the WhatsApp catalog | | `number_list` | A discount for the numbers of a list (`…/commerce/number-lists`, up to 10,000 numbers; or past customers) | | `coupon` | Applies when the customer enters its code at checkout; limits per customer and overall | A promotion takes `percentOff` or `amountOffCents`, a scope (all, products or categories), an optional window and minimum subtotal. By default **only the best discount applies**; promotions marked `combinable` add up. Wrong coupon codes are rate-limited per order and per customer. --- # PayPoint store payments > Customers pay WhatsApp orders with mobile money through PayPoint inside the checkout; the money goes to the store's own PayPoint account. Source: https://docs.nexafone.so/v1/commerce/payments/ The customer pays without leaving WhatsApp, and the money goes straight to the **store’s own PayPoint account**. Nexafon never holds the sale money; its own fee is charged to the store’s wallet ([billing](/v1/billing/#order-fees)). ## Set up the store 1. Save the store’s PayPoint API key and merchant id with `PUT /api/v1/workspaces/{workspaceId}/payments/paypoint` (or in the embedded **Payments** screen, with `payments:manage`). Nexafon answers only “saved” and the last 4 characters; the key is encrypted and never returned or logged. 2. `POST …/payments/paypoint/test` checks the key with PayPoint without charging anyone. 3. `PUT …/payments/paypoint/wallets` picks the wallets customers may use: `evc_plus`, `zaad`, `sahal`, `jeeb`, `edahab`, `premier_wallet`. ## What the customer sees 1. After delivery details the checkout shows **Pay with PayPoint**: the store’s wallets, the number to charge (their WhatsApp number, editable) and the total. 2. On **Pay**, Nexafon starts the charge with the store’s key and closes the checkout. A “waiting” message asks the customer to enter their PIN on their phone. 3. When PayPoint confirms, the order is marked `paid` **exactly once**, the customer gets a confirmation with the order number, items, total, wallet and reference, and `order.paid` goes out. 4. If it fails, the customer gets the reason in plain words (not enough balance, cancelled, no PIN in time, no account on this wallet) with **Try again** and **Another wallet**; `order.payment_failed` goes out. Only one payment attempt runs at a time per order. If no payment is started 10 minutes after checkout, the payment step is sent once more. A payment confirmed late, after another attempt already paid, never pays twice: it is flagged for a refund decision. ## Guarantees - No public endpoint can start a charge or mark an order paid: charges start only from the checkout, and only PayPoint’s confirmation of Nexafon’s own payment reference settles an order. - The amount charged is the order’s server-computed total. - Payment status is polled from PayPoint for up to 24 hours, so a PIN approved late is still recorded. --- # Billing concepts > Plans and features, seats, the prepaid wallet and top-ups, metered usage, invoices and dunning, and Nexafon's fee on paid WhatsApp orders. Source: https://docs.nexafone.so/v1/billing/ Each workspace (each partner store, too) is billed on its own. Billing screens work inside `` for users with `billing:manage`; others see that they lack access. Prices and limits always come from Nexafon’s server: an integration sends ids and quantities, never amounts. ## Plans and features A plan grants **features**: on/off switches, limits and metered items. Operations declare the feature they need (`x-feature` in the [API reference](/v1/api/)); without it they answer `feature_not_in_plan`, and above a limit `plan_limit_reached`. `GET /api/v1/me/access` tells the embedded screens what the workspace has. | Feature | Kind | What it covers | | --- | --- | --- | | `call_recording` | boolean | Record calls | | `calls.max_participants` | limit (participant) | People in one call | | `calls.priority_ring` | boolean | Priority ring strategy | | `commerce.catalog` | boolean | Store product catalog | | `commerce.orders` | boolean | Store orders | | `commerce.payments` | boolean | Store customer payments | | `commerce.products.max` | limit (product) | Products in the store catalog | | `ivr.depth` | limit (level) | Nested IVR menus | | `meetings.max_minutes` | limit (minute) | Length of one scheduled meeting | | `meetings.voice` | boolean | Scheduled voice meetings | | `recordings.library` | limit (recording) | Recordings in the call audio library | | `recordings.retention_days` | limit (day) | Recording retention | | `sdk.embeds` | boolean | SDK embeds and custom domain | | `seats` | limit (seat) | Staff seats; viewers do not use one | | `sso.workspace` | boolean | Workspace SSO (Microsoft or Google) | | `storage.bytes` | metered (byte) | Stored files: WhatsApp media, recordings, prompts, invoices and exports | | `whatsapp.number` | limit (number) | One business WhatsApp number per workspace | | `workspace.core` | boolean | The workspace itself; every workspace always has it and no plan can take it away | ## Seats Each member with a paid role takes one **seat** (viewers do not). A plan includes some seats; more are added as a seat add-on, quoted and prorated by the server. When every seat is taken, inviting or adding a member answers `seat_limit_reached`, and a partner user token for that store answers `no_free_seat`, unless the owner turned on **add seats automatically**: then a seat is bought from the wallet in the same step (or refused with `insufficient_balance` when the wallet cannot pay). Fewer seats take effect at renewal. ## Wallet Usage is paid from the workspace’s **wallet**: - **Prepaid** (the default): top up first; paid actions stop when the balance cannot cover them (`insufficient_balance`), while replies inside WhatsApp’s 24-hour window and incoming calls keep working. - **Postpaid** (approved by Nexafon): usage counts against a credit limit (`credit_limit_reached`) and is invoiced monthly. Store owners top up with mobile money through PayPoint from the billing screen: choose the wallet and number, enter the PIN on the phone, and the balance updates live once PayPoint confirms. When the balance falls below its alert threshold the owner is notified and a `billing.low_balance` webhook goes out; auto top-up can refill it. ## Usage Metered usage is charged to the wallet as it happens, at the workspace’s rate card: - paid WhatsApp templates (marketing and utility messages outside the 24-hour window); - business-initiated WhatsApp calls, per minute; - stored files beyond the plan’s allowance. ## Invoices and dunning Subscriptions renew monthly and are invoiced (`invoice.created`, `invoice.paid` webhooks). An unpaid invoice goes through reminders, then **past due** (paid templates and outbound calls pause: `billing_past_due`), then the workspace becomes read-only (`workspace_read_only`) until it is paid. Each step sends a `billing.dunning` webhook. ## Order fees Nexafon charges a fee on each **paid** WhatsApp order. The sale money goes to the store’s PayPoint account, so the fee is charged to the store’s **wallet**: - It is computed on the goods after discounts, **without delivery**: `fee = round_half_up(goods × percent) + fixed`, then kept between the rule’s minimum and maximum. Integer cents throughout. - The rule is the store’s plan’s fee rule, else Nexafon’s default; the rule in force when the order is paid applies. - It is booked once per order, within a second of payment, and shows as “Fee on order #N” in the wallet history. - A prepaid fee is booked even if it takes the balance below zero; postpaid fees appear on the monthly invoice. - Refunding or cancelling a paid order does not reverse its fee. Example: $27.00 of goods and $1.00 delivery at 1.5% books one fee of $0.41 (2700 × 1.5% = 40.5 cents, rounded up). --- # API overview > Base URL, authentication, versioning and deprecation, idempotency, pagination, errors and limits of the public v1 REST API. Source: https://docs.nexafone.so/v1/api/ The public API is the stable **v1** surface: every operation tagged `public` in `api/openapi.public.yaml`, generated from the backend’s own operation definitions, so this reference always matches what the server runs. - **[API console](/v1/api/console/)**: try every operation in the browser. - **[OpenAPI 3.1 spec](/v1/api/openapi.json)**: for code generators, Postman or agents. - Reference by area: | Area | Operations | | --- | --- | | [SDK sessions](/v1/api/sdk/) | 3 | | [Inbox](/v1/api/inbox/) | 5 | | [Members](/v1/api/members/) | 1 | | [Teams](/v1/api/teams/) | 2 | | [Templates](/v1/api/templates/) | 7 | | [Recordings](/v1/api/recordings/) | 2 | | [Webhook endpoints](/v1/api/webhook-endpoints/) | 10 | | [Commerce](/v1/api/commerce/) | 22 | | [Store payments](/v1/api/payments/) | 5 | | [Partner API](/v1/api/partner/) | 18 | ## Base URL and authentication ```text https://api.nexafone.so/api/v1/… ``` Send a credential as a bearer token: ```bash curl "https://api.nexafone.so/api/v1/workspaces/$WORKSPACE_ID/teams" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` | Credential | From | Reaches | | --- | --- | --- | | Secret key `sk_live_…`/`sk_test_…` | Your server | Its own workspace, with its scopes | | SDK session `sdk_…` | The SDK, after a [user token](/v1/server/user-tokens/) | The member’s workspace (or stores), within the signing key’s scopes | | Partner secret key `psk_…` | The partner’s server | The [Partner API](/v1/partners/api/) and the partner’s own stores | The `{workspaceId}` in a path only selects among what the credential may reach; anything else answers `404`, as does an id from another workspace. Requests and responses are JSON (`application/json`); errors are `application/problem+json`. ## Versioning and deprecation Public responses carry `API-Version: v1`. Within v1, changes are additive: new operations, new optional fields, new enum values and new event types. Write clients that ignore fields they do not know. An operation that will go away is first **deprecated**: it keeps working for at least **180 days**, is marked in the reference, and its responses carry `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers with the dates. Breaking changes come only in a new version, documented here side by side. ## Idempotency Operations that create things accept an `Idempotency-Key` header (1 to 255 visible ASCII characters, such as a UUID). Retrying the same request with the same key within 24 hours returns the stored response with `Idempotent-Replayed: true` instead of doing the work twice. The same key with a different body answers `idempotency_key_reused`; a retry while the first is still running answers `idempotency_in_flight`. A `5xx` releases the key so the retry runs again. Responses that show a secret once (a new webhook secret, an SDK session) never replay it. ## Pagination List operations take `limit` and return a `next` cursor when there are more; pass it as `cursor` to get the next page. ## Errors and limits Errors are [Problem Details](/v1/errors/) with a stable `code`. Requests are [rate limited](/v1/rate-limits/) per IP address, user and key. Every response carries `X-Request-Id`; quote it when you contact support. --- # SDK sessions API > Reference for the 3 public v1 operations of the SDK sessions API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/sdk/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## Exchange a publishable key and a user token for an SDK session `POST /api/v1/sdk/sessions` The publishable key's origin and app restrictions apply, and each user token is accepted once. The session acts as the workspace member the user maps to, within the scopes of the secret key that signed the token. A partner's publishable key is exchanged only on the partner's sites; its session acts as the user's membership of each store the token lists, and each new membership takes a seat in its store (no_free_seat names a store without one). | | | | --- | --- | | Operation id | `createSdkSession` | | Authentication | None: the request body carries what the operation needs. | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `X-Ios-Bundle-Identifier` | header | string | | iOS apps: the app's bundle id, checked against the key's allowed bundle ids | | `X-Android-Package` | header | string | | Android apps: the app's package name, checked against the key's allowed packages | ### Request body `application/json`: [CreateSessionInputBody](/v1/api/schemas/#createsessioninputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `embedOrigin` | string | | Sent by the Nexafon embed page: the origin of the page that frames it (max length 300) | | `publishableKey` | string | yes | pk_live_… or pk_test_…, or a partner's ppk_live_… or ppk_test_… (min length 1, max length 128) | | `userToken` | string | yes | A JWT (HS256) your server signs with one of the workspace's secret keys of the same mode: sub (your user id), jti (new for each token) and exp (at most 10 minutes ahead) are required; name, email, email_verified (a boolean) and role are optional. The first time a user signs in, email is required and email_verified must be true: that email links the user to the member with the same email, or to a new member. Later sign-ins use that link and ignore the email. With a partner's publishable key, the partner's server signs it with one of the partner's secret keys (psk_…) of the same mode that has the partner.stores:manage scope, and the session acts with no more than that key's store permissions: sub (the partner's user id), name, jti and exp are required, email and locale (en, so or ar) optional, and stores lists 1 to 50 of the partner's stores as {externalRef, role, teams?}: role is one of owner, admin, supervisor, agent or viewer, and teams, when present, the store's team ids the user is in. The user becomes or stays an active member of each store with that role (min length 1, max length 32768) | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [SDKSessionBody](/v1/api/schemas/#sdksessionbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 403 | Forbidden | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/sdk/sessions" \ -H "Content-Type: application/json" \ -d '{"publishableKey":"string","userToken":"string"}' ``` ## Refresh an SDK session `POST /api/v1/sdk/sessions/refresh` Returns a new token; the one sent stops working at once. | | | | --- | --- | | Operation id | `refreshSdkSession` | | Authentication | None: the request body carries what the operation needs. | ### Request body `application/json`: [TokenInputBody](/v1/api/schemas/#tokeninputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | The current session token (min length 1, max length 128) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [SDKSessionBody](/v1/api/schemas/#sdksessionbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/sdk/sessions/refresh" \ -H "Content-Type: application/json" \ -d '{"token":"string"}' ``` ## End an SDK session `POST /api/v1/sdk/sessions/revoke` Its realtime connections close. | | | | --- | --- | | Operation id | `revokeSdkSession` | | Authentication | None: the request body carries what the operation needs. | ### Request body `application/json`: [TokenInputBody](/v1/api/schemas/#tokeninputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | The current session token (min length 1, max length 128) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/sdk/sessions/revoke" \ -H "Content-Type: application/json" \ -d '{"token":"string"}' ``` --- # Inbox API > Reference for the 5 public v1 operations of the Inbox API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/inbox/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List conversations `GET /api/v1/workspaces/{workspaceId}/conversations` Conversations of your teams, pinned first, then the latest first. | | | | --- | --- | | Operation id | `listConversations` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `inbox:reply` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `status` | query | `"active"` \| `"open"` \| `"snoozed"` \| `"closed"` | | active: open and snoozed | | `teamId` | query | string (uuid) | | Only this team's conversations | | `assignee` | query | string | | me, none, or a member id (UUID) | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ConversationListOutputBody](/v1/api/schemas/#conversationlistoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/conversations" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Message a customer with a template `POST /api/v1/workspaces/{workspaceId}/conversations` Opens the customer's conversation, or reuses it, and queues the template. Outside the 24-hour window WhatsApp allows only templates. | | | | --- | --- | | Operation id | `startConversation` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `inbox:reply` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Request body `application/json`: [StartInputBody](/v1/api/schemas/#startinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | [SendBody](/v1/api/schemas/#sendbody) | yes | An approved template | | `phone` | string | yes | The customer's number with its country code (min length 3, max length 32) | | `teamId` | string (uuid) | | The team the conversation belongs to; the routing's default team when it is one of yours | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [StartOutputBody](/v1/api/schemas/#startoutputbody) | | 403 | Forbidden | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 503 | Service Unavailable | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/conversations" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"message":{"clientMsgId":"string","type":"text"},"phone":"string"}' ``` ## Get a conversation `GET /api/v1/workspaces/{workspaceId}/conversations/{conversationId}` | | | | --- | --- | | Operation id | `getConversation` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `inbox:reply` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `conversationId` | path | string | yes | Conversation id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ConversationBody](/v1/api/schemas/#conversationbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/conversations/{conversationId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## List a conversation's messages `GET /api/v1/workspaces/{workspaceId}/conversations/{conversationId}/messages` | | | | --- | --- | | Operation id | `listMessages` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `inbox:reply` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `conversationId` | path | string | yes | Conversation id (UUID) | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ThreadOutputBody](/v1/api/schemas/#threadoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/conversations/{conversationId}/messages" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Send a message `POST /api/v1/workspaces/{workspaceId}/conversations/{conversationId}/messages` Queues the message and returns it pending; status updates follow as message.updated events. Repeating clientMsgId returns the same message. | | | | --- | --- | | Operation id | `sendMessage` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `inbox:reply` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `conversationId` | path | string | yes | Conversation id (UUID) | ### Request body `application/json`: [SendBody](/v1/api/schemas/#sendbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientMsgId` | string | yes | Your id for this message: sending it again returns the message already queued instead of sending twice (min length 1, max length 64, pattern `^[A-Za-z0-9._:-]+$`) | | `media` | [MediaSendBody](/v1/api/schemas/#mediasendbody) | | | | `replyToMessageId` | string (uuid) | | Quote this message | | `template` | [TemplateSendBody](/v1/api/schemas/#templatesendbody) | | The only kind of message allowed outside the 24-hour window | | `text` | string | | The message, or the caption of an image, video or document (max length 4096) | | `type` | `"text"` \| `"image"` \| `"video"` \| `"audio"` \| `"document"` \| `"sticker"` \| `"template"` | yes | | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [MessageBody](/v1/api/schemas/#messagebody) | | 403 | Forbidden | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 503 | Service Unavailable | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/conversations/{conversationId}/messages" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"clientMsgId":"string","type":"text"}' ``` --- # Members API > Reference for the 1 public v1 operations of the Members API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/members/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List the workspace's members `GET /api/v1/workspaces/{workspaceId}/members` | | | | --- | --- | | Operation id | `listMembers` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `team:view` | | Plan feature | `seats` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ListMembersOutputBody](/v1/api/schemas/#listmembersoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/members" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Teams API > Reference for the 2 public v1 operations of the Teams API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/teams/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List the workspace's teams `GET /api/v1/workspaces/{workspaceId}/teams` | | | | --- | --- | | Operation id | `listTeams` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `team:view` | | Plan feature | `seats` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ListTeamsOutputBody](/v1/api/schemas/#listteamsoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/teams" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Get a team `GET /api/v1/workspaces/{workspaceId}/teams/{teamId}` | | | | --- | --- | | Operation id | `getTeam` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `team:view` | | Plan feature | `seats` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `teamId` | path | string | yes | Team id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [TeamDetailBody](/v1/api/schemas/#teamdetailbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/teams/{teamId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Templates API > Reference for the 7 public v1 operations of the Templates API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/templates/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List WhatsApp templates `GET /api/v1/workspaces/{workspaceId}/templates` The templates of the workspace's WhatsApp Business Account, as Meta last reported them. | | | | --- | --- | | Operation id | `listTemplates` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:send` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `status` | query | `"submitting"` \| `"submit_failed"` \| `"pending"` \| `"approved"` \| `"rejected"` \| `"paused"` \| `"disabled"` \| `"in_appeal"` \| `"pending_deletion"` \| `"limit_exceeded"` \| `"archived"` \| `"deleting"` | | Only templates with this status, such as approved for the send picker | | `category` | query | `"marketing"` \| `"utility"` \| `"authentication"` | | | | `prefix` | query | string | | Only names starting with this | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ListOutputBody3](/v1/api/schemas/#listoutputbody3) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Submit a new WhatsApp template `POST /api/v1/workspaces/{workspaceId}/templates` Checks the template against Meta's rules, then queues it for Meta's review: it starts as submitting, and template.updated events follow as Meta reviews it. | | | | --- | --- | | Operation id | `createTemplate` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:manage` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Request body `application/json`: [CreateInputBody2](/v1/api/schemas/#createinputbody2) | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | `"marketing"` \| `"utility"` \| `"authentication"` | yes | | | `definition` | [TemplateDefinitionBody](/v1/api/schemas/#templatedefinitionbody) | yes | | | `language` | string | yes | A WhatsApp language code, such as en_US, so or ar (min length 2, max length 16) | | `name` | string | yes | Lowercase letters, digits and underscores (min length 1, max length 512, pattern `^[a-z0-9_]+$`) | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [TemplateBody](/v1/api/schemas/#templatebody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"category":"marketing","definition":{"body":null},"language":"string","name":"string"}' ``` ## Get a WhatsApp template with its usage `GET /api/v1/workspaces/{workspaceId}/templates/{templateId}` | | | | --- | --- | | Operation id | `getTemplate` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:send` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `templateId` | path | string | yes | Template id (UUID) | | `since` | query | string (date-time) | | Count usage from this time; the last 30 days by default | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [TemplateBody](/v1/api/schemas/#templatebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates/{templateId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Edit a WhatsApp template `PUT /api/v1/workspaces/{workspaceId}/templates/{templateId}` Approved, rejected and paused templates go back to Meta's review; a template Meta refused to create is submitted again. | | | | --- | --- | | Operation id | `editTemplate` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:manage` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `templateId` | path | string | yes | Template id (UUID) | ### Request body `application/json`: [EditInputBody](/v1/api/schemas/#editinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | `"marketing"` \| `"utility"` | | Unchanged when absent | | `definition` | [TemplateDefinitionBody](/v1/api/schemas/#templatedefinitionbody) | yes | | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [TemplateBody](/v1/api/schemas/#templatebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates/{templateId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"definition":{"body":null}}' ``` ## Delete a WhatsApp template `DELETE /api/v1/workspaces/{workspaceId}/templates/{templateId}` Deletes this language of the template at Meta; a template.deleted event follows. | | | | --- | --- | | Operation id | `deleteTemplate` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:manage` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `templateId` | path | string | yes | Template id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [TemplateBody](/v1/api/schemas/#templatebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates/{templateId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Preview a template with parameters `POST /api/v1/workspaces/{workspaceId}/templates/{templateId}/preview` Checks the parameters exactly as sending does, and returns the message the customer will see. Invalid parameters get 422 with the field of each problem. | | | | --- | --- | | Operation id | `previewTemplate` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:send` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `templateId` | path | string | yes | Template id (UUID) | ### Request body `application/json`: [PreviewInputBody](/v1/api/schemas/#previewinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `components` | array of [TemplateComponentBody](/v1/api/schemas/#templatecomponentbody) | | The parameters, exactly as sending the template takes them (max 12 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PreviewOutputBody](/v1/api/schemas/#previewoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates/{templateId}/preview" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Sync templates from Meta `POST /api/v1/workspaces/{workspaceId}/templates/sync` Queues a read of every template from Meta; only templates that changed are written. A template.synced event follows. | | | | --- | --- | | Operation id | `syncTemplates` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `templates:manage` | | Plan feature | `whatsapp.number` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [SyncOutputBody](/v1/api/schemas/#syncoutputbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/templates/sync" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Recordings API > Reference for the 2 public v1 operations of the Recordings API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/recordings/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List the call audio library `GET /api/v1/workspaces/{workspaceId}/recordings` Recordings that a team's settings play are listed only for that team's members, Owner and Admin. | | | | --- | --- | | Operation id | `listRecordings` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `recordings:view` | | Plan feature | `recordings.library` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `kind` | query | `"greeting"` \| `"option"` \| `"fallback_closed"` \| `"fallback_prayer"` \| `"fallback_missed"` \| `"hold"` \| `"custom"` | | Only recordings of this kind | | `language` | query | `"en"` \| `"so"` \| `"ar"` | | Only recordings in this language | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ListOutputBody](/v1/api/schemas/#listoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/recordings" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Get a recording `GET /api/v1/workspaces/{workspaceId}/recordings/{recordingId}` | | | | --- | --- | | Operation id | `getRecording` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `recordings:view` | | Plan feature | `recordings.library` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `recordingId` | path | string | yes | Recording id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [RecordingBody](/v1/api/schemas/#recordingbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/recordings/{recordingId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Webhook endpoints API > Reference for the 10 public v1 operations of the Webhook endpoints API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/webhook-endpoints/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List the workspace's webhook endpoints `GET /api/v1/workspaces/{workspaceId}/webhook-endpoints` | | | | --- | --- | | Operation id | `listWebhookEndpoints` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointsOutputBody](/v1/api/schemas/#webhookendpointsoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Register a webhook endpoint `POST /api/v1/workspaces/{workspaceId}/webhook-endpoints` Every delivery is a POST with the event as JSON and a Nexafon-Signature header: t=,v1=." keyed with the secret>. Reject a timestamp more than 5 minutes old. The secret is in this response only. A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `createWebhookEndpoint` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Request body `application/json`: [CreateWebhookEndpointInputBody](/v1/api/schemas/#createwebhookendpointinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string | yes | Event types from GET /webhook-event-types (min 1 items, max 64 items) | | `url` | string | yes | An https URL on the public internet (min length 8, max length 2048) | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [WebhookSecretOutputBody](/v1/api/schemas/#webhooksecretoutputbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"events":["string"],"url":"string"}' ``` ## Get a webhook endpoint `GET /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}` | | | | --- | --- | | Operation id | `getWebhookEndpoint` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Change a webhook endpoint, or turn it off or back on `PATCH /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}` A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `updateWebhookEndpoint` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Request body `application/json`: [UpdateWebhookEndpointInputBody](/v1/api/schemas/#updatewebhookendpointinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string or null | | (min 1 items, max 64 items) | | `status` | `"active"` \| `"disabled"` | | active turns a disabled endpoint back on | | `url` | string | | (min length 8, max length 2048) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PATCH "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Delete a webhook endpoint and its delivery log `DELETE /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}` | | | | --- | --- | | Operation id | `deleteWebhookEndpoint` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## List every delivery attempt to an endpoint `GET /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/deliveries` | | | | --- | --- | | Operation id | `listWebhookDeliveries` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `eventId` | query | string (uuid) | | Only this event's attempts | | `status` | query | `"succeeded"` \| `"failed"` | | | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookDeliveriesOutputBody](/v1/api/schemas/#webhookdeliveriesoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/deliveries" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Send an event to an endpoint again `POST /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/events/{eventId}/replay` The event keeps its id. It is retried on the usual schedule; the delivery log shows every attempt. | | | | --- | --- | | Operation id | `replayWebhookEndpointEvent` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `eventId` | path | string | yes | Event id (UUID) from the delivery log | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/events/{eventId}/replay" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Replace a webhook endpoint's signing secret `POST /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/rotate-secret` Until previousSecretExpiresAt the Nexafon-Signature header carries a v1 signature for each secret, so the receiver can switch over without missing an event. The new secret is in this response only. A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `rotateWebhookEndpointSecret` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `keepPreviousHours` | query | integer (int64) | | How long deliveries are also signed with the replaced secret | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookSecretOutputBody](/v1/api/schemas/#webhooksecretoutputbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/rotate-secret" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Send a webhook.test event to an endpoint `POST /api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/test` Works on a disabled endpoint too. It is tried once; the delivery log shows the result. | | | | --- | --- | | Operation id | `sendWebhookTest` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [WebhookTestOutputBody](/v1/api/schemas/#webhooktestoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-endpoints/{endpointId}/test" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## List the events an endpoint can subscribe to `GET /api/v1/workspaces/{workspaceId}/webhook-event-types` | | | | --- | --- | | Operation id | `listWebhookEventTypes` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `developers:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEventTypesOutputBody](/v1/api/schemas/#webhookeventtypesoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/webhook-event-types" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Commerce API > Reference for the 22 public v1 operations of the Commerce API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/commerce/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## List the store's product categories in order `GET /api/v1/workspaces/{workspaceId}/commerce/categories` | | | | --- | --- | | Operation id | `listProductCategories` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [CategoriesBody](/v1/api/schemas/#categoriesbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/categories" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Create or rename categories and set their order `PUT /api/v1/workspaces/{workspaceId}/commerce/categories` | | | | --- | --- | | Operation id | `upsertProductCategories` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Request body `application/json`: [CategoriesInputBody](/v1/api/schemas/#categoriesinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `categories` | array of [CategoryInputBody](/v1/api/schemas/#categoryinputbody) or null | yes | In display order; categories left out keep their names and follow these (min 1 items, max 500 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [CategoriesBody](/v1/api/schemas/#categoriesbody) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/categories" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"categories":[{"externalId":null,"name":null}]}' ``` ## Get the store's delivery areas, fees and customer updates `GET /api/v1/workspaces/{workspaceId}/commerce/delivery` | | | | --- | --- | | Operation id | `getDeliverySettings` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [DeliveryBody](/v1/api/schemas/#deliverybody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/delivery" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Replace the store's delivery areas, fees and customer updates `PUT /api/v1/workspaces/{workspaceId}/commerce/delivery` Orders already placed keep the area name and fee they were priced with. Delivery needs at least one active area. | | | | --- | --- | | Operation id | `saveDeliverySettings` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Request body `application/json`: [DeliverySettingsBody](/v1/api/schemas/#deliverysettingsbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `areas` | array of [DeliveryAreaBody](/v1/api/schemas/#deliveryareabody) | yes | In the order checkout lists them; an area left out is removed (max 100 items) | | `deliveryEnabled` | boolean | yes | | | `freeDeliveryAboveCents` | integer (int64) | | Delivery is free from this order amount (after discounts) in cents; never free when absent (min 0, max 100000000000) | | `notifyStatuses` | array of string | yes | Order statuses the customer gets a WhatsApp message for: awaiting_payment, paid, preparing, out_for_delivery, delivered, cancelled, refunded (max 7 items) | | `pickupAddress` | string | yes | Where pickup orders are collected; shown to the customer (max length 500) | | `pickupEnabled` | boolean | yes | | | `requestLocation` | boolean | yes | Ask the customer for a location pin after checkout when they chose delivery | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [DeliveryBody](/v1/api/schemas/#deliverybody) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/delivery" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"areas":[{"active":null,"feeCents":null,"name":null}],"deliveryEnabled":true,"notifyStatuses":["string"],"pickupAddress":"string","pickupEnabled":true,"requestLocation":true}' ``` ## List the store's number lists `GET /api/v1/workspaces/{workspaceId}/commerce/number-lists` | | | | --- | --- | | Operation id | `listNumberLists` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [NumberListsBody](/v1/api/schemas/#numberlistsbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/number-lists" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Create a number list from uploaded numbers or a past-orders rule `POST /api/v1/workspaces/{workspaceId}/commerce/number-lists` | | | | --- | --- | | Operation id | `createNumberList` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Request body `application/json`: [NumberListFields](/v1/api/schemas/#numberlistfields) | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"uploaded"` \| `"past_orders"` | yes | uploaded: the numbers given; past_orders: every customer with at least minOrders paid orders | | `minOrders` | integer (int32) | | Required for past_orders (min 1, max 1000) | | `name` | string | yes | (min length 1, max length 80) | | `phones` | array of string or null | | International numbers with their country code, for uploaded (max 10000 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [NumberListBody](/v1/api/schemas/#numberlistbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/number-lists" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"uploaded","name":"string"}' ``` ## Get a number list with its numbers `GET /api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}` | | | | --- | --- | | Operation id | `getNumberList` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `listId` | path | string | yes | Number list id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [NumberListBody](/v1/api/schemas/#numberlistbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Replace a number list `PUT /api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}` | | | | --- | --- | | Operation id | `updateNumberList` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `listId` | path | string | yes | Number list id (UUID) | ### Request body `application/json`: [NumberListFields](/v1/api/schemas/#numberlistfields) | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"uploaded"` \| `"past_orders"` | yes | uploaded: the numbers given; past_orders: every customer with at least minOrders paid orders | | `minOrders` | integer (int32) | | Required for past_orders (min 1, max 1000) | | `name` | string | yes | (min length 1, max length 80) | | `phones` | array of string or null | | International numbers with their country code, for uploaded (max 10000 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [NumberListBody](/v1/api/schemas/#numberlistbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"uploaded","name":"string"}' ``` ## Delete a number list `DELETE /api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}` Answers 409 promotion_in_use while a promotion uses it. | | | | --- | --- | | Operation id | `deleteNumberList` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `listId` | path | string | yes | Number list id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/number-lists/{listId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## List the store's orders `GET /api/v1/workspaces/{workspaceId}/commerce/orders` Newest first. Members who do not see every team see the orders of their teams. | | | | --- | --- | | Operation id | `listOrders` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `orders:view` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `status` | query | array of `"new"` \| `"awaiting_payment"` \| `"paid"` \| `"preparing"` \| `"out_for_delivery"` \| `"delivered"` \| `"cancelled"` \| `"refunded"` or null | | Only orders in these statuses, comma-separated | | `fulfilment` | query | `"delivery"` \| `"pickup"` | | | | `phone` | query | string | | Only the orders of this customer number | | `number` | query | integer (int64) | | The order with this number | | `from` | query | string (date-time) | | Created at or after | | `to` | query | string (date-time) | | Created before | | `cursor` | query | string | | | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreOrdersBody](/v1/api/schemas/#storeordersbody) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/orders" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Get an order with its items and history `GET /api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}` | | | | --- | --- | | Operation id | `getOrder` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `orders:view` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `orderId` | path | string | yes | Order id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreOrderBody](/v1/api/schemas/#storeorderbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Cancel an order `POST /api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}/cancel` Stock is not reserved for orders: a partner rejects an order it cannot fill by cancelling it. | | | | --- | --- | | Operation id | `cancelOrder` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `orders:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `orderId` | path | string | yes | Order id (UUID) | ### Request body `application/json`: [OrderCancelBody](/v1/api/schemas/#ordercancelbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | | Why, such as out of stock (max length 500) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreOrderBody](/v1/api/schemas/#storeorderbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}/cancel" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Move an order to preparing, out for delivery, delivered or cancelled `POST /api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}/status` Statuses only move forward; 409 order_status_not_allowed otherwise. The customer gets a WhatsApp message for the statuses the store chose, and the store's endpoints get order.status_changed. | | | | --- | --- | | Operation id | `setOrderStatus` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `orders:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `orderId` | path | string | yes | Order id (UUID) | ### Request body `application/json`: [OrderStatusBody](/v1/api/schemas/#orderstatusbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `note` | string | | Kept in the order's history; for cancelled, the reason (max length 500) | | `status` | `"preparing"` \| `"out_for_delivery"` \| `"delivered"` \| `"cancelled"` | yes | Payment and checkout set the other statuses | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreOrderBody](/v1/api/schemas/#storeorderbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/orders/{orderId}/status" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"preparing"}' ``` ## List the store's products with their WhatsApp catalog status `GET /api/v1/workspaces/{workspaceId}/commerce/products` | | | | --- | --- | | Operation id | `listProducts` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `category` | query | string | | Only the products of this category's externalId | | `status` | query | `"queued"` \| `"in_review"` \| `"approved"` \| `"rejected"` \| `"failed"` \| `"inactive"` | | Only products whose catalog status is this | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [ProductsBody](/v1/api/schemas/#productsbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/products" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Create or update a product by its external id `PUT /api/v1/workspaces/{workspaceId}/commerce/products/{externalId}` Answers 201 when the product is new and 200 otherwise. Sending the same product again changes nothing. Changes reach the store's WhatsApp catalog within a minute. A new product counts against commerce.products.max (403 plan_limit_reached). | | | | --- | --- | | Operation id | `upsertProduct` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `externalId` | path | string | yes | The partner's own id for the product | ### Request body `application/json`: [ProductFields](/v1/api/schemas/#productfields) | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | false keeps the product but takes it out of the WhatsApp catalog; true when absent | | `availability` | `"in_stock"` \| `"out_of_stock"` | | (default `"in_stock"`) | | `categoryExternalId` | string | | A category upserted before; none when absent (max length 100, pattern `^[A-Za-z0-9._:-]*$`) | | `currency` | `"USD"` | | (default `"USD"`) | | `description` | string | | (max length 5000) | | `imageUrls` | array of string or null | yes | HTTPS URLs on the public internet; Meta fetches them, Nexafon does not host them. The first is the main image (min 1 items, max 10 items) | | `name` | string | yes | (min length 1, max length 200) | | `priceCents` | integer (int64) | yes | In US cents (min 0, max 100000000000) | | `saleEndsAt` | string (date-time) | | Open-ended when absent | | `salePriceCents` | integer (int64) | | Below priceCents; shown while the sale window is open (min 0, max 100000000000) | | `saleStartsAt` | string (date-time) | | Open from now when absent | | `stock` | integer (int32) | | Absent when the partner does not count stock; 0 shows the product out of stock (min 0) | | `variants` | array of [VariantBody](/v1/api/schemas/#variantbody) or null | | Sizes or colours, each its own catalog item grouped under the product; the product itself is the one item when absent (max 50 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [UpsertResultBody](/v1/api/schemas/#upsertresultbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/products/{externalId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"imageUrls":["string"],"name":"string","priceCents":0}' ``` ## Delete a product `DELETE /api/v1/workspaces/{workspaceId}/commerce/products/{externalId}` Its items leave the store's WhatsApp catalog within a minute. | | | | --- | --- | | Operation id | `deleteProduct` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `externalId` | path | string | yes | The partner's own id for the product | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/products/{externalId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Create or update up to 500 products `POST /api/v1/workspaces/{workspaceId}/commerce/products/batch` All or nothing: one product that is not valid fails the request and stores none. Each result says whether the product was created, updated or unchanged. | | | | --- | --- | | Operation id | `upsertProducts` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.catalog` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Request body `application/json`: [BatchBody](/v1/api/schemas/#batchbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `products` | array of [ProductUpsertBody](/v1/api/schemas/#productupsertbody) or null | yes | (min 1 items, max 500 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [BatchResultBody](/v1/api/schemas/#batchresultbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/products/batch" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"products":[{"externalId":null,"imageUrls":null,"name":null,"priceCents":null}]}' ``` ## List the store's promotions `GET /api/v1/workspaces/{workspaceId}/commerce/promotions` | | | | --- | --- | | Operation id | `listPromotions` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PromotionsBody](/v1/api/schemas/#promotionsbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/promotions" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Create a flash sale, a number-list discount or a coupon `POST /api/v1/workspaces/{workspaceId}/commerce/promotions` Only the best discount applies to an order unless promotions are combinable. A flash sale reaches the store's WhatsApp catalog as the items' sale price within a minute. | | | | --- | --- | | Operation id | `createPromotion` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | ### Request body `application/json`: [PromotionFields](/v1/api/schemas/#promotionfields) | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | true when absent | | `amountOffCents` | integer (int64) | | In US cents: per unit for a flash sale, off the products in scope otherwise (min 1, max 100000000000) | | `categoryExternalIds` | array of string or null | | The categories' external ids when scope is categories (max 100 items) | | `code` | string | | The coupon code; compared without case (max length 32) | | `combinable` | boolean | | Adds to the other combinable promotions; otherwise only the best discount applies | | `endsAt` | string (date-time) | | Open-ended when absent | | `kind` | `"flash_sale"` \| `"number_list"` \| `"coupon"` | yes | flash_sale: lowers the price of the products in scope inside its window and shows in the WhatsApp catalog; number_list: a discount for the numbers of a list; coupon: applies when the customer enters its code at checkout | | `maxPerCustomer` | integer (int32) | | A coupon's or number-list discount's uses per customer number (min 1) | | `maxRedemptions` | integer (int32) | | A coupon's or number-list discount's uses across all customers (min 1) | | `minSubtotalCents` | integer (int64) | | Applies only from this cart subtotal in cents (min 0, max 100000000000) | | `name` | string | yes | (min length 1, max length 120) | | `numberListId` | string (uuid) | | Only the numbers of this list get it; required for number_list | | `percentOff` | integer (int32) | | Exactly one of percentOff and amountOffCents (min 1, max 100) | | `productExternalIds` | array of string or null | | The products' external ids when scope is products (max 500 items) | | `scope` | `"all"` \| `"products"` \| `"categories"` | | (default `"all"`) | | `startsAt` | string (date-time) | | Open from now when absent | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [PromotionBody](/v1/api/schemas/#promotionbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/promotions" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"flash_sale","name":"string"}' ``` ## Get a promotion `GET /api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}` | | | | --- | --- | | Operation id | `getPromotion` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `promotionId` | path | string | yes | Promotion id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PromotionBody](/v1/api/schemas/#promotionbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Replace a promotion `PUT /api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}` | | | | --- | --- | | Operation id | `updatePromotion` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `promotionId` | path | string | yes | Promotion id (UUID) | ### Request body `application/json`: [PromotionFields](/v1/api/schemas/#promotionfields) | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | true when absent | | `amountOffCents` | integer (int64) | | In US cents: per unit for a flash sale, off the products in scope otherwise (min 1, max 100000000000) | | `categoryExternalIds` | array of string or null | | The categories' external ids when scope is categories (max 100 items) | | `code` | string | | The coupon code; compared without case (max length 32) | | `combinable` | boolean | | Adds to the other combinable promotions; otherwise only the best discount applies | | `endsAt` | string (date-time) | | Open-ended when absent | | `kind` | `"flash_sale"` \| `"number_list"` \| `"coupon"` | yes | flash_sale: lowers the price of the products in scope inside its window and shows in the WhatsApp catalog; number_list: a discount for the numbers of a list; coupon: applies when the customer enters its code at checkout | | `maxPerCustomer` | integer (int32) | | A coupon's or number-list discount's uses per customer number (min 1) | | `maxRedemptions` | integer (int32) | | A coupon's or number-list discount's uses across all customers (min 1) | | `minSubtotalCents` | integer (int64) | | Applies only from this cart subtotal in cents (min 0, max 100000000000) | | `name` | string | yes | (min length 1, max length 120) | | `numberListId` | string (uuid) | | Only the numbers of this list get it; required for number_list | | `percentOff` | integer (int32) | | Exactly one of percentOff and amountOffCents (min 1, max 100) | | `productExternalIds` | array of string or null | | The products' external ids when scope is products (max 500 items) | | `scope` | `"all"` \| `"products"` \| `"categories"` | | (default `"all"`) | | `startsAt` | string (date-time) | | Open from now when absent | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PromotionBody](/v1/api/schemas/#promotionbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"kind":"flash_sale","name":"string"}' ``` ## Delete a promotion `DELETE /api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}` Orders that used it keep their discount. | | | | --- | --- | | Operation id | `deletePromotion` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `commerce:manage` | | Plan feature | `commerce.orders` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | The store's workspace id (UUID) | | `promotionId` | path | string | yes | Promotion id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/commerce/promotions/{promotionId}" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` --- # Store payments API > Reference for the 5 public v1 operations of the Store payments API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/payments/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## Get the store's PayPoint connection `GET /api/v1/workspaces/{workspaceId}/payments/paypoint` Whether the store's own PayPoint key is saved (never the key) and the wallets customers may pay with. | | | | --- | --- | | Operation id | `getStorePayPoint` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `payments:manage` | | Plan feature | `commerce.payments` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StorePayPointBody](/v1/api/schemas/#storepaypointbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/payments/paypoint" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Save the store's PayPoint key `PUT /api/v1/workspaces/{workspaceId}/payments/paypoint` Replaces any saved key. The key is write-only: no response, now or later, contains more than its last four characters. The workspace's owners and payment managers are told. A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `saveStorePayPointKey` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `payments:manage` | | Plan feature | `commerce.payments` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Request body `application/json`: [StorePayPointKeyBody](/v1/api/schemas/#storepaypointkeybody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `apiKey` | string | yes | The store's own PayPoint API key. It is stored encrypted and never returned. (min length 1, max length 512) | | `merchantId` | string | yes | The store's PayPoint merchant id (min length 1, max length 100, pattern `^[A-Za-z0-9_.-]+$`) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StorePayPointKeySavedBody](/v1/api/schemas/#storepaypointkeysavedbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 503 | Service Unavailable | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/payments/paypoint" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"apiKey":"string","merchantId":"string"}' ``` ## Remove the store's PayPoint key `DELETE /api/v1/workspaces/{workspaceId}/payments/paypoint` The workspace's owners and payment managers are told. A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `deleteStorePayPointKey` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `payments:manage` | | Plan feature | `commerce.payments` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/payments/paypoint" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Check the store's PayPoint key with PayPoint `POST /api/v1/workspaces/{workspaceId}/payments/paypoint/test` Asks PayPoint whether it accepts the saved key, without charging anyone. | | | | --- | --- | | Operation id | `testStorePayPointKey` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `payments:manage` | | Plan feature | `commerce.payments` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StorePayPointKeyTestBody](/v1/api/schemas/#storepaypointkeytestbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 503 | Service Unavailable | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/payments/paypoint/test" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" ``` ## Choose the wallets customers may pay the store with `PUT /api/v1/workspaces/{workspaceId}/payments/paypoint/wallets` A dashboard session needs a sign-in within the last 10 minutes; otherwise it answers 401 reauthentication_required. An SDK session is refused with 403 dashboard_sign_in_required. | | | | --- | --- | | Operation id | `setStorePayPointWallets` | | Authentication | A secret key (`sk_live_…` or `sk_test_…`), an SDK session token (`sdk_…`), or a secret partner key for one of the partner’s stores. | | Permission (key scope) | `payments:manage` | | Plan feature | `commerce.payments` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `workspaceId` | path | string | yes | Workspace id (UUID) | ### Request body `application/json`: [StorePayPointWalletsBody](/v1/api/schemas/#storepaypointwalletsbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `wallets` | array of `"evc_plus"` \| `"zaad"` \| `"sahal"` \| `"jeeb"` \| `"edahab"` \| `"premier_wallet"` | yes | The wallets customers may pay with in the WhatsApp checkout (max 6 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StorePayPointBody](/v1/api/schemas/#storepaypointbody) | | 401 | Unauthorized | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/workspaces/{workspaceId}/payments/paypoint/wallets" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"wallets":["evc_plus"]}' ``` --- # Partner API API > Reference for the 18 public v1 operations of the Partner API API, generated from the OpenAPI spec. Source: https://docs.nexafone.so/v1/api/partner/ Generated from `api/openapi.public.yaml`, the same spec the API serves. Try these operations in the [API console](/v1/api/console/); errors are [Problem Details](/v1/errors/). ## Get the partner of the key `GET /api/v1/partner` | | | | --- | --- | | Operation id | `getPartnerProfile` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.account:view` | | Plan feature | `workspace.core` | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PartnerProfileBody](/v1/api/schemas/#partnerprofilebody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## List the partner's stores `GET /api/v1/partner/stores` | | | | --- | --- | | Operation id | `listPartnerApiStores` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:view` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoresBody](/v1/api/schemas/#storesbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/stores" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Create a store, or return the one with this externalRef `POST /api/v1/partner/stores` Creates a workspace for the store on the partner's default plan and answers 201; when the partner already has a store with this externalRef it answers 200 with that store and changes nothing. | | | | --- | --- | | Operation id | `createPartnerStore` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:manage` | | Plan feature | `workspace.core` | ### Request body `application/json`: [NewStoreBody](/v1/api/schemas/#newstorebody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | string | yes | ISO 3166-1 alpha-2, such as SO (pattern `^[A-Za-z]{2}$`) | | `externalRef` | string | yes | The partner's own id for the store; creating again with the same value returns the same store (min length 1, max length 255, pattern `^[A-Za-z0-9._:-]+$`) | | `locale` | `"en"` \| `"so"` \| `"ar"` | yes | | | `name` | string | yes | (min length 1, max length 120) | | `owner` | [StoreOwnerBody](/v1/api/schemas/#storeownerbody) | yes | | | `timezone` | string | yes | IANA time zone, such as Africa/Mogadishu (min length 1, max length 64) | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [StoreBody](/v1/api/schemas/#storebody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/stores" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"country":"string","externalRef":"string","locale":"en","name":"string","owner":{"email":"user@example.com","name":"string"},"timezone":"string"}' ``` ## Get a store by its externalRef `GET /api/v1/partner/stores/{externalRef}` | | | | --- | --- | | Operation id | `getPartnerStore` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:view` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `externalRef` | path | string | yes | The partner's own id for the store | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreBody](/v1/api/schemas/#storebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/stores/{externalRef}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Rename, close or reopen a store `PATCH /api/v1/partner/stores/{externalRef}` | | | | --- | --- | | Operation id | `updatePartnerStore` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `externalRef` | path | string | yes | The partner's own id for the store | ### Request body `application/json`: [StoreChangeBody](/v1/api/schemas/#storechangebody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | | (min length 1, max length 120) | | `status` | `"active"` \| `"suspended"` | | suspended closes the store and keeps its data; active reopens a store the partner closed | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreBody](/v1/api/schemas/#storebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PATCH "https://api.nexafone.so/api/v1/partner/stores/{externalRef}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Close a store `POST /api/v1/partner/stores/{externalRef}/close` Suspends the store: it becomes read-only and keeps its data. PATCH status active reopens it. | | | | --- | --- | | Operation id | `closePartnerStore` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `externalRef` | path | string | yes | The partner's own id for the store | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [StoreBody](/v1/api/schemas/#storebody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/stores/{externalRef}/close" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Change a user's role and teams in a store `PUT /api/v1/partner/stores/{externalRef}/members/{partnerUserId}` The user must have signed in to the store through a partner session. Their open screens fetch their access again at once. A store keeps at least one owner, and a role that needs a seat takes one (no_free_seat when the store has none). | | | | --- | --- | | Operation id | `updatePartnerStoreMember` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `externalRef` | path | string | yes | The partner's own id for the store | | `partnerUserId` | path | string | yes | The partner's id for the user: the sub of its user tokens | ### Request body `application/json`: [UpdatePartnerMemberInputBody](/v1/api/schemas/#updatepartnermemberinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `role` | string | yes | owner, admin, supervisor, agent or viewer (min length 1, max length 64) | | `teams` | array of string | | The store's team ids the user is in; absent leaves the teams as they are (max 50 items) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [PartnerMemberBody](/v1/api/schemas/#partnermemberbody) | | 403 | Forbidden | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PUT "https://api.nexafone.so/api/v1/partner/stores/{externalRef}/members/{partnerUserId}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"role":"string"}' ``` ## Disable a user's membership of a store `DELETE /api/v1/partner/stores/{externalRef}/members/{partnerUserId}` The user can no longer open the store: requests to it are refused at once and its live updates stop within seconds; a session left with no store it can open ends. Leaving a store out of the user tokens does not do this, and a later user token that lists the store enables the membership again. | | | | --- | --- | | Operation id | `disablePartnerStoreMember` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.stores:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `externalRef` | path | string | yes | The partner's own id for the store | | `partnerUserId` | path | string | yes | The partner's id for the user: the sub of its user tokens | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/partner/stores/{externalRef}/members/{partnerUserId}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## List the partner's webhook endpoints `GET /api/v1/partner/webhook-endpoints` | | | | --- | --- | | Operation id | `listPartnerWebhookEndpoints` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointsOutputBody](/v1/api/schemas/#webhookendpointsoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Register a partner-wide webhook endpoint `POST /api/v1/partner/webhook-endpoints` It receives the subscribed events of every store of the partner; the envelope's partner.storeExternalRef names the store. Every delivery is signed like a store's own: Nexafon-Signature: t=,v1=." keyed with the secret>. The secret is in this response only. | | | | --- | --- | | Operation id | `createPartnerWebhookEndpoint` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Request body `application/json`: [CreatePartnerEndpointInputBody](/v1/api/schemas/#createpartnerendpointinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string | yes | Event types from GET /partner/webhook-event-types (min 1 items, max 64 items) | | `url` | string | yes | An https URL on the public internet (min length 8, max length 2048) | ### Responses | Status | Description | Body | | --- | --- | --- | | 201 | Created | `application/json`: [WebhookSecretOutputBody](/v1/api/schemas/#webhooksecretoutputbody) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"events":["string"],"url":"string"}' ``` ## Get a partner webhook endpoint `GET /api/v1/partner/webhook-endpoints/{endpointId}` | | | | --- | --- | | Operation id | `getPartnerWebhookEndpoint` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Change a partner webhook endpoint, or turn it off or back on `PATCH /api/v1/partner/webhook-endpoints/{endpointId}` | | | | --- | --- | | Operation id | `updatePartnerWebhookEndpoint` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Request body `application/json`: [UpdatePartnerEndpointInputBody](/v1/api/schemas/#updatepartnerendpointinputbody) | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string or null | | (min 1 items, max 64 items) | | `status` | `"active"` \| `"disabled"` | | active turns a disabled endpoint back on | | `url` | string | | (min length 8, max length 2048) | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X PATCH "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Delete a partner webhook endpoint and its delivery log `DELETE /api/v1/partner/webhook-endpoints/{endpointId}` | | | | --- | --- | | Operation id | `deletePartnerWebhookEndpoint` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 204 | No Content | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X DELETE "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## List every delivery attempt to a partner endpoint `GET /api/v1/partner/webhook-endpoints/{endpointId}/deliveries` | | | | --- | --- | | Operation id | `listPartnerWebhookDeliveries` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `eventId` | query | string (uuid) | | Only this event's attempts | | `status` | query | `"succeeded"` \| `"failed"` | | | | `cursor` | query | string | | The next value of the previous page | | `limit` | query | integer (int64) | | | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookDeliveriesOutputBody](/v1/api/schemas/#webhookdeliveriesoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}/deliveries" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Send an event to a partner endpoint again `POST /api/v1/partner/webhook-endpoints/{endpointId}/events/{eventId}/replay` The event keeps its id. It is retried on the usual schedule; the delivery log shows every attempt. | | | | --- | --- | | Operation id | `replayPartnerWebhookEndpointEvent` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `eventId` | path | string | yes | Event id (UUID) from the delivery log | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 409 | Conflict | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}/events/{eventId}/replay" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Replace a partner webhook endpoint's signing secret `POST /api/v1/partner/webhook-endpoints/{endpointId}/rotate-secret` Until previousSecretExpiresAt deliveries carry a v1 signature for each secret. The new secret is in this response only. | | | | --- | --- | | Operation id | `rotatePartnerWebhookEndpointSecret` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | | `keepPreviousHours` | query | integer (int64) | | How long deliveries are also signed with the replaced secret | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookSecretOutputBody](/v1/api/schemas/#webhooksecretoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}/rotate-secret" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## Send a webhook.test event to a partner endpoint `POST /api/v1/partner/webhook-endpoints/{endpointId}/test` Works on a disabled endpoint too. It is tried once; the delivery log shows the result. | | | | --- | --- | | Operation id | `sendPartnerWebhookTest` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpointId` | path | string | yes | Webhook endpoint id (UUID) | ### Responses | Status | Description | Body | | --- | --- | --- | | 202 | Accepted | `application/json`: [WebhookTestOutputBody](/v1/api/schemas/#webhooktestoutputbody) | | 404 | Not Found | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 422 | Unprocessable Entity | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | | 500 | Internal Server Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X POST "https://api.nexafone.so/api/v1/partner/webhook-endpoints/{endpointId}/test" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` ## List the events a partner endpoint can subscribe to `GET /api/v1/partner/webhook-event-types` | | | | --- | --- | | Operation id | `listPartnerWebhookEventTypes` | | Authentication | A secret partner key (`psk_live_…` or `psk_test_…`), from a server only. | | Permission (key scope) | `partner.webhooks:manage` | | Plan feature | `workspace.core` | ### Responses | Status | Description | Body | | --- | --- | --- | | 200 | OK | `application/json`: [WebhookEventTypesOutputBody](/v1/api/schemas/#webhookeventtypesoutputbody) | | default | Error | `application/problem+json`: [Problem](/v1/api/schemas/#problem) | ### Example ```bash curl -X GET "https://api.nexafone.so/api/v1/partner/webhook-event-types" \ -H "Authorization: Bearer $NEXAFON_PARTNER_SECRET_KEY" ``` --- # Schemas > The 126 request and response schemas of the public v1 API. Source: https://docs.nexafone.so/v1/api/schemas/ Every object the public operations send or return, generated from `api/openapi.public.yaml`. ### BatchBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `products` | array of [ProductUpsertBody](/v1/api/schemas/#productupsertbody) or null | yes | (min 1 items, max 500 items) | ### BatchResultBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | array of [UpsertResultBody](/v1/api/schemas/#upsertresultbody) | yes | In the order of the request | ### CallNoteBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `callId` | string (uuid) | yes | | | `missed` | boolean | yes | Nobody answered the call | | `reason` | string | | How the call ended, such as no_answer or cancelled | ### CatalogBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `errorCode` | string | | | | `errorMessage` | string | | | | `metaCatalogId` | string | | | | `status` | `"pending"` \| `"no_number"` \| `"active"` \| `"failed"` | yes | pending: not set up with Meta yet; no_number: the store has no WhatsApp number connected; failed: it could not be set up, errorCode and errorMessage say why | ### CategoriesBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `categories` | array of [CategoryBody](/v1/api/schemas/#categorybody) | yes | | ### CategoriesInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `categories` | array of [CategoryInputBody](/v1/api/schemas/#categoryinputbody) or null | yes | In display order; categories left out keep their names and follow these (min 1 items, max 500 items) | ### CategoryBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | | | `name` | string | yes | | | `position` | integer (int32) | yes | Display order, from 0 | ### CategoryInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | (min length 1, max length 100, pattern `^[A-Za-z0-9._:-]+$`) | | `name` | string | yes | (min length 1, max length 120) | ### CategoryRefBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | | | `name` | string | yes | | ### ChoiceBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | | | `id` | string | | | | `title` | string | yes | | ### ContactBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalRef` | string | | The business's own id for this contact | | `id` | string (uuid) | yes | | | `language` | `"en"` \| `"so"` \| `"ar"` | yes | Language of the automatic menu | | `name` | string | yes | | | `phone` | string | yes | E.164 | | `tags` | array of string | yes | | | `whatsappName` | string | | The name on the customer's WhatsApp profile | ### ConversationBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `assigneeMemberId` | string (uuid) | | | | `closedAt` | string (date-time) | | | | `contact` | [ContactBody](/v1/api/schemas/#contactbody) | yes | | | `createdAt` | string (date-time) | yes | | | `id` | string (uuid) | yes | | | `lastMessage` | [LastMessageBody](/v1/api/schemas/#lastmessagebody) | | | | `lastMessageAt` | string (date-time) | yes | | | `numberId` | string (uuid) | yes | The business WhatsApp number the customer wrote to | | `pinned` | boolean | yes | | | `routingDeadline` | string (date-time) | | When a chat waiting for a menu choice goes to the default team | | `routingState` | `"awaiting_menu"` \| `"routed"` | yes | | | `snoozedUntil` | string (date-time) | | | | `status` | `"open"` \| `"closed"` \| `"snoozed"` | yes | | | `teamId` | string (uuid) | | Absent while the customer has not chosen a team from the menu | | `unreadCount` | integer (int64) | yes | | | `updatedAt` | string (date-time) | yes | | | `windowExpiresAt` | string (date-time) | | Until then any message can be sent; after it only an approved template | ### ConversationListOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `conversations` | array of [ConversationBody](/v1/api/schemas/#conversationbody) | yes | | | `next` | string | | Pass as cursor for the next page; absent on the last page | ### CreateInputBody2 | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | `"marketing"` \| `"utility"` \| `"authentication"` | yes | | | `definition` | [TemplateDefinitionBody](/v1/api/schemas/#templatedefinitionbody) | yes | | | `language` | string | yes | A WhatsApp language code, such as en_US, so or ar (min length 2, max length 16) | | `name` | string | yes | Lowercase letters, digits and underscores (min length 1, max length 512, pattern `^[a-z0-9_]+$`) | ### CreatePartnerEndpointInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string | yes | Event types from GET /partner/webhook-event-types (min 1 items, max 64 items) | | `url` | string | yes | An https URL on the public internet (min length 8, max length 2048) | ### CreateSessionInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `embedOrigin` | string | | Sent by the Nexafon embed page: the origin of the page that frames it (max length 300) | | `publishableKey` | string | yes | pk_live_… or pk_test_…, or a partner's ppk_live_… or ppk_test_… (min length 1, max length 128) | | `userToken` | string | yes | A JWT (HS256) your server signs with one of the workspace's secret keys of the same mode: sub (your user id), jti (new for each token) and exp (at most 10 minutes ahead) are required; name, email, email_verified (a boolean) and role are optional. The first time a user signs in, email is required and email_verified must be true: that email links the user to the member with the same email, or to a new member. Later sign-ins use that link and ignore the email. With a partner's publishable key, the partner's server signs it with one of the partner's secret keys (psk_…) of the same mode that has the partner.stores:manage scope, and the session acts with no more than that key's store permissions: sub (the partner's user id), name, jti and exp are required, email and locale (en, so or ar) optional, and stores lists 1 to 50 of the partner's stores as {externalRef, role, teams?}: role is one of owner, admin, supervisor, agent or viewer, and teams, when present, the store's team ids the user is in. The user becomes or stays an active member of each store with that role (min length 1, max length 32768) | ### CreateWebhookEndpointInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string | yes | Event types from GET /webhook-event-types (min 1 items, max 64 items) | | `url` | string | yes | An https URL on the public internet (min length 8, max length 2048) | ### DayHoursBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `closes` | string | yes | Closing time, HH:MM, after the opening time (pattern `^(([01][0-9]\|2[0-3]):[0-5][0-9]\|24:00)$`) | | `day` | `"sat"` \| `"sun"` \| `"mon"` \| `"tue"` \| `"wed"` \| `"thu"` \| `"fri"` | yes | | | `opens` | string | yes | Opening time, HH:MM in the workspace's time zone (pattern `^([01][0-9]\|2[0-3]):[0-5][0-9]$`) | ### DeliveryAreaBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | yes | Only active areas are offered in checkout | | `feeCents` | integer (int64) | yes | The delivery fee in US cents (min 0, max 100000000000) | | `id` | string (uuid) | | Absent for a new area | | `name` | string | yes | As the customer picks it in checkout (min length 1, max length 30) | ### DeliveryBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `areas` | array of [DeliveryAreaBody](/v1/api/schemas/#deliveryareabody) | yes | In the order checkout lists them; an area left out is removed (max 100 items) | | `deliveryEnabled` | boolean | yes | | | `freeDeliveryAboveCents` | integer (int64) | | Delivery is free from this order amount (after discounts) in cents; never free when absent (min 0, max 100000000000) | | `notifyStatuses` | array of string | yes | Order statuses the customer gets a WhatsApp message for: awaiting_payment, paid, preparing, out_for_delivery, delivered, cancelled, refunded (max 7 items) | | `pickupAddress` | string | yes | Where pickup orders are collected; shown to the customer (max length 500) | | `pickupEnabled` | boolean | yes | | | `requestLocation` | boolean | yes | Ask the customer for a location pin after checkout when they chose delivery | | `saved` | boolean | yes | false while the store has not saved its delivery settings and the defaults apply | ### DeliverySettingsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `areas` | array of [DeliveryAreaBody](/v1/api/schemas/#deliveryareabody) | yes | In the order checkout lists them; an area left out is removed (max 100 items) | | `deliveryEnabled` | boolean | yes | | | `freeDeliveryAboveCents` | integer (int64) | | Delivery is free from this order amount (after discounts) in cents; never free when absent (min 0, max 100000000000) | | `notifyStatuses` | array of string | yes | Order statuses the customer gets a WhatsApp message for: awaiting_payment, paid, preparing, out_for_delivery, delivered, cancelled, refunded (max 7 items) | | `pickupAddress` | string | yes | Where pickup orders are collected; shown to the customer (max length 500) | | `pickupEnabled` | boolean | yes | | | `requestLocation` | boolean | yes | Ask the customer for a location pin after checkout when they chose delivery | ### EditInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | `"marketing"` \| `"utility"` | | Unchanged when absent | | `definition` | [TemplateDefinitionBody](/v1/api/schemas/#templatedefinitionbody) | yes | | ### FieldError | Field | Type | Required | Description | | --- | --- | --- | --- | | `location` | string | | Where the error occurred, e.g. 'body.name' or 'path.id' | | `message` | string | yes | What is wrong with the value | | `value` | any | | The value that was rejected | ### FlowButtonBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `cta` | string | yes | The button that opens the Flow | ### FlowFieldBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `value` | string | yes | Text, a number, true or false, or JSON for a list or group | ### FlowReplyBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `fields` | array of [FlowFieldBody](/v1/api/schemas/#flowfieldbody) | yes | The answers, sorted by name | | `name` | string | | The Flow's name | ### LastMessageBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `deleted` | boolean | yes | | | `direction` | `"inbound"` \| `"outbound"` \| `"internal"` | yes | | | `status` | `"received"` \| `"pending"` \| `"sent"` \| `"delivered"` \| `"read"` \| `"failed"` \| `"saved"` | yes | | | `text` | string | yes | Text or caption; empty for a deleted message | | `type` | string | yes | | ### ListMembersOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `members` | array of [MemberBody](/v1/api/schemas/#memberbody) | yes | | ### ListOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `count` | integer (int64) | yes | Recordings that count toward the plan limit; defaults are not counted | | `limit` | integer (int64) | yes | Most recordings the plan allows; 0 when unlimited is true | | `recordings` | array of [RecordingBody](/v1/api/schemas/#recordingbody) | yes | | | `unlimited` | boolean | yes | The plan allows any number of recordings | ### ListOutputBody3 | Field | Type | Required | Description | | --- | --- | --- | --- | | `next` | string | | Pass as cursor for the next page; absent on the last page | | `syncError` | string | | Why the last sync failed | | `syncedAt` | string (date-time) | | When the templates were last read in full from Meta | | `templates` | array of [TemplateBody](/v1/api/schemas/#templatebody) | yes | By name, then language | ### ListTeamsOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `teams` | array of [TeamDetailBody](/v1/api/schemas/#teamdetailbody) | yes | | ### MediaBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `caption` | string | | | | `fileId` | string (uuid) | | Set once the file is stored; open it with the message's media link | | `filename` | string | | | | `mimeType` | string | | | | `status` | `"pending"` \| `"ready"` \| `"failed"` | yes | pending while an inbound file is being copied from WhatsApp | | `voice` | boolean | | A voice note recorded in WhatsApp | ### MediaSendBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `fileId` | string (uuid) | yes | A file uploaded through the files API | | `filename` | string | | Shown to the customer for documents (max length 240) | ### MemberBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdAt` | string (date-time) | yes | | | `email` | string (email) | yes | | | `id` | string (uuid) | yes | | | `joinedAt` | string (date-time) | | | | `lastActiveAt` | string (date-time) | | When one of the member's sessions was last refreshed, to the day; absent without a session | | `name` | string | yes | The name on the member's account; empty when they have not set one | | `role` | string | yes | | | `status` | `"invited"` \| `"active"` \| `"disabled"` | yes | | | `teams` | array of [TeamBody](/v1/api/schemas/#teambody) | yes | The teams the member belongs to; they scope what the member sees | | `userId` | string | yes | | ### MenuBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `body` | string | yes | | | `headerImage` | string | | An https link to the image shown above the buttons | | `listButton` | string | | Set when the options show as a list instead of buttons | | `options` | array of [MenuOptionBody](/v1/api/schemas/#menuoptionbody) | yes | | ### MenuOptionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `title` | string | yes | | ### MessageBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `call` | [CallNoteBody](/v1/api/schemas/#callnotebody) | | A WhatsApp call of the customer's | | `choice` | [ChoiceBody](/v1/api/schemas/#choicebody) | | The customer's tap on a button or list row | | `clientMsgId` | string | | | | `conversationId` | string (uuid) | yes | | | `createdAt` | string (date-time) | yes | | | `deleted` | boolean | yes | | | `deliveredAt` | string (date-time) | | | | `direction` | `"inbound"` \| `"outbound"` \| `"internal"` | yes | internal: a note only the team sees | | `errorCode` | string | | | | `errorMessage` | string | | Why the message failed, written for the agent | | `failedAt` | string (date-time) | | | | `flow` | [FlowButtonBody](/v1/api/schemas/#flowbuttonbody) | | A business message whose button opens a WhatsApp Flow | | `flowReply` | [FlowReplyBody](/v1/api/schemas/#flowreplybody) | | The customer's answers to a WhatsApp Flow | | `forwarded` | boolean | | | | `id` | string (uuid) | yes | | | `location` | [MessageLocationBody](/v1/api/schemas/#messagelocationbody) | | | | `locationRequest` | boolean | | A business message asking the customer to share their location | | `media` | [MediaBody](/v1/api/schemas/#mediabody) | | | | `menu` | [MenuBody](/v1/api/schemas/#menubody) | | Buttons or list sent by the business | | `order` | [OrderBody](/v1/api/schemas/#orderbody) | | A cart the customer sent from the catalog | | `products` | [MessageProductsBody](/v1/api/schemas/#messageproductsbody) | | Items of the store's catalog the shop sent | | `reactions` | array of [ReactionBody](/v1/api/schemas/#reactionbody) | yes | | | `readAt` | string (date-time) | | | | `replyToMessageId` | string (uuid) | | | | `sentAt` | string (date-time) | | | | `sentByMemberId` | string (uuid) | | Absent for the customer's messages and automatic ones | | `status` | `"received"` \| `"pending"` \| `"sent"` \| `"delivered"` \| `"read"` \| `"failed"` \| `"saved"` | yes | pending until WhatsApp accepts the message; statuses never go back | | `template` | [TemplateRefBody](/v1/api/schemas/#templaterefbody) | | | | `text` | string | yes | Text, caption or note; empty for a deleted message | | `type` | `"text"` \| `"image"` \| `"video"` \| `"audio"` \| `"document"` \| `"sticker"` \| `"location"` \| `"template"` \| `"reaction"` \| `"interactive"` \| `"contacts"` \| `"unsupported"` \| `"note"` \| `"call"` \| `"order"` | yes | | ### MessageLocationBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | | | | `latitude` | number (double) | yes | | | `longitude` | number (double) | yes | | | `name` | string | | | ### MessageProductSectionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `retailerIds` | array of string | yes | The catalog items' retailer ids | | `title` | string | | | ### MessageProductsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `header` | string | | | | `kind` | `"product"` \| `"product_list"` \| `"carousel"` \| `"catalog_message"` | yes | product: one item; product_list: sections of items; carousel: a card per item; catalog_message: the whole catalog, with the item shown as its thumbnail | | `sections` | array of [MessageProductSectionBody](/v1/api/schemas/#messageproductsectionbody) | yes | | ### NewStoreBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `country` | string | yes | ISO 3166-1 alpha-2, such as SO (pattern `^[A-Za-z]{2}$`) | | `externalRef` | string | yes | The partner's own id for the store; creating again with the same value returns the same store (min length 1, max length 255, pattern `^[A-Za-z0-9._:-]+$`) | | `locale` | `"en"` \| `"so"` \| `"ar"` | yes | | | `name` | string | yes | (min length 1, max length 120) | | `owner` | [StoreOwnerBody](/v1/api/schemas/#storeownerbody) | yes | | | `timezone` | string | yes | IANA time zone, such as Africa/Mogadishu (min length 1, max length 64) | ### NumberListBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdAt` | string (date-time) | yes | | | `id` | string (uuid) | yes | | | `kind` | `"uploaded"` \| `"past_orders"` | yes | | | `minOrders` | integer (int32) | | | | `name` | string | yes | | | `phones` | array of string or null | | Only when one list is read | | `size` | integer (int32) | yes | Numbers in an uploaded list | | `updatedAt` | string (date-time) | yes | | | `usedBy` | integer (int64) | yes | Promotions that use the list | ### NumberListFields | Field | Type | Required | Description | | --- | --- | --- | --- | | `kind` | `"uploaded"` \| `"past_orders"` | yes | uploaded: the numbers given; past_orders: every customer with at least minOrders paid orders | | `minOrders` | integer (int32) | | Required for past_orders (min 1, max 1000) | | `name` | string | yes | (min length 1, max length 80) | | `phones` | array of string or null | | International numbers with their country code, for uploaded (max 10000 items) | ### NumberListsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `lists` | array of [NumberListBody](/v1/api/schemas/#numberlistbody) | yes | By name | ### OrderBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `catalogId` | string | | | | `items` | array of [OrderItemBody](/v1/api/schemas/#orderitembody) | yes | | | `text` | string | | The customer's note with the cart | ### OrderCancelBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | | Why, such as out of stock (max length 500) | ### OrderItemBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `currency` | string | | ISO 4217 code | | `itemPrice` | string | | Decimal price per item as WhatsApp reported it; the order logic prices from the catalog | | `productRetailerId` | string | yes | The catalog item's retailer id | | `quantity` | integer (int64) | yes | | ### OrderStatusBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `note` | string | | Kept in the order's history; for cancelled, the reason (max length 500) | | `status` | `"preparing"` \| `"out_for_delivery"` \| `"delivered"` \| `"cancelled"` | yes | Payment and checkout set the other statuses | ### PartnerAppearanceBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `colorMode` | `"light"` \| `"dark"` \| `"auto"` | yes | | | `font` | `"ibm_plex_sans"` \| `"inter"` \| `"system"` | yes | | | `primaryColor` | string | yes | Brand colour, such as #0f766e (pattern `^#[0-9a-fA-F]{6}$`) | | `radius` | integer (int32) | yes | Corner radius in pixels (min 0, max 24) | ### PartnerMemberBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalRef` | string | yes | The partner's own id for the store | | `memberId` | string (uuid) | yes | | | `partnerUserId` | string | yes | The partner's id for the user: the sub of its user tokens | | `role` | string | yes | | | `status` | `"active"` \| `"disabled"` | yes | | | `teamIds` | array of string (uuid) | yes | | | `workspaceId` | string (uuid) | yes | | ### PartnerPlanBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `key` | string | yes | | | `name` | string | yes | Empty when the plan no longer exists | ### PartnerPlanFeatureBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `enabled` | boolean | yes | | | `key` | string | yes | | | `limit` | integer (int64) | | Limit features: the most allowed; absent means no limit | ### PartnerProfileBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `defaultPlan` | [PartnerPlanBody](/v1/api/schemas/#partnerplanbody) | yes | The plan every new store starts on | | `features` | array of [PartnerPlanFeatureBody](/v1/api/schemas/#partnerplanfeaturebody) | yes | The features of the default plan's published version | | `id` | string (uuid) | yes | | | `name` | string | yes | | | `slug` | string | yes | | | `status` | `"active"` \| `"suspended"` | yes | | ### PreviewInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `components` | array of [TemplateComponentBody](/v1/api/schemas/#templatecomponentbody) | | The parameters, exactly as sending the template takes them (max 12 items) | ### PreviewOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `body` | string | yes | | | `buttons` | array of [TemplatePreviewButtonBody](/v1/api/schemas/#templatepreviewbuttonbody) | yes | | | `footer` | string | | | | `header` | [TemplatePreviewHeaderBody](/v1/api/schemas/#templatepreviewheaderbody) | | | | `templateId` | string | yes | | | `text` | string | yes | Header, body and footer as plain text | ### Problem | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | A stable, machine-readable error code | | `detail` | string | | A human-readable explanation of this occurrence | | `errors` | array of [FieldError](/v1/api/schemas/#fielderror) or null | | The individual problems, such as one per invalid field | | `requestId` | string | | The request id to quote when reporting the problem | | `status` | integer (int64) | yes | The HTTP status code | | `title` | string | yes | The HTTP status text | | `type` | string (uri) | yes | A link to the documentation of this error code, or about:blank | ### ProductBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | yes | | | `availability` | `"in_stock"` \| `"out_of_stock"` | yes | | | `category` | [CategoryRefBody](/v1/api/schemas/#categoryrefbody) | | | | `currency` | `"USD"` | yes | | | `description` | string | yes | | | `externalId` | string | yes | | | `imageUrls` | array of string | yes | | | `name` | string | yes | | | `priceCents` | integer (int64) | yes | | | `saleEndsAt` | string (date-time) | | | | `salePriceCents` | integer (int64) | | | | `saleStartsAt` | string (date-time) | | | | `stock` | integer (int32) | | | | `sync` | [SyncBody](/v1/api/schemas/#syncbody) | yes | The product's worst item: a rejected size shows on the product | | `updatedAt` | string (date-time) | yes | | | `variants` | array of [VariantViewBody](/v1/api/schemas/#variantviewbody) | yes | | ### ProductFields | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | false keeps the product but takes it out of the WhatsApp catalog; true when absent | | `availability` | `"in_stock"` \| `"out_of_stock"` | | (default `"in_stock"`) | | `categoryExternalId` | string | | A category upserted before; none when absent (max length 100, pattern `^[A-Za-z0-9._:-]*$`) | | `currency` | `"USD"` | | (default `"USD"`) | | `description` | string | | (max length 5000) | | `imageUrls` | array of string or null | yes | HTTPS URLs on the public internet; Meta fetches them, Nexafon does not host them. The first is the main image (min 1 items, max 10 items) | | `name` | string | yes | (min length 1, max length 200) | | `priceCents` | integer (int64) | yes | In US cents (min 0, max 100000000000) | | `saleEndsAt` | string (date-time) | | Open-ended when absent | | `salePriceCents` | integer (int64) | | Below priceCents; shown while the sale window is open (min 0, max 100000000000) | | `saleStartsAt` | string (date-time) | | Open from now when absent | | `stock` | integer (int32) | | Absent when the partner does not count stock; 0 shows the product out of stock (min 0) | | `variants` | array of [VariantBody](/v1/api/schemas/#variantbody) or null | | Sizes or colours, each its own catalog item grouped under the product; the product itself is the one item when absent (max 50 items) | ### ProductUpsertBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | false keeps the product but takes it out of the WhatsApp catalog; true when absent | | `availability` | `"in_stock"` \| `"out_of_stock"` | | (default `"in_stock"`) | | `categoryExternalId` | string | | A category upserted before; none when absent (max length 100, pattern `^[A-Za-z0-9._:-]*$`) | | `currency` | `"USD"` | | (default `"USD"`) | | `description` | string | | (max length 5000) | | `externalId` | string | yes | The partner's own id for the product (min length 1, max length 100, pattern `^[A-Za-z0-9._:-]+$`) | | `imageUrls` | array of string or null | yes | HTTPS URLs on the public internet; Meta fetches them, Nexafon does not host them. The first is the main image (min 1 items, max 10 items) | | `name` | string | yes | (min length 1, max length 200) | | `priceCents` | integer (int64) | yes | In US cents (min 0, max 100000000000) | | `saleEndsAt` | string (date-time) | | Open-ended when absent | | `salePriceCents` | integer (int64) | | Below priceCents; shown while the sale window is open (min 0, max 100000000000) | | `saleStartsAt` | string (date-time) | | Open from now when absent | | `stock` | integer (int32) | | Absent when the partner does not count stock; 0 shows the product out of stock (min 0) | | `variants` | array of [VariantBody](/v1/api/schemas/#variantbody) or null | | Sizes or colours, each its own catalog item grouped under the product; the product itself is the one item when absent (max 50 items) | ### ProductsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `catalog` | [CatalogBody](/v1/api/schemas/#catalogbody) | yes | | | `next` | string | | Pass as cursor for the next page; absent on the last page | | `products` | array of [ProductBody](/v1/api/schemas/#productbody) | yes | By name | ### PromotionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | yes | | | `amountOffCents` | integer (int64) | | In US cents: per unit for a flash sale, off the products in scope otherwise (min 1, max 100000000000) | | `categoryExternalIds` | array of string | yes | | | `code` | string | | The coupon code; compared without case (max length 32) | | `combinable` | boolean | | Adds to the other combinable promotions; otherwise only the best discount applies | | `createdAt` | string (date-time) | yes | | | `endsAt` | string (date-time) | | Open-ended when absent | | `id` | string (uuid) | yes | | | `kind` | `"flash_sale"` \| `"number_list"` \| `"coupon"` | yes | flash_sale: lowers the price of the products in scope inside its window and shows in the WhatsApp catalog; number_list: a discount for the numbers of a list; coupon: applies when the customer enters its code at checkout | | `maxPerCustomer` | integer (int32) | | A coupon's or number-list discount's uses per customer number (min 1) | | `maxRedemptions` | integer (int32) | | A coupon's or number-list discount's uses across all customers (min 1) | | `minSubtotalCents` | integer (int64) | | Applies only from this cart subtotal in cents (min 0, max 100000000000) | | `name` | string | yes | (min length 1, max length 120) | | `numberListId` | string (uuid) | | Only the numbers of this list get it; required for number_list | | `open` | boolean | yes | Active and inside its window now | | `percentOff` | integer (int32) | | Exactly one of percentOff and amountOffCents (min 1, max 100) | | `productExternalIds` | array of string | yes | | | `redemptions` | integer (int64) | yes | Orders that used the coupon | | `scope` | `"all"` \| `"products"` \| `"categories"` | yes | | | `startsAt` | string (date-time) | | Open from now when absent | | `updatedAt` | string (date-time) | yes | | ### PromotionFields | Field | Type | Required | Description | | --- | --- | --- | --- | | `active` | boolean | | true when absent | | `amountOffCents` | integer (int64) | | In US cents: per unit for a flash sale, off the products in scope otherwise (min 1, max 100000000000) | | `categoryExternalIds` | array of string or null | | The categories' external ids when scope is categories (max 100 items) | | `code` | string | | The coupon code; compared without case (max length 32) | | `combinable` | boolean | | Adds to the other combinable promotions; otherwise only the best discount applies | | `endsAt` | string (date-time) | | Open-ended when absent | | `kind` | `"flash_sale"` \| `"number_list"` \| `"coupon"` | yes | flash_sale: lowers the price of the products in scope inside its window and shows in the WhatsApp catalog; number_list: a discount for the numbers of a list; coupon: applies when the customer enters its code at checkout | | `maxPerCustomer` | integer (int32) | | A coupon's or number-list discount's uses per customer number (min 1) | | `maxRedemptions` | integer (int32) | | A coupon's or number-list discount's uses across all customers (min 1) | | `minSubtotalCents` | integer (int64) | | Applies only from this cart subtotal in cents (min 0, max 100000000000) | | `name` | string | yes | (min length 1, max length 120) | | `numberListId` | string (uuid) | | Only the numbers of this list get it; required for number_list | | `percentOff` | integer (int32) | | Exactly one of percentOff and amountOffCents (min 1, max 100) | | `productExternalIds` | array of string or null | | The products' external ids when scope is products (max 500 items) | | `scope` | `"all"` \| `"products"` \| `"categories"` | | (default `"all"`) | | `startsAt` | string (date-time) | | Open from now when absent | ### PromotionsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `promotions` | array of [PromotionBody](/v1/api/schemas/#promotionbody) | yes | Newest first | ### ReactionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `by` | `"contact"` \| `"member"` | yes | | | `emoji` | string | yes | | | `memberId` | string (uuid) | | | ### RecordingBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdAt` | string (date-time) | yes | | | `createdBy` | string | yes | | | `durationMs` | integer (int64) | | Length of the playable audio, measured by the server | | `failureCode` | `"too_short"` \| `"too_long"` \| `"unreadable"` \| `"processing_error"` | | | | `failureReason` | string | | Why the last upload failed, in English | | `id` | string (uuid) | yes | | | `isDefault` | boolean | yes | The workspace's fallback for its kind and language; it can be replaced but not deleted | | `kind` | `"greeting"` \| `"option"` \| `"fallback_closed"` \| `"fallback_prayer"` \| `"fallback_missed"` \| `"hold"` \| `"custom"` | yes | | | `language` | `"en"` \| `"so"` \| `"ar"` | | Absent when it plays for callers of any language | | `name` | string | yes | | | `playable` | boolean | yes | Calls can play it. A replacement that is processing or failed leaves the previous audio playing. | | `size` | integer (int64) | | Size in bytes of the playable audio as uploaded | | `source` | `"recorded"` \| `"uploaded"` \| `"default"` | yes | | | `status` | `"processing"` \| `"ready"` \| `"failed"` | yes | State of the last upload | | `updatedAt` | string (date-time) | yes | | | `uses` | array of [UseBody](/v1/api/schemas/#usebody) | yes | Where it plays; a recording in use can't be deleted | ### SDKSessionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `appearance` | [PartnerAppearanceBody](/v1/api/schemas/#partnerappearancebody) | | An exchange with a partner's key: the partner's default appearance for its stores' embeds | | `expiresAt` | string (date-time) | yes | Refresh the session before this | | `memberId` | string (uuid) | | A workspace's session: the member it acts as | | `mode` | `"live"` \| `"test"` | yes | | | `name` | string | | An exchange only: the user's name from the user token | | `refreshUntil` | string (date-time) | yes | Refreshes keep the session going until this; then exchange a new user token | | `sessionId` | string (uuid) | yes | | | `stores` | array of [SDKSessionStoreBody](/v1/api/schemas/#sdksessionstorebody) | | An exchange with a partner's key: the stores the session opens, in the user token's order; GET /api/v1/me/workspaces lists them at any time | | `token` | string | yes | The session token. Keep it in memory (web) or in the Keychain or Keystore (mobile), never in localStorage, and send it as Authorization: Bearer | | `userId` | string | yes | | | `workspaceId` | string (uuid) | | A workspace's session: its workspace | ### SDKSessionStoreBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalRef` | string | yes | The partner's own id for the store | | `memberId` | string (uuid) | yes | | | `name` | string | yes | | | `role` | string | yes | The user's role key in the store, such as agent | | `workspaceId` | string (uuid) | yes | | ### SendBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientMsgId` | string | yes | Your id for this message: sending it again returns the message already queued instead of sending twice (min length 1, max length 64, pattern `^[A-Za-z0-9._:-]+$`) | | `media` | [MediaSendBody](/v1/api/schemas/#mediasendbody) | | | | `replyToMessageId` | string (uuid) | | Quote this message | | `template` | [TemplateSendBody](/v1/api/schemas/#templatesendbody) | | The only kind of message allowed outside the 24-hour window | | `text` | string | | The message, or the caption of an image, video or document (max length 4096) | | `type` | `"text"` \| `"image"` \| `"video"` \| `"audio"` \| `"document"` \| `"sticker"` \| `"template"` | yes | | ### StartInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `message` | [SendBody](/v1/api/schemas/#sendbody) | yes | An approved template | | `phone` | string | yes | The customer's number with its country code (min length 3, max length 32) | | `teamId` | string (uuid) | | The team the conversation belongs to; the routing's default team when it is one of yours | ### StartOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `conversation` | [ConversationBody](/v1/api/schemas/#conversationbody) | yes | | | `message` | [MessageBody](/v1/api/schemas/#messagebody) | yes | | ### StoreBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `closedAt` | string (date-time) | | Set while the partner has closed the store | | `country` | string | yes | ISO 3166-1 alpha-2 | | `createdAt` | string (date-time) | yes | | | `externalRef` | string | yes | The partner's own id for the store | | `locale` | `"en"` \| `"so"` \| `"ar"` | yes | | | `name` | string | yes | | | `owner` | [StoreOwnerBody](/v1/api/schemas/#storeownerbody) | yes | | | `status` | `"trialing"` \| `"active"` \| `"past_due"` \| `"suspended"` \| `"cancelled"` | yes | | | `timezone` | string | yes | | | `workspaceId` | string (uuid) | yes | Use it in /api/v1/workspaces/{workspaceId}/… to act in the store | ### StoreChangeBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | | (min length 1, max length 120) | | `status` | `"active"` \| `"suspended"` | | suspended closes the store and keeps its data; active reopens a store the partner closed | ### StoreOrderBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | | | | `cancelReason` | string | | | | `checkedOutAt` | string (date-time) | | | | `contactPhone` | string | | | | `conversationId` | string (uuid) | | | | `couponCode` | string | | | | `createdAt` | string (date-time) | yes | | | `currency` | `"USD"` | yes | | | `customerName` | string | | | | `customerPhone` | string | yes | | | `deliveryArea` | string | | | | `deliveryFeeCents` | integer (int64) | yes | | | `discountCents` | integer (int64) | yes | | | `discountReason` | string | | The promotions that make up the discount | | `events` | array of [StoreOrderEventBody](/v1/api/schemas/#storeordereventbody) or null | | The order's history, oldest first; on a single order | | `fulfilment` | `"delivery"` \| `"pickup"` | | Chosen in checkout | | `id` | string (uuid) | yes | | | `itemCount` | integer (int64) | yes | | | `items` | array of [StoreOrderItemBody](/v1/api/schemas/#storeorderitembody) or null | | On a single order | | `location` | [StoreOrderLocationBody](/v1/api/schemas/#storeorderlocationbody) | | The pin the customer shared after checkout | | `nextStatuses` | array of string | yes | The statuses the store may set now | | `note` | string | | Cart items that were left out, and why | | `number` | integer (int64) | yes | The store's own order number | | `status` | `"new"` \| `"awaiting_payment"` \| `"paid"` \| `"preparing"` \| `"out_for_delivery"` \| `"delivered"` \| `"cancelled"` \| `"refunded"` | yes | | | `subtotalCents` | integer (int64) | yes | | | `teamId` | string (uuid) | | | | `totalCents` | integer (int64) | yes | subtotalCents - discountCents + deliveryFeeCents, computed by Nexafon | | `updatedAt` | string (date-time) | yes | | ### StoreOrderEventBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `actorId` | string | | | | `actorType` | `"member"` \| `"partner_key"` \| `"api_key"` \| `"customer"` \| `"system"` | yes | | | `at` | string (date-time) | yes | | | `fromStatus` | string | | | | `kind` | `"created"` \| `"summary_sent"` \| `"status_changed"` \| `"checkout"` \| `"location"` \| `"customer_notified"` \| `"customer_not_notified"` \| `"payment_failed"` | yes | | | `note` | string | | | | `toStatus` | string | | | ### StoreOrderItemBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `color` | string | | | | `name` | string | yes | | | `productExternalId` | string | yes | | | `quantity` | integer (int32) | yes | | | `retailerId` | string | yes | The WhatsApp retailer_id the customer ordered | | `size` | string | | | | `unitPriceCents` | integer (int64) | yes | The store's price when the order was priced, in US cents | ### StoreOrderLocationBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `latitude` | number (double) | yes | | | `longitude` | number (double) | yes | | ### StoreOrdersBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `next` | string | | Pass as cursor for the next page; absent on the last page | | `orders` | array of [StoreOrderBody](/v1/api/schemas/#storeorderbody) | yes | Newest first | ### StoreOwnerBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | (max length 320) | | `name` | string | yes | (min length 1, max length 120) | ### StorePayPointBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `available` | boolean | yes | PayPoint is configured on this server; without it no store takes payments | | `last4` | string | yes | The saved key's last four characters, all of it that is ever shown | | `merchantId` | string | yes | | | `options` | array of [StoreWalletBody](/v1/api/schemas/#storewalletbody) | yes | Every wallet a store can take, in the order customers see them | | `saved` | boolean | yes | The store's own PayPoint key is saved | | `updatedAt` | string (date-time) | | When the key was last saved | | `wallets` | array of `"evc_plus"` \| `"zaad"` \| `"sahal"` \| `"jeeb"` \| `"edahab"` \| `"premier_wallet"` | yes | The wallets customers may pay with, in the order they see them | ### StorePayPointKeyBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `apiKey` | string | yes | The store's own PayPoint API key. It is stored encrypted and never returned. (min length 1, max length 512) | | `merchantId` | string | yes | The store's PayPoint merchant id (min length 1, max length 100, pattern `^[A-Za-z0-9_.-]+$`) | ### StorePayPointKeySavedBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `last4` | string | yes | The key's last four characters, all of it that is ever shown | | `saved` | boolean | yes | | ### StorePayPointKeyTestBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `ok` | boolean | yes | PayPoint accepted the key; nobody was charged | | `reason` | `"refused"` \| `"unreachable"` | | refused: PayPoint does not accept the key; unreachable: PayPoint did not answer, try again | ### StorePayPointWalletsBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `wallets` | array of `"evc_plus"` \| `"zaad"` \| `"sahal"` \| `"jeeb"` \| `"edahab"` \| `"premier_wallet"` | yes | The wallets customers may pay with in the WhatsApp checkout (max 6 items) | ### StoreWalletBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | `"evc_plus"` \| `"zaad"` \| `"sahal"` \| `"jeeb"` \| `"edahab"` \| `"premier_wallet"` | yes | | | `name` | string | yes | The wallet's brand name, the same in every language | ### StoresBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `next` | string | | Pass as cursor for the next page; absent on the last page | | `stores` | array of [StoreBody](/v1/api/schemas/#storebody) | yes | Newest first | ### SyncBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `lastSyncedAt` | string (date-time) | | When the item was last pushed to Meta | | `reason` | string | | Meta's rejection reason, or why the push failed | | `status` | `"queued"` \| `"in_review"` \| `"approved"` \| `"rejected"` \| `"failed"` \| `"inactive"` | yes | queued: waiting for the next push, within a minute; in_review: Meta is reviewing; failed: Meta refused the push; inactive: not in the catalog | ### SyncOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `syncError` | string | | | | `syncedAt` | string (date-time) | | When the templates were last read in full from Meta | ### TeamBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `role` | `"member"` \| `"lead"` | yes | | ### TeamDetailBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `color` | `"chart-1"` \| `"chart-2"` \| `"chart-3"` \| `"chart-4"` \| `"chart-5"` | yes | Theme token shown next to the team's conversations | | `createdAt` | string (date-time) | yes | | | `id` | string (uuid) | yes | | | `isDefault` | boolean | yes | Routing sends everything it does not route elsewhere to this team | | `members` | array of [TeamMemberBody](/v1/api/schemas/#teammemberbody) | yes | Members in ring order (min 1 items, max 200 items) | | `name` | string | yes | (min length 1, max length 80) | | `queue` | [TeamQueueBody](/v1/api/schemas/#teamqueuebody) | yes | | | `updatedAt` | string (date-time) | yes | | | `workingHours` | array of [DayHoursBody](/v1/api/schemas/#dayhoursbody) or null | yes | Open days; a day that is not listed is closed. null means always open. (max 7 items) | ### TeamMemberBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `lead` | boolean | | Team leads see this team's reports and can pick up its queued calls | | `memberId` | string (uuid) | yes | | ### TeamQueueBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `announcePositionSeconds` | integer (int64) | | How often a waiting caller hears their place in the queue, shorter than the longest wait; no announcements when unset (min 30, max 300) | | `closedRecordingId` | string (uuid) | | Recording played outside working hours; the workspace's default closed message when unset | | `holdRecordingId` | string (uuid) | | Recording played while the caller waits; silence when unset | | `maxWaitSeconds` | integer (int64) | yes | Longest wait in the queue (min 60, max 600) | | `missedRecordingId` | string (uuid) | | Recording played when nobody answers within the longest wait; the workspace's default missed-call message when unset | | `ringTimeoutSeconds` | integer (int64) | yes | After this the caller waits in the queue (min 30, max 60) | | `secondsPerPerson` | integer (int64) | yes | How long each member rings with the priority strategy (min 5, max 60) | | `strategy` | `"all"` \| `"priority"` | yes | all rings every member at once; priority rings one member at a time in the members' order | ### TemplateBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | `"marketing"` \| `"utility"` \| `"authentication"` | yes | | | `createdAt` | string (date-time) | yes | | | `definition` | [TemplateDefinitionBody](/v1/api/schemas/#templatedefinitionbody) | yes | | | `editPending` | boolean | yes | An edit is on its way to Meta | | `errorCode` | string | | Why the last submission, edit or deletion failed | | `errorMessage` | string | | | | `id` | string | yes | | | `language` | string | yes | | | `name` | string | yes | | | `parameterFormat` | `"positional"` \| `"named"` | yes | | | `quality` | string | | Meta's quality rating, such as GREEN | | `rejectedReason` | string | | Meta's reason, for rejected templates | | `status` | `"submitting"` \| `"submit_failed"` \| `"pending"` \| `"approved"` \| `"rejected"` \| `"paused"` \| `"disabled"` \| `"in_appeal"` \| `"pending_deletion"` \| `"limit_exceeded"` \| `"archived"` \| `"deleting"` | yes | | | `syncedAt` | string (date-time) | | | | `updatedAt` | string (date-time) | yes | | | `usage` | [TemplateUsageBody](/v1/api/schemas/#templateusagebody) | | | ### TemplateButtonBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `example` | string | | url: an example end of the address; copy_code: an example code (max length 200) | | `phoneNumber` | string | | For phone_number buttons, with its country code (max length 32) | | `text` | string | | (max length 25) | | `type` | `"quick_reply"` \| `"url"` \| `"phone_number"` \| `"copy_code"` \| `"otp"` \| `"voice_call_request"` | yes | voice_call_request asks the customer for permission to call them (Meta's call permission request); it is always the last button | | `url` | string | | For url buttons; may end with {{1}}, filled in when sending (max length 2000) | ### TemplateComponentBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `index` | integer (int64) | | For buttons: the button's position (min 0, max 9) | | `parameters` | array of [TemplateParameterBody](/v1/api/schemas/#templateparameterbody) or null | | (max 20 items) | | `subType` | `"quick_reply"` \| `"url"` \| `"copy_code"` | | For buttons: url also fills a one-time password button | | `type` | `"header"` \| `"body"` \| `"button"` | yes | | ### TemplateCurrencyBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount1000` | integer (int64) | yes | The amount times 1000: 12500 is 12.50 | | `code` | string | yes | ISO 4217 currency code, such as USD (max length 3) | | `fallbackValue` | string | yes | Shown where WhatsApp cannot format the amount, such as $12.50 (max length 64) | ### TemplateDateTimeBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `fallbackValue` | string | yes | The date or time as the customer reads it (max length 64) | ### TemplateDefinitionBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `body` | [TemplateTextBody](/v1/api/schemas/#templatetextbody) | yes | | | `buttons` | array of [TemplateButtonBody](/v1/api/schemas/#templatebuttonbody) | | (max 11 items) | | `footer` | [TemplateFooterBody](/v1/api/schemas/#templatefooterbody) | | | | `header` | [TemplateHeaderBody](/v1/api/schemas/#templateheaderbody) | | | | `unsupported` | array of string | | Parts Nexafon shows but cannot edit or send, such as carousels | ### TemplateExampleBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | | The variable, for named variables; numbered ones take their examples in order (max length 64) | | `value` | string | yes | (max length 200) | ### TemplateFooterBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `codeExpirationMinutes` | integer (int64) | | Authentication templates only (min 0, max 90) | | `text` | string | | (max length 60) | ### TemplateHeaderBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `examples` | array of [TemplateExampleBody](/v1/api/schemas/#templateexamplebody) | | (max 1 items) | | `format` | `"text"` \| `"image"` \| `"video"` \| `"document"` \| `"location"` | yes | | | `sampleFileId` | string (uuid) | | For image, video and document headers: an uploaded file (files API, kind media) Meta reviews the header with | | `text` | string | | For text headers; at most one variable (max length 60) | ### TemplateLocationBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | | (max length 200) | | `latitude` | number (double) | yes | (min -90, max 90) | | `longitude` | number (double) | yes | (min -180, max 180) | | `name` | string | | (max length 100) | ### TemplateMediaBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `filename` | string | | For documents (max length 240) | | `link` | string (uri) | yes | A public https link WhatsApp downloads the file from (max length 2048) | ### TemplateParameterBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `couponCode` | string | | (max length 15) | | `currency` | [TemplateCurrencyBody](/v1/api/schemas/#templatecurrencybody) | | | | `dateTime` | [TemplateDateTimeBody](/v1/api/schemas/#templatedatetimebody) | | | | `document` | [TemplateMediaBody](/v1/api/schemas/#templatemediabody) | | | | `image` | [TemplateMediaBody](/v1/api/schemas/#templatemediabody) | | | | `location` | [TemplateLocationBody](/v1/api/schemas/#templatelocationbody) | | | | `parameterName` | string | | For templates with named variables (max length 64) | | `payload` | string | | (max length 1024) | | `text` | string | | (max length 1024) | | `type` | `"text"` \| `"currency"` \| `"date_time"` \| `"image"` \| `"video"` \| `"document"` \| `"location"` \| `"payload"` \| `"coupon_code"` | yes | text, currency and date_time fill a variable; image, video, document and location fill a media or location header; payload is a quick-reply button's payload; coupon_code fills a copy-code button | | `video` | [TemplateMediaBody](/v1/api/schemas/#templatemediabody) | | | ### TemplatePreviewButtonBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | | The code a copy-code or one-time password button copies | | `phoneNumber` | string | | | | `text` | string | | | | `type` | string | yes | | | `url` | string | | The full address, with the parameter filled in | ### TemplatePreviewHeaderBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | | | | `filename` | string | | | | `format` | string | yes | | | `latitude` | string | | | | `link` | string | | | | `longitude` | string | | | | `place` | string | | | | `text` | string | | | ### TemplateRefBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `category` | string | | | | `language` | string | yes | | | `name` | string | yes | | ### TemplateSendBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `components` | array of [TemplateComponentBody](/v1/api/schemas/#templatecomponentbody) or null | | (max 10 items) | | `language` | string | yes | Such as en_US (max length 16) | | `name` | string | yes | An approved template of the workspace (max length 512) | ### TemplateTextBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `examples` | array of [TemplateExampleBody](/v1/api/schemas/#templateexamplebody) | | (max 100 items) | | `securityRecommendation` | boolean | | Authentication templates: add Meta's do-not-share line | | `text` | string | | Variables are {{1}}, {{2}}, … or lowercase names such as {{order_id}}; empty for authentication templates (max length 1024) | ### TemplateUsageBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `delivered` | integer (int64) | yes | | | `failed` | integer (int64) | yes | | | `pending` | integer (int64) | yes | Queued and not yet accepted by WhatsApp | | `read` | integer (int64) | yes | | | `sent` | integer (int64) | yes | | | `since` | string (date-time) | yes | | ### ThreadOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `messages` | array of [MessageBody](/v1/api/schemas/#messagebody) | yes | The latest first | | `next` | string | | Pass as cursor for older messages; absent at the start of the conversation | ### TokenInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | The current session token (min length 1, max length 128) | ### UpdatePartnerEndpointInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string or null | | (min 1 items, max 64 items) | | `status` | `"active"` \| `"disabled"` | | active turns a disabled endpoint back on | | `url` | string | | (min length 8, max length 2048) | ### UpdatePartnerMemberInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `role` | string | yes | owner, admin, supervisor, agent or viewer (min length 1, max length 64) | | `teams` | array of string | | The store's team ids the user is in; absent leaves the teams as they are (max 50 items) | ### UpdateWebhookEndpointInputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | | (max length 200) | | `events` | array of string or null | | (min 1 items, max 64 items) | | `status` | `"active"` \| `"disabled"` | | active turns a disabled endpoint back on | | `url` | string | | (min length 8, max length 2048) | ### UpsertResultBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | | | `result` | `"created"` \| `"updated"` \| `"unchanged"` | yes | unchanged: the product was the same, nothing is pushed to Meta | ### UseBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `digit` | integer (int64) | | The key of the IVR option | | `label` | string | yes | Where it plays, in English | | `teamId` | string (uuid) | | The team whose settings play it; for an IVR option, the team the option routes to | | `teamName` | string | | | | `type` | `"workspace_default"` \| `"routing_greeting"` \| `"ivr_option"` \| `"team_hold"` \| `"team_closed"` \| `"team_missed"` \| `"prayer_message"` \| `"team_prayer"` | yes | | ### VariantBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `availability` | `"in_stock"` \| `"out_of_stock"` | | The product's when absent | | `color` | string | | (max length 100) | | `externalId` | string | yes | The partner's id for the variant, such as a size id; it is the WhatsApp retailer_id of the variant's catalog item (min length 1, max length 100, pattern `^[A-Za-z0-9._:-]+$`) | | `priceCents` | integer (int64) | | The variant's own price in cents; the product's when absent (min 0, max 100000000000) | | `salePriceCents` | integer (int64) | | The variant's own sale price in cents; the product's when absent (min 0, max 100000000000) | | `size` | string | | (max length 100) | | `stock` | integer (int32) | | The variant's own stock; the product's when absent. 0 shows it out of stock (min 0) | ### VariantViewBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `availability` | `"in_stock"` \| `"out_of_stock"` | | | | `color` | string | | | | `externalId` | string | yes | The WhatsApp retailer_id | | `priceCents` | integer (int64) | | | | `salePriceCents` | integer (int64) | | | | `size` | string | | | | `stock` | integer (int32) | | | | `sync` | [SyncBody](/v1/api/schemas/#syncbody) | yes | | ### WebhookDeliveriesOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `deliveries` | array of [WebhookDeliveryBody](/v1/api/schemas/#webhookdeliverybody) | yes | Every attempt, the latest first | | `next` | string | | Pass as cursor for the next page; absent on the last page | ### WebhookDeliveryBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `attempt` | integer (int64) | yes | 1 for the first attempt of this delivery | | `attemptedAt` | string (date-time) | yes | | | `error` | string | | Why the attempt failed, when the response code does not say | | `eventId` | string (uuid) | yes | The id in the delivered body; receivers dedupe on it | | `eventType` | string | yes | | | `id` | string (uuid) | yes | | | `latencyMs` | integer (int64) | yes | | | `nextRetryAt` | string (date-time) | | When the next attempt runs; absent after a success or the last attempt | | `responseCode` | integer (int64) | | Absent when no response arrived | | `status` | `"succeeded"` \| `"failed"` | yes | | | `trigger` | `"event"` \| `"replay"` \| `"test"` | yes | | ### WebhookEndpointBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `createdAt` | string (date-time) | yes | | | `createdBy` | string | yes | User id of who created the endpoint | | `description` | string | yes | | | `disabledAt` | string (date-time) | | | | `disabledReason` | `"failing"` \| `"manual"` | | failing: Nexafon turned it off after every delivery failed for 3 days | | `events` | array of string | yes | Event types the endpoint receives | | `failingSince` | string (date-time) | | Deliveries have failed since then without a success | | `id` | string (uuid) | yes | | | `lastSuccessAt` | string (date-time) | | | | `previousSecretExpiresAt` | string (date-time) | | After a rotation, deliveries are also signed with the previous secret until then | | `secretMasked` | string | yes | The signing secret's prefix and last four characters, such as whsec_…WXYZ | | `status` | `"active"` \| `"disabled"` | yes | | | `updatedAt` | string (date-time) | yes | | | `url` | string | yes | | ### WebhookEndpointsOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoints` | array of [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | yes | | ### WebhookEventTypeBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | yes | | | `emitted` | boolean | yes | False while the module that sends it is not available yet; subscribing already is allowed | | `type` | string | yes | | ### WebhookEventTypesOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `eventTypes` | array of [WebhookEventTypeBody](/v1/api/schemas/#webhookeventtypebody) | yes | | ### WebhookSecretOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint` | [WebhookEndpointBody](/v1/api/schemas/#webhookendpointbody) | yes | | | `secret` | string | | The signing secret. It is shown only in this response and cannot be retrieved again; a replay of the request for the same Idempotency-Key leaves it out. | ### WebhookTestOutputBody | Field | Type | Required | Description | | --- | --- | --- | --- | | `eventId` | string (uuid) | yes | The webhook.test event; its attempt shows in the delivery log | --- # Rate limits > The request limits per IP address, user and API key, how a 429 answers, and how to back off. Source: https://docs.nexafone.so/v1/rate-limits/ Nexafon limits requests per **IP address**, per **signed-in user** and per **API key**, with a sliding window (GCRA). Every operation belongs to a class; most are `standard`, and a few that create credentials or touch money are `sensitive` (creating keys, inviting members, creating webhook endpoints, saving a PayPoint key). The limits, generated from the backend: | Class | Counted | Rate | Burst | | --- | --- | --- | --- | | sensitive | Per IP address | 20 per minute | 20 | | sensitive | Per API key | 60 per minute | 60 | | sensitive | Per signed-in user | 20 per minute | 20 | | standard | Per IP address | 600 per minute | 600 | | standard | Per API key | 1200 per minute | 1200 | | standard | Per signed-in user | 600 per minute | 600 | The SDK session exchange counts against the publishable key once the request has proved it comes from the key’s allowed origin. ## When you hit a limit The API answers `429 Too Many Requests` with the code [`rate_limited`](/v1/errors/rate_limited/) and a `Retry-After` header in seconds: ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 Content-Type: application/problem+json {"type": "https://docs.nexafone.so/errors/rate_limited", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "Too many requests. Try again later."} ``` Wait at least `Retry-After` seconds, then retry; with several workers, add jitter. Send writes with an [`Idempotency-Key`](/v1/api/#idempotency) so retries are safe. ## WhatsApp’s own limits WhatsApp limits how many messages and calls a number may send; Nexafon passes those on as `whatsapp.*` codes, such as `whatsapp.throughput_limited` or `whatsapp.business_call_limit`. They are listed in the [error catalog](/v1/errors/). --- # Errors > Every API error is RFC 9457 Problem Details with a stable code; this is the catalog of codes, generated from the backend. Source: https://docs.nexafone.so/v1/errors/ Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details, sent as `application/problem+json`: ```json { "type": "https://docs.nexafone.so/errors/validation_failed", "title": "Unprocessable Entity", "status": 422, "code": "validation_failed", "detail": "The request is invalid.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B", "errors": [{ "location": "body.name", "message": "expected length >= 1", "value": "" }] } ``` | Field | | | --- | --- | | `code` | A stable, machine-readable code: **branch on this** | | `type` | A link to the code’s page in these docs | | `status`, `title` | The HTTP status and its text | | `detail` | A human-readable explanation of this occurrence; it may change, so never parse it | | `requestId` | Quote it to support; also in the `X-Request-Id` header | | `errors` | For validation errors, one entry per problem with `location`, `message` and the rejected `value` | A `5xx` never contains internal details. Retry `429`, `502`, `503` and `504` with backoff (a `429` says how long in `Retry-After`); fix the request for other `4xx` codes. ## Catalog Every code the API can answer with, read from the backend’s source when the docs are built: ### General | Code | Status | Meaning | | --- | --- | --- | | [`bad_request`](/v1/errors/bad_request/) | 400 | The request is malformed, such as invalid JSON or a bad Idempotency-Key header. | | [`client_closed_request`](/v1/errors/client_closed_request/) | 499 | The request was cancelled. | | [`conflict`](/v1/errors/conflict/) | 409 | The request conflicts with the current state, such as a duplicate name. | | [`dashboard_sign_in_required`](/v1/errors/dashboard_sign_in_required/) | 403 | Sign in to the dashboard to make this change. | | [`forbidden`](/v1/errors/forbidden/) | 403 | The credential is valid but may not do this: a secret key’s scopes or a member’s role lack the operation’s permission, or an IP allow-list or origin check refused the request. | | [`idempotency_in_flight`](/v1/errors/idempotency_in_flight/) | 409 | A request with the same Idempotency-Key is still running. | | [`idempotency_key_reused`](/v1/errors/idempotency_key_reused/) | 422 | The Idempotency-Key was already used for a different request body or operation. | | [`internal`](/v1/errors/internal/) | 500 | Something failed on Nexafon’s side. | | [`method_not_allowed`](/v1/errors/method_not_allowed/) | 405 | This endpoint does not support the request method. | | [`not_acceptable`](/v1/errors/not_acceptable/) | 406 | | | [`not_found`](/v1/errors/not_found/) | 404 | Nothing with that id exists in this workspace. | | [`payload_too_large`](/v1/errors/payload_too_large/) | 413 | The request body is too large. | | [`precondition_failed`](/v1/errors/precondition_failed/) | 412 | | | [`rate_limited`](/v1/errors/rate_limited/) | 429 | Too many requests for this IP address, user or API key. | | [`reauthentication_required`](/v1/errors/reauthentication_required/) | 401 | Sign in again to continue. | | [`test_mode_live`](/v1/errors/test_mode_live/) | 403 | Test keys and test SDK sessions cannot call Meta for a live WhatsApp number (messages, WhatsApp calls, templates, number and catalog changes) or move money (payments, subscriptions, wallet settings). | | [`timeout`](/v1/errors/timeout/) | 504 | The operation ran out of time. | | [`unauthenticated`](/v1/errors/unauthenticated/) | 401 | The Authorization header is missing, or its key or token is unknown, revoked or of the wrong kind. | | [`unavailable`](/v1/errors/unavailable/) | 503 | A dependency is briefly unavailable. | | [`unsupported_media_type`](/v1/errors/unsupported_media_type/) | 415 | | | [`validation_failed`](/v1/errors/validation_failed/) | 422 | The request does not match the operation’s schema or rules. | ### WhatsApp (from Meta) | Code | Status | Meaning | | --- | --- | --- | | [`whatsapp.account_restricted`](/v1/errors/whatsapp.account_restricted/) | 422 | The WhatsApp Business Account is locked or failed verification. | | [`whatsapp.auth_failed`](/v1/errors/whatsapp.auth_failed/) | 502 | WhatsApp no longer accepts this connection's access token. Reconnect WhatsApp. | | [`whatsapp.bad_response`](/v1/errors/whatsapp.bad_response/) | 502 | WhatsApp sent a response Nexafon could not read. | | [`whatsapp.business_call_limit`](/v1/errors/whatsapp.business_call_limit/) | 429 | This number reached the daily limit of connected business-initiated calls. | | [`whatsapp.call_already_ongoing`](/v1/errors/whatsapp.call_already_ongoing/) | 409 | A call with this contact is already in progress. | | [`whatsapp.call_concurrency_limit`](/v1/errors/whatsapp.call_concurrency_limit/) | 429 | This number has too many calls in progress. Try again when one ends. | | [`whatsapp.call_connection_failed`](/v1/errors/whatsapp.call_connection_failed/) | 502, 504 | WhatsApp could not connect the call. Try again. | | [`whatsapp.call_country_unsupported`](/v1/errors/whatsapp.call_country_unsupported/) | 422 | Business-initiated WhatsApp calls are not available for this number's country. | | [`whatsapp.call_media_failed`](/v1/errors/whatsapp.call_media_failed/) | 502 | The call lost its audio: media could not be sent in time. | | [`whatsapp.call_permission_already_permanent`](/v1/errors/whatsapp.call_permission_already_permanent/) | 409 | This contact already allows calls permanently, so no request is needed. | | [`whatsapp.call_permission_lookup_limit`](/v1/errors/whatsapp.call_permission_lookup_limit/) | 429 | Too many call permission checks for this contact. Try again later. | | [`whatsapp.call_permission_missing`](/v1/errors/whatsapp.call_permission_missing/) | 422 | This contact has not allowed calls from your business. Send a call permission request first. | | [`whatsapp.call_permission_request_limit`](/v1/errors/whatsapp.call_permission_request_limit/) | 429 | You have sent this contact the most call permission requests allowed for now. Try again after the limit resets. | | [`whatsapp.call_rate_limited`](/v1/errors/whatsapp.call_rate_limited/) | 429 | This number is placing calls too quickly. Wait before calling again. | | [`whatsapp.call_receiver_uncallable`](/v1/errors/whatsapp.call_receiver_uncallable/) | 422 | This contact cannot receive WhatsApp calls right now. | | [`whatsapp.call_setup_failed`](/v1/errors/whatsapp.call_setup_failed/) | 502 | WhatsApp could not set up the call. Try again. | | [`whatsapp.call_sip_number`](/v1/errors/whatsapp.call_sip_number/) | 422 | Calls on this number are handled over SIP, not the Calling API. | | [`whatsapp.calling_cannot_be_enabled`](/v1/errors/whatsapp.calling_cannot_be_enabled/) | 422 | Calling can only be turned on for numbers with a messaging limit of at least 2,000. | | [`whatsapp.calling_disabled_low_quality`](/v1/errors/whatsapp.calling_disabled_low_quality/) | 422 | WhatsApp paused calling on this number for low quality. Try again when the restriction lifts. | | [`whatsapp.calling_not_enabled`](/v1/errors/whatsapp.calling_not_enabled/) | 422 | Calling is not turned on for this WhatsApp number. | | [`whatsapp.calling_prerequisites_missing`](/v1/errors/whatsapp.calling_prerequisites_missing/) | 422 | Calling cannot be turned on until the number is subscribed to call webhooks. | | [`whatsapp.country_restricted`](/v1/errors/whatsapp.country_restricted/) | 422 | This WhatsApp Business Account may not message contacts in this country. | | [`whatsapp.deletion_pending`](/v1/errors/whatsapp.deletion_pending/) | 409 | This number was deleted recently. Wait 5 minutes before registering it. | | [`whatsapp.deregistration_failed`](/v1/errors/whatsapp.deregistration_failed/) | 422 | A previous deregistration of this number failed. Deregister it again first. | | [`whatsapp.display_name_not_approved`](/v1/errors/whatsapp.display_name_not_approved/) | 422 | This number's display name is not approved yet, so it cannot send messages. | | [`whatsapp.ecosystem_limited`](/v1/errors/whatsapp.ecosystem_limited/) | 422 | WhatsApp did not deliver this marketing message to protect the contact's experience. | | [`whatsapp.experiment_excluded`](/v1/errors/whatsapp.experiment_excluded/) | 422 | WhatsApp held this marketing message back as part of an experiment. | | [`whatsapp.flow_blocked`](/v1/errors/whatsapp.flow_blocked/) | 422 | The WhatsApp Flow in this message is blocked. | | [`whatsapp.flow_throttled`](/v1/errors/whatsapp.flow_throttled/) | 429 | The WhatsApp Flow in this message hit its hourly limit. | | [`whatsapp.invalid_input`](/v1/errors/whatsapp.invalid_input/) | 422 | | | [`whatsapp.invalid_parameter`](/v1/errors/whatsapp.invalid_parameter/) | 422 | The request to WhatsApp is missing a required value. | | [`whatsapp.marketing_disabled`](/v1/errors/whatsapp.marketing_disabled/) | 422 | Marketing templates are turned off for this WhatsApp Business Account. | | [`whatsapp.media_download_failed`](/v1/errors/whatsapp.media_download_failed/) | 422 | WhatsApp could not download the contact's media. | | [`whatsapp.media_too_large`](/v1/errors/whatsapp.media_too_large/) | 413 | The media is larger than the … MB Nexafon accepts. | | [`whatsapp.media_upload_failed`](/v1/errors/whatsapp.media_upload_failed/) | 422 | WhatsApp could not fetch or accept the media. Check its type, size and link. | | [`whatsapp.messaging_limit`](/v1/errors/whatsapp.messaging_limit/) | 429 | This number reached its messaging limit after quality issues. | | [`whatsapp.no_call_id`](/v1/errors/whatsapp.no_call_id/) | 502 | WhatsApp did not return an ID for the new call. | | [`whatsapp.not_found`](/v1/errors/whatsapp.not_found/) | 404 | WhatsApp could not find that item. | | [`whatsapp.payment_required`](/v1/errors/whatsapp.payment_required/) | 422 | WhatsApp calls need a valid payment method on the WhatsApp Business Account. | | [`whatsapp.permission_missing`](/v1/errors/whatsapp.permission_missing/) | 502 | The WhatsApp connection is missing a permission this action needs. Reconnect WhatsApp. | | [`whatsapp.phone_not_registered`](/v1/errors/whatsapp.phone_not_registered/) | 422 | This WhatsApp number is not registered with the Cloud API. | | [`whatsapp.phone_number_deleted`](/v1/errors/whatsapp.phone_number_deleted/) | 422 | This WhatsApp number has been deleted from the WhatsApp Business Account. | | [`whatsapp.rate_limited`](/v1/errors/whatsapp.rate_limited/) | 429 | The WhatsApp Business Account reached its request limit. Try again later. | | [`whatsapp.recipient_blocked`](/v1/errors/whatsapp.recipient_blocked/) | 422 | This contact is blocked on WhatsApp by your business. | | [`whatsapp.recipient_not_allowed`](/v1/errors/whatsapp.recipient_not_allowed/) | 422 | That number is not on the test number's allowed recipient list. Add it in WhatsApp Manager first. | | [`whatsapp.recipient_opted_out`](/v1/errors/whatsapp.recipient_opted_out/) | 422 | This contact has stopped marketing messages from your business. | | [`whatsapp.recipient_throttled`](/v1/errors/whatsapp.recipient_throttled/) | 429 | Too many messages to this contact in a short time. Wait before sending more. | | [`whatsapp.recipient_undeliverable`](/v1/errors/whatsapp.recipient_undeliverable/) | 422 | That number cannot receive WhatsApp messages. Check it is correct and has an up-to-date WhatsApp installed. | | [`whatsapp.reengagement_required`](/v1/errors/whatsapp.reengagement_required/) | 422 | More than 24 hours have passed since the contact last wrote. Send an approved template instead. | | [`whatsapp.registration_throttled`](/v1/errors/whatsapp.registration_throttled/) | 429 | Too many registration attempts for this number. Try again later. | | [`whatsapp.rejected`](/v1/errors/whatsapp.rejected/) | 422, 502 | WhatsApp could not process the request. | | [`whatsapp.same_sender_recipient`](/v1/errors/whatsapp.same_sender_recipient/) | 422 | A business number cannot message itself. | | [`whatsapp.spam_limited`](/v1/errors/whatsapp.spam_limited/) | 429 | WhatsApp limited sending because too many recent messages were blocked or reported. | | [`whatsapp.template_disabled`](/v1/errors/whatsapp.template_disabled/) | 422 | WhatsApp disabled this template after repeated quality pauses. | | [`whatsapp.template_field_too_long`](/v1/errors/whatsapp.template_field_too_long/) | 422 | A template field is longer than WhatsApp allows. | | [`whatsapp.template_format_invalid`](/v1/errors/whatsapp.template_format_invalid/) | 422 | The template body is not formatted the way WhatsApp requires. | | [`whatsapp.template_limit`](/v1/errors/whatsapp.template_limit/) | 422 | This WhatsApp Business Account has reached its template limit. | | [`whatsapp.template_not_found`](/v1/errors/whatsapp.template_not_found/) | 422 | That template does not exist or is not approved in this language. | | [`whatsapp.template_param_format`](/v1/errors/whatsapp.template_param_format/) | 422 | A template variable has the wrong format. | | [`whatsapp.template_param_invalid`](/v1/errors/whatsapp.template_param_invalid/) | 422 | WhatsApp rejected one of the template's variables. | | [`whatsapp.template_param_mismatch`](/v1/errors/whatsapp.template_param_mismatch/) | 422 | The template's variables do not match what the template expects. | | [`whatsapp.template_param_placement`](/v1/errors/whatsapp.template_param_placement/) | 422 | Template variables cannot be at the very start or end of the text. | | [`whatsapp.template_paused`](/v1/errors/whatsapp.template_paused/) | 422 | WhatsApp paused this template for low quality. | | [`whatsapp.template_policy_violation`](/v1/errors/whatsapp.template_policy_violation/) | 422 | The template's content breaks WhatsApp policy. | | [`whatsapp.template_status_locked`](/v1/errors/whatsapp.template_status_locked/) | 422 | This template's status cannot be changed right now. | | [`whatsapp.template_text_too_long`](/v1/errors/whatsapp.template_text_too_long/) | 422 | The template text is too long once its variables are filled in. | | [`whatsapp.template_too_many_params`](/v1/errors/whatsapp.template_too_many_params/) | 422 | The template has too many variables for its length. | | [`whatsapp.throughput_limited`](/v1/errors/whatsapp.throughput_limited/) | 429 | WhatsApp is sending as fast as this number allows. Try again in a moment. | | [`whatsapp.timeout`](/v1/errors/whatsapp.timeout/) | 504 | WhatsApp did not answer in time. Try again. | | [`whatsapp.token_expired`](/v1/errors/whatsapp.token_expired/) | 502 | The WhatsApp access token has expired. Reconnect WhatsApp. | | [`whatsapp.too_many_pin_attempts`](/v1/errors/whatsapp.too_many_pin_attempts/) | 429 | The PIN was entered too quickly. Wait a moment and try again. | | [`whatsapp.unavailable`](/v1/errors/whatsapp.unavailable/) | 502, 503 | The WhatsApp Business Account is in maintenance. Try again later. | | [`whatsapp.unreachable`](/v1/errors/whatsapp.unreachable/) | 502 | Could not reach WhatsApp. Try again. | | [`whatsapp.unsupported_message_type`](/v1/errors/whatsapp.unsupported_message_type/) | 422 | That number cannot receive this type of message. | | [`whatsapp.verification_required`](/v1/errors/whatsapp.verification_required/) | 422 | This number must be verified before it can be registered. | | [`whatsapp.wrong_pin`](/v1/errors/whatsapp.wrong_pin/) | 422 | The two-step verification PIN is wrong. | ### Keys and SDK sessions | Code | Status | Meaning | | --- | --- | --- | | [`api_key_limit`](/v1/errors/api_key_limit/) | 409 | A workspace can have … active API keys. Revoke one you no longer use first. | | [`client_not_allowed`](/v1/errors/client_not_allowed/) | 403 | The page’s origin (or the app’s bundle id or package name) is not on the publishable key’s list. | | [`feature_not_in_plan`](/v1/errors/feature_not_in_plan/) | 403 | The workspace’s plan does not include the feature the operation declares (x-feature in the API reference). | | [`invalid_publishable_key`](/v1/errors/invalid_publishable_key/) | 401 | The publishable key is unknown or revoked, or the workspace or partner cannot use it. | | [`invalid_user_token`](/v1/errors/invalid_user_token/) | 401 | The user token is not a valid HS256 JWT signed with one of the workspace’s (or partner’s) secret keys of the same mode as the publishable key, or a required claim (sub, jti, exp) is missing. | | [`last_owner`](/v1/errors/last_owner/) | 409 | A workspace needs at least one owner. Make someone else an owner first. | | [`no_free_seat`](/v1/errors/no_free_seat/) | 403 | The store has no free seat for a new member. | | [`partner_key_limit`](/v1/errors/partner_key_limit/) | 409 | A partner can have … active keys. Revoke one it no longer uses first. | | [`partner_store_unknown`](/v1/errors/partner_store_unknown/) | 403 | The partner user token lists an externalRef that is not one of the partner’s stores. | | [`partner_suspended`](/v1/errors/partner_suspended/) | 403 | The partner is suspended, so its keys, sessions and stores’ embeds are refused until Nexafon reactivates it. | | [`sdk_email_not_verified`](/v1/errors/sdk_email_not_verified/) | 422 | The user token needs email_verified set to true the first time this user signs in: the email is how the user is matched to a member. | | [`sdk_email_required`](/v1/errors/sdk_email_required/) | 422 | The user token needs an email claim, with email_verified set to true, the first time this user signs in. | | [`sdk_member_disabled`](/v1/errors/sdk_member_disabled/) | 403 | The user’s membership was disabled. | | [`sdk_member_not_found`](/v1/errors/sdk_member_not_found/) | 403 | No member of the workspace has the token’s email, and the workspace does not add SDK users automatically. | | [`sdk_role_not_allowed`](/v1/errors/sdk_role_not_allowed/) | 403 | New SDK users can be given … or a role below it. | | [`sdk_session_expired`](/v1/errors/sdk_session_expired/) | 401 | The SDK session passed its refreshUntil. | | [`user_token_expired`](/v1/errors/user_token_expired/) | 401 | The token’s exp has passed, or lies more than 10 minutes ahead. | | [`user_token_replayed`](/v1/errors/user_token_replayed/) | 401 | Each user token works once (its jti is remembered). | ### Partners | Code | Status | Meaning | | --- | --- | --- | | [`partner_plan_unavailable`](/v1/errors/partner_plan_unavailable/) | 409 | The partner's default store plan has no published version. Ask Nexafon to publish it. | ### Commerce | Code | Status | Meaning | | --- | --- | --- | | [`coupon_code_taken`](/v1/errors/coupon_code_taken/) | 409 | Another promotion already uses this coupon code. | | [`order_status_not_allowed`](/v1/errors/order_status_not_allowed/) | 409 | Only a delivery order goes out for delivery. | | [`plan_limit_reached`](/v1/errors/plan_limit_reached/) | 403 | A limit of the workspace’s plan is reached. | | [`promotion_in_use`](/v1/errors/promotion_in_use/) | 409 | A promotion uses this number list. Change or delete that promotion first. | | [`promotion_name_taken`](/v1/errors/promotion_name_taken/) | 409 | A number list with this name already exists. | | [`retailer_id_taken`](/v1/errors/retailer_id_taken/) | 409 | A retailer id is already used by another product of the store. | | [`whatsapp_flow_decrypt_failed`](/v1/errors/whatsapp_flow_decrypt_failed/) | 421 | The request cannot be decrypted with the business key. | | [`whatsapp_flow_not_published`](/v1/errors/whatsapp_flow_not_published/) | 409 | This WhatsApp Flow is not published on the business number yet. Try again in a few minutes. | | [`whatsapp_flow_signature_invalid`](/v1/errors/whatsapp_flow_signature_invalid/) | 432 | The request signature is missing or does not match the body. | | [`whatsapp_flow_token_invalid`](/v1/errors/whatsapp_flow_token_invalid/) | 427 | | ### Billing | Code | Status | Meaning | | --- | --- | --- | | [`addon_not_offered`](/v1/errors/addon_not_offered/) | 409 | The plan you move to at the renewal does not offer this add-on. | | [`approver_is_author`](/v1/errors/approver_is_author/) | 403 | You asked for this …, so someone else has to approve or reject it. | | [`approver_is_requester`](/v1/errors/approver_is_requester/) | 403 | You asked for this correction, so someone else must decide it. | | [`billing_past_due`](/v1/errors/billing_past_due/) | 402 | Billing is past due, so calls to customers are paused until the invoice is paid. Customers can still call you. | | [`billing_unavailable`](/v1/errors/billing_unavailable/) | 503 | Billing can't check the balance right now, so paid messages and calls are paused. Try again in a moment. | | [`credit_limit_reached`](/v1/errors/credit_limit_reached/) | 402 | This workspace has used its credit limit, so calls to customers are paused. Customers can still call you. | | [`insufficient_balance`](/v1/errors/insufficient_balance/) | 402 | The prepaid wallet cannot cover the charge. | | [`invoice_pdf_pending`](/v1/errors/invoice_pdf_pending/) | 409 | The invoice PDF is still being prepared. Try again in a minute. | | [`payer_numbers_limited`](/v1/errors/payer_numbers_limited/) | 429 | This workspace already asked … new numbers to approve a payment today. Pay from a number that paid before, or try again tomorrow. | | [`rates_missing`](/v1/errors/rates_missing/) | 503 | This can't be priced because no Meta rate is loaded for it yet. Ask Nexafon support to load the rate card. | | [`seat_limit_reached`](/v1/errors/seat_limit_reached/) | 403 | Every seat of the plan is taken. | | [`use_seats_endpoint`](/v1/errors/use_seats_endpoint/) | 409 | This add-on is the extra seats of the workspace's subscription. Change the subscription's seats instead. | ### Calls, meetings and recordings | Code | Status | Meaning | | --- | --- | --- | | [`already_in_call`](/v1/errors/already_in_call/) | 409 | This person is already ringing or in the call. | | [`bad_offer`](/v1/errors/bad_offer/) | 422 | The device's SDP offer cannot be answered. | | [`call_already_answered`](/v1/errors/call_already_answered/) | 409 | Someone else answered this call. | | [`call_answered_on_other_device`](/v1/errors/call_answered_on_other_device/) | 409 | You answered this call on another device. | | [`call_back_unavailable`](/v1/errors/call_back_unavailable/) | 409 | Only a staff call that rang you and that you did not answer can be called back. | | [`call_not_ringing`](/v1/errors/call_not_ringing/) | 409 | This call is not ringing for you. | | [`call_over`](/v1/errors/call_over/) | 409 | This call has ended. | | [`call_permission_already_granted`](/v1/errors/call_permission_already_granted/) | 409 | This customer already allows your calls permanently. | | [`call_permission_missing`](/v1/errors/call_permission_missing/) | 409 | The customer has not allowed calls from the business. | | [`call_permission_request_limit`](/v1/errors/call_permission_request_limit/) | 429 | WhatsApp allows one call permission request a day and two a week per customer until a call connects. | | [`call_permission_template_missing`](/v1/errors/call_permission_template_missing/) | 422 | More than 24 hours have passed since the customer last wrote, so only an approved template with a call permission button can ask them. Create one under Templates. | | [`call_recording_consent_missing`](/v1/errors/call_recording_consent_missing/) | 409 | Pick the recording announcement in the WhatsApp call settings before recording calls. | | [`call_recording_not_in_plan`](/v1/errors/call_recording_not_in_plan/) | 403 | Your workspace's plan does not include call recording. | | [`media_unavailable`](/v1/errors/media_unavailable/) | 409, 503 | Calls cannot connect right now. Try again in a moment. | | [`meeting_cancelled`](/v1/errors/meeting_cancelled/) | 409 | This meeting was cancelled. | | [`meeting_not_open`](/v1/errors/meeting_not_open/) | 409 | This meeting has not started. | | [`meeting_over`](/v1/errors/meeting_over/) | 409 | This meeting is over. | | [`no_transfer`](/v1/errors/no_transfer/) | 409 | No warm transfer is in progress on this call. | | [`not_a_whatsapp_call`](/v1/errors/not_a_whatsapp_call/) | 409 | This is not a WhatsApp call. | | [`not_in_call`](/v1/errors/not_in_call/) | 409 | Only someone in the call can do this. | | [`queue_empty`](/v1/errors/queue_empty/) | 409 | Nobody is waiting in your queues right now. | | [`recording_in_use`](/v1/errors/recording_in_use/) | 409 | This recording is in use. Pick another recording in the places listed, then delete it. | | [`recording_not_ready`](/v1/errors/recording_not_ready/) | 409 | This recording has no audio yet. Wait until it is ready. | | [`transfer_in_progress`](/v1/errors/transfer_in_progress/) | 409 | Finish or cancel the transfer in progress first. | | [`whatsapp_call_country_unsupported`](/v1/errors/whatsapp_call_country_unsupported/) | 422 | WhatsApp does not offer calls to customers from business numbers in …. Customers can still call you. | | [`whatsapp_call_max_duration`](/v1/errors/whatsapp_call_max_duration/) | 409 | Calls to customers end after … minutes, so this call was ended. | | [`whatsapp_call_not_connected`](/v1/errors/whatsapp_call_not_connected/) | 409 | This call is not connected to the customer. | | [`whatsapp_calling_off`](/v1/errors/whatsapp_calling_off/) | 409 | Calling is not turned on for this WhatsApp number. | | [`whatsapp_calls_unavailable`](/v1/errors/whatsapp_calls_unavailable/) | 503 | WhatsApp calls are not available on this server. | | [`whatsapp_number_disconnected`](/v1/errors/whatsapp_number_disconnected/) | 409 | Connect the business WhatsApp number before calling customers. | ### Messaging and templates | Code | Status | Meaning | | --- | --- | --- | | [`client_msg_id_reused`](/v1/errors/client_msg_id_reused/) | 409 | This clientMsgId was already used for another message. Use a new id for each message. | | [`message_not_sent`](/v1/errors/message_not_sent/) | 409 | Only messages that reached WhatsApp can get a reaction. | | [`template_busy`](/v1/errors/template_busy/) | 409 | This template is still being sent to WhatsApp. Try again in a moment. | | [`template_exists`](/v1/errors/template_exists/) | 409 | A template with this name already exists in this language. Pick another name, or edit that template. | | [`template_not_approved`](/v1/errors/template_not_approved/) | 422 | | | [`template_not_editable`](/v1/errors/template_not_editable/) | 409 | | | [`test_mode_live_number`](/v1/errors/test_mode_live_number/) | 403 | Test keys and test SDK sessions never message real customers. | | [`whatsapp_byo_keys_off`](/v1/errors/whatsapp_byo_keys_off/) | 403 | Connecting with your own keys is not offered. Connect the number with Meta's signup instead. | | [`whatsapp_calling_not_changed`](/v1/errors/whatsapp_calling_not_changed/) | 502 | Meta did not change calling for this number. Check the number in WhatsApp Manager and try again. | | [`whatsapp_number_limit`](/v1/errors/whatsapp_number_limit/) | 409 | This workspace already has a WhatsApp number. Each business connects one number; routing sends customers to teams. | | [`whatsapp_number_missing`](/v1/errors/whatsapp_number_missing/) | 409 | Connect the business WhatsApp number before managing templates. | | [`whatsapp_number_not_in_account`](/v1/errors/whatsapp_number_not_in_account/) | 422 | This phone number is not in that WhatsApp Business Account. Check the IDs in Meta's WhatsApp Manager. | | [`whatsapp_number_not_included`](/v1/errors/whatsapp_number_not_included/) | 403 | Your plan does not include a WhatsApp number. | | [`whatsapp_number_not_onboarded`](/v1/errors/whatsapp_number_not_onboarded/) | 409 | Nexafon has no Meta access for this number, so it cannot change its calling. Connect the number again. | | [`whatsapp_number_taken`](/v1/errors/whatsapp_number_taken/) | 409 | This number is already connected to another workspace. | | [`whatsapp_token_not_granted`](/v1/errors/whatsapp_token_not_granted/) | 422 | | | [`whatsapp_webhooks_not_subscribed`](/v1/errors/whatsapp_webhooks_not_subscribed/) | 502 | Meta did not confirm the webhook subscription. Try again. | | [`whatsapp_window_closed`](/v1/errors/whatsapp_window_closed/) | 422 | WhatsApp only allows free-form messages within 24 hours of the customer’s last message. | ### Webhooks | Code | Status | Meaning | | --- | --- | --- | | [`webhook_endpoint_disabled`](/v1/errors/webhook_endpoint_disabled/) | 409 | The endpoint is turned off. | | [`webhook_endpoint_limit`](/v1/errors/webhook_endpoint_limit/) | 409 | A partner can have … webhook endpoints. Delete one it no longer uses first. | ### Realtime | Code | Status | Meaning | | --- | --- | --- | | [`upgrade_required`](/v1/errors/upgrade_required/) | 426 | Open this endpoint as a WebSocket, version 13. | ### Workspace | Code | Status | Meaning | | --- | --- | --- | | [`approver_is_proposer`](/v1/errors/approver_is_proposer/) | 403 | You proposed this change, so another admin must approve it. | | [`domain_taken`](/v1/errors/domain_taken/) | varies | | | [`email_not_verified`](/v1/errors/email_not_verified/) | 403 | Verify your email address before accepting an invitation. | | [`embed_origin_limit`](/v1/errors/embed_origin_limit/) | 409 | A workspace can have at most 20 embed origins. Remove one first. | | [`file_infected`](/v1/errors/file_infected/) | 403 | This file was flagged as malware and can't be opened. | | [`file_not_scanned`](/v1/errors/file_not_scanned/) | 409 | This file has not been scanned for malware. Confirm that you want to open it anyway. | | [`impersonation_approval_expired`](/v1/errors/impersonation_approval_expired/) | 409 | The approval expired … minutes after it was given. Request the session again. | | [`impersonation_not_allowed`](/v1/errors/impersonation_not_allowed/) | 403 | Exports and call credentials are not available while viewing a workspace. | | [`impersonation_read_only`](/v1/errors/impersonation_read_only/) | 403 | You are viewing this workspace read-only. Nothing can be changed. | | [`invitation_email_mismatch`](/v1/errors/invitation_email_mismatch/) | 403 | This invitation was sent to a different email address. Sign in with that address to accept it. | | [`invitation_expired`](/v1/errors/invitation_expired/) | 410 | This invitation has expired. Ask for a new one. | | [`last_team`](/v1/errors/last_team/) | 409 | You can't delete the only team. Routing needs at least one. | | [`role_escalation`](/v1/errors/role_escalation/) | 403 | Only an owner can make, change or remove an owner. | | [`routing_draft_stale`](/v1/errors/routing_draft_stale/) | 409 | Routing was published after this draft was started. Review the draft and save it again, then publish. | | [`routing_ivr_draft_only`](/v1/errors/routing_ivr_draft_only/) | 409 | The call menu changes only through the routing draft: save it to /routing/draft, then publish it with /routing/draft/publish. | | [`slug_taken`](/v1/errors/slug_taken/) | 409 | This workspace address is already taken. | | [`team_name_taken`](/v1/errors/team_name_taken/) | 409 | A team called … already exists. | | [`upload_incomplete`](/v1/errors/upload_incomplete/) | 409 | The file has not been uploaded yet. Upload it with the form, then confirm. | | [`upload_mismatch`](/v1/errors/upload_mismatch/) | 422 | The uploaded file does not match what was announced or is not the type it claims to be. Upload it again. | | [`workspace_limit_reached`](/v1/errors/workspace_limit_reached/) | 409 | You already own … workspaces, the most allowed. Contact support to add another. | | [`workspace_read_only`](/v1/errors/workspace_read_only/) | 403 | This workspace is cancelled. You can view it but not change anything. | --- # The Nexafon CLI > Set up an integration with nexafon init, check it with nexafon doctor, and give AI coding agents the Nexafon skill with nexafon skill add. Source: https://docs.nexafone.so/v1/tools/cli/ `@nexafon/cli` does the setup steps of the [quickstart](/v1/quickstart/) in your app’s own idiom and checks the result. :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: `@nexafon/cli` is not on npm yet, so `npx @nexafon/cli` fails for now. Until it is published, build it in a checkout of the Nexafon repo (after `task setup`) and run it from there; `--from` then installs the SDK packages from the same checkout: ```sh pnpm --filter @nexafon/cli build node /path/to/nexafon/cli/dist/bin.js init --dir /path/to/your/app --from /path/to/nexafon node /path/to/nexafon/cli/dist/bin.js doctor --dir /path/to/your/app node /path/to/nexafon/cli/dist/bin.js skill add --dir /path/to/your/app ``` Or pack it once and run the tarball from your app’s root: ```sh pnpm --filter @nexafon/cli pack --pack-destination /tmp # in the checkout npx --package /tmp/nexafon-cli-0.1.0.tgz nexafon doctor # in your app ``` After the release the commands are: ```sh npx @nexafon/cli init # in your app's root npx @nexafon/cli doctor npx @nexafon/cli skill add ``` The CLI has no Nexafon address built in: pass them as flags or set `NEXAFON_API_URL` (https://api.nexafone.so), `NEXAFON_APP_URL` (https://app.nexafone.so) and `NEXAFON_EMBED_CDN_URL` (https://app.nexafone.so/embed). ## nexafon init Run it in your app’s root. It never writes a key: it leaves empty placeholders and tells you where to paste them. 1. **Detects the framework** from `package.json`, `composer.json` and the files around them: React (Vite), Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, plain HTML, Laravel, PHP or WordPress (`--framework` overrides it). 2. **Installs the packages** with your package manager (npm, pnpm, yarn or bun, from the lockfile), or Composer for PHP. With `--from` it builds and packs them from a checkout, or takes a folder of packed `.tgz` files; without it, it prints the command to use after release. 3. **Writes env placeholders**: `.env.local` for Vite and Next.js, `.env` for Nuxt, SvelteKit, Angular and Laravel, with the secret key never under a public prefix. It adds the file to `.gitignore` when git would commit it, and adds the names (keys left empty) to `.env.example`, creating it when your app has none. 4. **Scaffolds the token endpoint and the component**, and wires what it can safely change: the Vite plugin list (the endpoint runs in Vite’s dev and preview servers; the config imports `./server/nexafon-token.ts` with its extension, as Vite’s native config loader needs), Nuxt’s `runtimeConfig`, Laravel’s `routes/web.php`. It never replaces an existing file without `--force`. 5. **Sets the appearance from your brand colour**: it reads your Tailwind config, CSS custom properties (Tailwind v4 `@theme`, shadcn’s `--primary`, `--brand`), Sass variables and theme files (MUI, Vuetify), converts HSL and OKLCH to hex, and passes it as `appearance.primaryColor` ([appearance](/v1/web/appearance/)). `--color #rrggbb` overrides it. 6. **Prints the next steps**, including the origin to add to the publishable key’s allowed websites and the embed domains: `--origin`, else `APP_URL` in your env file, else your framework’s dev server. It must be the exact origin you open the app on, port included. `--dry-run` prints every step the real run would take (env files, `.gitignore`, `.env.example`, files, patches and the install commands) and changes nothing. `nexafon --help` prints the options of one command. Every scaffolded token endpoint starts with a `signedInUser` that returns nobody, so it answers `401` until you connect it to your app’s own session, as [user tokens](/v1/server/user-tokens/) describes. It also starts with a `mayUseNexafon` (`nexafon_may_use()` in plain PHP) that returns false, so it answers `403` to every signed-in user until you return true for your staff: a token can make its user a member of your workspace, so never for customers who signed up. Use your app’s staff flag or role; return true for every signed-in user only when your app truly has no customer accounts (no public sign-up), and say so in a comment there. WordPress uses the `nexafon_use` capability instead. Both steps are yours: the CLI cannot know your sign-in or who your staff are. The Laravel and WordPress scaffolds already sign for the framework’s own user (`$request->user()`, `wp_get_current_user()`), so there only the staff check is left. When the user record has no role, sign with `agent`; `admin` only for your app’s own admins. Until you paste the secret key, every scaffolded endpoint answers `500` with the plain text “Nexafon secret key is not set” (no stack trace). In Laravel the CSRF check runs before the controller: a `POST` without the `X-CSRF-TOKEN` header gets `419`, and `401` is what a signed-out request with a valid CSRF token gets. The scaffolded Blade component sends the header. Script-tag integrations (Laravel, WordPress, plain HTML and PHP) need the loader’s address, `NEXAFON_EMBED_CDN_URL`: the folder that serves `v1/nexafon.js`, here https://app.nexafone.so/embed (the same address as in the [HTML guide’s](/v1/web/html/) script tag; for a local Nexafon stack, `EMBED_CDN_URL` in the checkout’s `.env`). Pass it as `--embed-cdn-url` or set it in the environment and `init` writes it in; left empty, the page asks its own host for `/v1/nexafon.js` and fails with “Nexafon is not defined”. The npm packages have it built in. Without Composer, `init` prints the PHP install also as `docker run` commands with the pinned Composer image. Installed from a checkout, `composer.json` gets a path repository at the checkout’s absolute path, which works only where that path exists until `nexafon/nexafon-php` is published. | Framework | Token endpoint | Component | | --- | --- | --- | | React (Vite) | `server/nexafon-token.ts` and a Vite plugin | `src/NexafonSupport.tsx` | | Next.js | `app/api/nexafon-token/route.ts` | `app/nexafon-support.tsx` | | Vue | `server/nexafon-token.ts` and a Vite plugin | `src/nexafon.ts`, `src/components/NexafonSupport.vue` | | Nuxt | `server/api/nexafon-token.post.ts` | `plugins/nexafon.client.ts`, `components/NexafonSupport.vue` | | Angular | `src/nexafon-token.ts` (Express) | `src/app/nexafon.ts`, `src/app/nexafon-support.ts` | | Svelte, SvelteKit | a Vite plugin, or `src/routes/api/nexafon-token/+server.ts` | `src/lib/NexafonSupport.svelte` | | HTML | `nexafon/nexafon-token.ts` (Node) | `nexafon/nexafon.html` | | Laravel | `NexafonTokenController` and `POST /nexafon/token` | ``, `config/nexafon.php` | | PHP | `nexafon-token.php` | `nexafon-snippet.html` | | WordPress | a plugin’s REST route | the `[nexafon_inbox]` shortcode | | Flag | | | --- | --- | | `--dir ` | The app’s root (default: the current folder) | | `--framework ` | `react-vite`, `next`, `vue`, `nuxt`, `angular`, `svelte`, `sveltekit`, `html`, `laravel`, `php`, `wordpress` | | `--from ` | A Nexafon checkout or a folder of packed `.tgz` files to install from | | `--no-install` | Only print the install commands | | `--color ` | The brand colour, instead of the detected one | | `--embed-cdn-url ` | The loader’s address for script-tag integrations (`NEXAFON_EMBED_CDN_URL`) | | `--origin ` | The origin you open your app on, printed for the allowed websites (`NEXAFON_APP_ORIGIN`) | | `--force` | Replace files `init` created before | | `--dry-run` | Print every step it would take, write and install nothing | ## nexafon doctor `doctor` reads the keys from your app’s env files (and `wp-config.php`, and the environment) without printing them, and checks the integration end to end. It exits with `1` when a check fails, so it can run in CI. | Check | What it catches | | --- | --- | | Keys | A missing or empty key, the **wrong key type** (a secret key in the publishable key’s variable or the reverse), test and live keys mixed, a workspace key with a partner key | | Loader script | Whether the loader address your page uses (`NEXAFON_EMBED_CDN_URL` for Laravel and WordPress, the HTML/PHP snippet’s script tag, or the address built into the installed `@nexafon/js`) is set and serves `v1/nexafon.js` | | Secrets out of the frontend | A secret key under a `VITE_`, `NEXT_PUBLIC_`, `NUXT_PUBLIC_` or `PUBLIC_` variable, an env file with secrets that git tracks, a secret key written in frontend code | | Token signing | Signs a test user token and exchanges it at `POST /api/v1/sdk/sessions`; Nexafon checks the signature and stops before creating a session. Finds a secret key of another workspace or mode, a revoked key, a plan without SDK embeds | | Allowed origins | Whether your app’s origin may start sessions with the publishable key (**origin not allowed**: `client_not_allowed`) | | Embed domains | Whether Nexafon’s embed pages list your origin in their CSP `frame-ancestors` | | Webhooks | Whether your `NEXAFON_WEBHOOK_SECRET` is the endpoint’s signing secret (**bad webhook secret**), then sends a real `webhook.test` delivery and reports whether your receiver accepted it; `--listen ` receives it in `doctor` and verifies the signature there | | Mobile push credentials | Not available yet: the [mobile SDKs](/v1/mobile/) are coming soon | | Flag | | | --- | --- | | `--api-url ` | Nexafon’s API (`NEXAFON_API_URL`) | | `--nexafon-app-url ` | Nexafon’s app, which serves the embed pages (`NEXAFON_APP_URL`) | | `--origin ` | Your app’s origin (`NEXAFON_APP_ORIGIN`; default: your framework’s dev server) | | `--workspace ` | Your workspace id, to check webhooks with a secret key (`NEXAFON_WORKSPACE_ID`) | | `--webhook-endpoint ` | The endpoint to test (default: the one whose secret you configured) | | `--listen ` | Receive the test delivery on this port and verify it | | `--json` | Machine-readable output | Webhook checks need a secret key with `developers:manage` (`partner.webhooks:manage` for a partner key). `doctor` takes Nexafon’s addresses only from its flags or your shell’s environment, never from the app’s env files, because it sends your secret key there. It refuses an address that is not `https`, except `http` to `localhost`. ## nexafon skill add Installs the [Nexafon agent skill](/v1/tools/ai-agents/) in a project: `.claude/skills/nexafon/` for Claude Code, a Nexafon section in `AGENTS.md` for Codex, Copilot and other agents, and `.cursor/rules/nexafon.mdc` for Cursor. Running it again only updates them. `--no-agents-md` and `--no-cursor` leave those files alone. --- # AI coding agents > The Nexafon agent skill teaches Claude Code, Cursor, Codex and Copilot to integrate Nexafon correctly, with references generated from the tested examples. Source: https://docs.nexafone.so/v1/tools/ai-agents/ The **Nexafon skill** gives an AI coding agent what it needs to add Nexafon to an app on the first try: when to use which package, the integration steps, the security rules, and code that comes from Nexafon’s tested examples. ## Install it In your project: :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: ```sh npx @nexafon/cli skill add ``` It writes `.claude/skills/nexafon/` (Claude Code loads it in its next session), a Nexafon section in `AGENTS.md` (read by Codex, Copilot, Jules and other agents) and `.cursor/rules/nexafon.mdc` for Cursor. Until the CLI is published, run it from a checkout, as [the CLI page](/v1/tools/cli/) shows. Claude Code can also install it as a plugin from the Nexafon repository’s marketplace entry (`.claude-plugin/marketplace.json`): `/plugin marketplace add Keyd-Solutions/nexafon`, then `/plugin install nexafon@nexafon`. Then ask the agent, for example, “add the Nexafon inbox to this app”. ## What it teaches - **When to use what**: web or mobile (the mobile SDKs are [coming soon](/v1/mobile/), so it does not invent them), a single business or a [partner](/v1/partners/) platform, and which framework guide applies: HTML, React, Next.js, Vue, Nuxt, Angular, Svelte, WordPress, Laravel/PHP. - **The steps**: keys → a server token endpoint → the component → appearance, preferring `nexafon init` to do the scaffolding and `nexafon doctor` to check it. - **The rules**: never put a secret key in frontend code or a public env variable; keep sessions in memory only; register every origin as an allowed website and an embed domain; sign tokens only for the app’s own signed-in user, answer `401` without one and `403` to signed-in users who are not staff (never customers; everyone only in an app with no customer accounts at all), and send the user’s real `emailVerified` and a `role` (`agent` unless the user is one of the app’s own admins). - **Your brand**: it reads the colour from your Tailwind config, CSS variables or theme files and passes it as `appearance.primaryColor`. - **References** per framework and server language (Node, PHP, Python, Go), webhooks, events, errors and theming. ## Never out of date Every code block in the skill names the example or docs snippet it comes from, such as `examples/react/src/App.tsx`; CI fails when a block differs from its file (`task skill:check`). The examples themselves run end to end against Nexafon in CI, so the agent copies code that works with the current SDK. The skill is also checked by evals: an agent that has only the skill and the CLI integrates a fresh React (Vite) app and a fresh Laravel app, and a browser test confirms the inbox loads through the app’s own token endpoint. For questions an agent should look up rather than know, give it the [MCP server](/v1/ai/mcp/): it searches these docs and the example apps while the agent works, and with a test key sends a test message and shows your webhook deliveries and recent errors. Agents without MCP can read [`/llms.txt`](/llms.txt) or any page’s `.md` version. --- # MCP server > Give Claude Code and other AI agents Nexafon's docs and examples, and safe test actions with a test key, through the Nexafon MCP server. Source: https://docs.nexafone.so/v1/ai/mcp/ The Nexafon MCP server (`@nexafon/mcp`) lets an AI coding agent read these docs and the example apps while it writes your integration, and, with a **test** secret key, send a test message and look at your webhook deliveries and recent errors. It speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio, so it works in Claude Code and any other MCP client that starts local servers. It pairs with the [Nexafon agent skill](/v1/tools/ai-agents/), which teaches the agent the integration steps and rules, and the [CLI](/v1/tools/cli/), which scaffolds the integration and checks it with `nexafon doctor`. ## Tools | Tool | Needs | What it does | | --- | --- | --- | | `search_docs(query, limit?)` | | Searches these docs; each result has the page title, its URL and a snippet | | `get_page(path)` | | Returns a page as Markdown, the same text as its `.md` twin (`/v1/quickstart/` or `/v1/quickstart.md`) | | `get_example(name, file?)` | | Lists the files of an example app (`react`, `next`, `partner`, …) with its README, or returns one file | | `send_test_message(to, text)` | test key | Sends a text to a customer through [`sendMessage`](/v1/api/inbox/) in test mode | | `list_webhook_deliveries(limit?, endpointId?, status?)` | test key | The latest delivery attempts across your [webhook endpoints](/v1/webhooks/), the newest first | | `recent_errors(limit?)` | test key | Failed webhook deliveries, failing or turned-off endpoints, and outbound messages WhatsApp did not accept | The docs and the examples are bundled into the package when it is built, so the first three tools work offline and match the version you installed. Links in their answers point at `NEXAFON_DOCS_URL`. - **There is no separate test-send operation.** `send_test_message` calls the documented send-message operation with your test key, so [test mode](/v1/concepts/#test-mode) applies: a real WhatsApp number answers [`test_mode_live`](/v1/errors/test_mode_live/) unless the workspace has a WhatsApp test number. The customer must have written within the last 24 hours; a new conversation starts with a template. - **`recent_errors`** reports what the public API exposes. The API keeps no log of failed requests: those come back to your own code as [Problem Details](/v1/errors/). - `get_example` serves only the files committed under `examples/`, and never a `.env` file (the `.env.example` templates are included). ## Settings The server reads its settings from its environment: | Variable | | | --- | --- | | `NEXAFON_DOCS_URL` | Required. The docs site, `https://docs.nexafone.so`, for the links in results | | `NEXAFON_API_URL` | The API, `https://api.nexafone.so`; needed by the test-key tools. https, or http only to localhost, 127.0.0.1 or a `*.localhost` name, since the key is sent there. Against a local Nexafon stack use its `API_URL` unchanged: the server sends `*.localhost` names to 127.0.0.1 itself (macOS does not resolve them for Node), with the Host header the stack routes by | | `NEXAFON_SECRET_KEY` | A **test** secret key (`sk_test_…`) with the `inbox:reply` and `developers:manage` scopes; needed by the test-key tools | | `NEXAFON_WORKSPACE_ID` | The key's workspace id: the `{workspaceId}` in API paths, also sent as `workspaceId` in every webhook delivery | | `NEXAFON_APP_URL`, `NEXAFON_EMBED_CDN_URL` | Optional: fill in the dashboard and SDK loader URLs in the pages the server returns | :::caution[Test keys only] The server refuses a live key (`sk_live_…`) and any other kind of key, so an agent never acts on real customers with it. The key never appears in a tool result, an error or the server's log. Keep it out of files you commit: let the MCP config read it from your environment, as below. ::: ## Claude Code :::note[Not published yet] The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon repository is public and tagged, so installing them by name fails for now. Until then, [install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here are the ones to use after release. ::: `@nexafon/mcp` is not on npm yet, so `npx -y @nexafon/mcp@0.1.0` fails for now. Until it is published, build it in a checkout of the repo (`pnpm install`, then `pnpm --filter @nexafon/mcp build`) and start it with `node`. Add the server to a project, so everyone working on it gets the tools, with the key and workspace id read from each developer's own environment: ```sh claude mcp add nexafon --scope project \ --env NEXAFON_DOCS_URL=https://docs.nexafone.so \ --env NEXAFON_API_URL=https://api.nexafone.so \ --env 'NEXAFON_WORKSPACE_ID=${NEXAFON_WORKSPACE_ID:-}' \ --env 'NEXAFON_SECRET_KEY=${NEXAFON_SECRET_KEY:-}' \ -- node /path/to/nexafon/mcp/dist/bin.js ``` That writes `.mcp.json` at the project root. Claude Code fills in `${…}` from the environment it starts in; a variable that is not set becomes empty (`:-`), which leaves the test-key tools off and the docs tools working: ```json { "mcpServers": { "nexafon": { "command": "node", "args": ["/path/to/nexafon/mcp/dist/bin.js"], "env": { "NEXAFON_DOCS_URL": "https://docs.nexafone.so", "NEXAFON_API_URL": "https://api.nexafone.so", "NEXAFON_WORKSPACE_ID": "${NEXAFON_WORKSPACE_ID:-}", "NEXAFON_SECRET_KEY": "${NEXAFON_SECRET_KEY:-}" } } } } ``` After the release, replace `node /path/to/nexafon/mcp/dist/bin.js` with `npx -y @nexafon/mcp@0.1.0` (in the JSON, `"command": "npx"` and `"args": ["-y", "@nexafon/mcp@0.1.0"]`). Run `/mcp` in Claude Code to see the server and its tools. ## Other MCP clients Any client that starts stdio servers runs it the same way: the command `node /path/to/nexafon/mcp/dist/bin.js` (after the release, `npx -y @nexafon/mcp@0.1.0`) with the variables above in its environment. Cursor reads the same `mcpServers` object from `.cursor/mcp.json`, and Claude Desktop from its `claude_desktop_config.json`. Where a client cannot read a variable from your environment, put the test key in its user-level config, never in a file you commit. --- # Security > Keeping keys safe, scopes and IP allow-lists, allowed origins and embed domains, the Content-Security-Policy your pages need, and how sessions stay out of your page. Source: https://docs.nexafone.so/v1/security/ ## Keys - **Secret keys** (`sk_…`, `psk_…`) stay on your servers: in environment variables or a secret store, never in a browser, an app, a repository or a log. Anyone holding one acts as your integration. - **Publishable keys** (`pk_…`, `ppk_…`) may be public: they only start sessions, and only from their allowed origins (or app ids) together with a user token signed by your server. - Keys are shown once. Nexafon stores only a hash, so a lost key is replaced, not recovered. - **Rotate** by creating the new key, deploying it, then revoking the old one; a revoked key answers `401` at once. Use test keys (`sk_test_…`) everywhere except production. ### Secret key scopes A secret key acts with its **scopes**: the permissions you give it when you create it, never more than your own. Give each key only what its job needs. For keys that sign [user tokens](/v1/server/user-tokens/), the scopes are also the most an embedded user may do, so include what your users need in the embed (for example `inbox:reply` and `calls:answer`). The permissions: | Permission | Lets the key | Needs plan feature | | --- | --- | --- | | `audit:view` | See the workspace audit log | | | `billing:manage` | Pay, top up, change seats | | | `billing:view` | See plan, wallet and invoices | | | `call_groups:manage` | Set how a team rings: strategy, order and timings | | | `call_reports:view` | See who answered and missed calls, per call and per staff member | | | `call_routing:manage` | Call menu drafts, schedules and holidays | | | `calls:answer` | Take WhatsApp and team calls | | | `calls:pickup_queue` | Pick any caller from a queue | | | `channels:manage` | Numbers, templates, IVR, hours | | | `commerce:manage` | Add, change and remove the store's products | | | `commerce:view` | See the store's products | | | `developers:manage` | API keys, webhooks, embeds | | | `files:view` | Open stored files; each kind of file also needs its own permission | | | `group_calls:start` | Call several staff members or a team at once | | | `inbox:assign_others` | Assign conversations to teammates | | | `inbox:reply` | Send messages and templates | | | `internal_calls:start` | Call one staff member inside Nexafon | | | `meetings:create` | Schedule voice meetings and run the ones you host | | | `meetings:manage` | Change, cancel and end any meeting and see its attendance | | | `numbers:manage` | Connect the business WhatsApp number | | | `onboarding:manage` | Run the setup wizard and see the setup checklist | | | `orders:manage` | Accept, update and cancel orders | | | `orders:view` | See the store's orders | | | `payments:manage` | Take and refund customer payments | | | `queue:pickup` | Pick up a waiting caller or take the next one from a queue | | | `recordings:download` | Download recordings | | | `recordings:listen` | Play call recordings | | | `recordings:manage` | Record, upload, replace and delete call audio | | | `recordings:view` | Hear the call audio library | | | `reports:detail` | See every report tab, agents and team calls, scoped to the teams you lead | | | `reports:schedule` | Schedule reports by email | | | `reports:view` | See team and call reports | | | `team:manage` | Invite, change roles, remove | | | `team:view` | See members, their roles and invitations | | | `templates:manage` | Create, edit, delete and sync WhatsApp templates | | | `templates:send` | Send approved WhatsApp templates | | ### IP allow-lists A secret key can be limited to your servers’ IP addresses or ranges; calls from anywhere else are refused. ## Origins and embed domains Two lists protect the embed, and a page must be on both: | List | Where | Protects | | --- | --- | --- | | A publishable key’s **allowed origins** | Developers → API keys | Which sites may start sessions with the key (`client_not_allowed`) | | The workspace’s **embed domains** | Developers → Embed domains | Which sites may frame Nexafon’s screens: Nexafon sends them as the CSP `frame-ancestors` of its embed pages, so browsers refuse to show the frames elsewhere | Enter origins as scheme and host (and port), such as `https://portal.example.com`, without a path. A workspace has at most 20 embed domains. For partners, the partner’s **domains** serve as both lists for every store. ## Your page’s Content-Security-Policy If your site sends a Content-Security-Policy, allow the loader script and Nexafon’s frames: ```http Content-Security-Policy: script-src 'self' https://app.nexafone.so/embed; frame-src https://app.nexafone.so ``` The SDK needs nothing else from your page: the frames talk to Nexafon themselves, and `getUserToken` calls your own server. With `popOutCalls`, pop-ups from your site must be allowed for calls to move to the pop-out window. ## Sessions stay out of your page - The user token is short-lived (at most 10 minutes) and works once; fetch a new one for every `getUserToken` call. Answer `401` from your token endpoint when nobody is signed in, `403` to signed-in users who are not your staff (a token can make its user a member of your workspace, so never sign for customers who signed up), and send `Cache-Control: no-store`. - The SDK session lives only in the memory of Nexafon’s frames: nothing is written to your site’s `localStorage`, `sessionStorage` or cookies, and your scripts never see the session token. - Every message between your page and the frames checks the sender’s origin and the protocol version. - Signing out of your product should end Nexafon too: call `Nexafon.destroy()`. ## Webhooks Verify every delivery’s [signature](/v1/webhooks/signatures/) against the raw body, reject old timestamps, and dedupe on the event `id`. Keep each endpoint’s secret like a secret key, and rotate it with the overlap window. ## Data Every request runs inside the workspace it belongs to; ids from other workspaces answer `404`. Tenant secrets such as WhatsApp and PayPoint keys are encrypted at rest and never returned by the API. --- # Go-live checklist > Everything to check before your Nexafon integration goes to production. Source: https://docs.nexafone.so/v1/go-live/ ## Keys and configuration - [ ] Create **live** keys (`pk_live_…`, `sk_live_…`) and keep the test keys for staging. - [ ] The secret key lives in your production secret store, not in code, images or logs. - [ ] The secret key’s scopes are no wider than the job ([security](/v1/security/#secret-key-scopes)); add an IP allow-list for your servers. - [ ] The publishable key’s allowed origins and the workspace’s embed domains list exactly your production origins, with `https`. ## Token endpoint - [ ] It checks your own session and answers `401` when nobody is signed in. - [ ] It answers `403` to signed-in users who are not your staff: customers who signed up never get a token. - [ ] It returns a **new** token on every call, with `Cache-Control: no-store`. - [ ] Each production user maps to the right member (email verified the first time); nobody signs in as the owner. - [ ] Your server clock is synchronised (NTP): tokens may live 10 minutes at most. ## Pages - [ ] Your Content-Security-Policy allows `https://app.nexafone.so/embed` scripts and `https://app.nexafone.so` frames. - [ ] `Nexafon.init` runs once per page; `Nexafon.destroy()` runs on sign-out. - [ ] Your appearance (colour, mode, font) looks right in light and dark mode. - [ ] Multi-page sites use `popOutCalls: true`, and you tested a call across a navigation. - [ ] You handle `session.error` (show your own message, log the `code`). ## Webhooks - [ ] Endpoints are `https` and answer `2xx` within 10 seconds, queuing slow work. - [ ] Signatures are verified on the raw body; deliveries are deduped on `id`. - [ ] Someone receives the “webhook turned off” email, and you know how to replay events. ## WhatsApp and calls - [ ] The WhatsApp number is connected and approved; calling is turned on if you use calls. - [ ] Templates you send outside the 24-hour window are approved. - [ ] The wallet has credit, and someone gets the low-balance notice (or auto top-up is on). ## Commerce (if you sell in WhatsApp) - [ ] Products sync from your platform, and rejected items are handled (`catalog.product_status`). - [ ] The store’s PayPoint key is saved and tested, and its wallets are chosen. - [ ] Delivery areas, fees and Shop opening hours are set; order webhooks reach your platform. ## Before you call it done - [ ] Errors are handled by `code`, not `detail`, and `429` waits for `Retry-After`. - [ ] You ran the [quickstart](/v1/quickstart/) flow end to end with live keys on a staging copy of your site. --- # Changelog > Changes to the Nexafon v1 API, web SDK, server helpers and webhooks. Source: https://docs.nexafone.so/v1/changelog/ Within v1 changes are additive; anything deprecated is announced here and in `Deprecation`/`Sunset` headers at least 180 days before it goes away ([versioning](/v1/api/#versioning-and-deprecation)). ## v1: first public release **API** - Public REST API v1: inbox (conversations and messages), templates, teams, members, recordings, webhook endpoints, commerce (products, categories, orders, delivery, promotions, number lists) and store payments. - SDK sessions: `POST /api/v1/sdk/sessions`, with refresh and revoke. - Partner API: stores, staff members and partner-wide webhook endpoints. - Problem Details errors with stable codes, idempotency keys, rate limits. **Web SDK** (`@nexafon/js`, served from `https://app.nexafone.so/embed/v1/nexafon.js`) - ``, ``, ``, ``; `init`, `mount`, `openChat`, `call`, `startInternalCall`, `setAppearance`, events. - Pop-out call windows for multi-page sites; the store switcher for partners. - Wrappers: `@nexafon/react` (React and Next.js), `@nexafon/vue` (Vue and Nuxt), `@nexafon/angular`. **Server helpers** - `@nexafon/node`, `nexafon/nexafon-php`, `nexafon` (Python) and the Go module: user tokens, partner user tokens, webhook verification and a Partner API client. **Webhooks** - Signed events for messages, conversations, contacts, calls, meetings, templates, billing, invoices, catalog reviews and orders, retried for 72 hours. **Coming next:** the iOS, Android, Flutter and React Native SDKs with mobile push. --- # addon_not_offered > The addon_not_offered Problem Details code: The plan you move to at the renewal does not offer this add-on. Source: https://docs.nexafone.so/v1/errors/addon_not_offered/ | | | | --- | --- | | HTTP status | `409` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/addon_not_offered` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The plan you move to at the renewal does not offer this add-on. - Your plan does not offer this add-on. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/addon_not_offered", "title": "Conflict", "status": 409, "code": "addon_not_offered", "detail": "The plan you move to at the renewal does not offer this add-on.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # already_in_call > The already_in_call Problem Details code: This person is already ringing or in the call. Source: https://docs.nexafone.so/v1/errors/already_in_call/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/already_in_call` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This person is already ringing or in the call. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/already_in_call", "title": "Conflict", "status": 409, "code": "already_in_call", "detail": "This person is already ringing or in the call.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # api_key_limit > The api_key_limit Problem Details code: A workspace can have … active API keys. Revoke one you no longer use first. Source: https://docs.nexafone.so/v1/errors/api_key_limit/ | | | | --- | --- | | HTTP status | `409` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/api_key_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A workspace can have … active API keys. Revoke one you no longer use first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/api_key_limit", "title": "Conflict", "status": 409, "code": "api_key_limit", "detail": "A workspace can have … active API keys. Revoke one you no longer use first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # approver_is_author > The approver_is_author Problem Details code: You asked for this …, so someone else has to approve or reject it. Source: https://docs.nexafone.so/v1/errors/approver_is_author/ | | | | --- | --- | | HTTP status | `403` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/approver_is_author` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You asked for this …, so someone else has to approve or reject it. - You drafted, edited or submitted this …. Someone else has to approve it. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/approver_is_author", "title": "Forbidden", "status": 403, "code": "approver_is_author", "detail": "You asked for this …, so someone else has to approve or reject it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # approver_is_proposer > The approver_is_proposer Problem Details code: You proposed this change, so another admin must approve it. Source: https://docs.nexafone.so/v1/errors/approver_is_proposer/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/approver_is_proposer` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You proposed this change, so another admin must approve it. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/approver_is_proposer", "title": "Forbidden", "status": 403, "code": "approver_is_proposer", "detail": "You proposed this change, so another admin must approve it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # approver_is_requester > The approver_is_requester Problem Details code: You asked for this correction, so someone else must decide it. Source: https://docs.nexafone.so/v1/errors/approver_is_requester/ | | | | --- | --- | | HTTP status | `403` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/approver_is_requester` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You asked for this correction, so someone else must decide it. - You proposed these terms, so someone else must approve them. - You recorded this bank transfer, so someone else must decide it. - You requested this adjustment, so someone else must decide it. - You requested this billing mode change, so someone else must decide it. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/approver_is_requester", "title": "Forbidden", "status": 403, "code": "approver_is_requester", "detail": "You asked for this correction, so someone else must decide it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # bad_offer > The bad_offer Problem Details code: The device's SDP offer cannot be answered. Source: https://docs.nexafone.so/v1/errors/bad_offer/ | | | | --- | --- | | HTTP status | `422` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/bad_offer` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The device's SDP offer cannot be answered. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/bad_offer", "title": "Unprocessable Entity", "status": 422, "code": "bad_offer", "detail": "The device's SDP offer cannot be answered.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # bad_request > The bad_request Problem Details code: The request is malformed, such as invalid JSON or a bad Idempotency-Key header. Source: https://docs.nexafone.so/v1/errors/bad_request/ | | | | --- | --- | | HTTP status | `400` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/bad_request` | The request is malformed, such as invalid JSON or a bad `Idempotency-Key` header. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The Idempotency-Key header must be 1 to 255 visible ASCII characters. - The callback could not be read. - The request body could not be read. ## Handling it Fix the request before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/bad_request", "title": "Bad Request", "status": 400, "code": "bad_request", "detail": "The Idempotency-Key header must be 1 to 255 visible ASCII characters.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # billing_past_due > The billing_past_due Problem Details code: Billing is past due, so calls to customers are paused until the invoice is paid. Customers can still call you. Source: https://docs.nexafone.so/v1/errors/billing_past_due/ | | | | --- | --- | | HTTP status | `402` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/billing_past_due` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Billing is past due, so calls to customers are paused until the invoice is paid. Customers can still call you. - Billing is past due, so paid templates are paused until the invoice is paid. Free replies inside the customer's 24-hour window still send. - Billing is past due, so the call was ended. Calls to customers resume once the invoice is paid. ## Handling it A payment is needed: top up the wallet or settle the invoice, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/billing_past_due", "title": "Payment Required", "status": 402, "code": "billing_past_due", "detail": "Billing is past due, so calls to customers are paused until the invoice is paid. Customers can still call you.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # billing_unavailable > The billing_unavailable Problem Details code: Billing can't check the balance right now, so paid messages and calls are paused. Try again in a moment. Source: https://docs.nexafone.so/v1/errors/billing_unavailable/ | | | | --- | --- | | HTTP status | `503` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/billing_unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Billing can't check the balance right now, so paid messages and calls are paused. Try again in a moment. - Paid template messages can't be sent because billing is not set up on this server. - WhatsApp calls to customers can't be placed because billing is not set up on this server. ## Handling it Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/billing_unavailable", "title": "Service Unavailable", "status": 503, "code": "billing_unavailable", "detail": "Billing can't check the balance right now, so paid messages and calls are paused. Try again in a moment.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_already_answered > The call_already_answered Problem Details code: Someone else answered this call. Source: https://docs.nexafone.so/v1/errors/call_already_answered/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_already_answered` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Someone else answered this call. - This caller is no longer waiting. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_already_answered", "title": "Conflict", "status": 409, "code": "call_already_answered", "detail": "Someone else answered this call.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_answered_on_other_device > The call_answered_on_other_device Problem Details code: You answered this call on another device. Source: https://docs.nexafone.so/v1/errors/call_answered_on_other_device/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_answered_on_other_device` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You answered this call on another device. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_answered_on_other_device", "title": "Conflict", "status": 409, "code": "call_answered_on_other_device", "detail": "You answered this call on another device.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_back_unavailable > The call_back_unavailable Problem Details code: Only a staff call that rang you and that you did not answer can be called back. Source: https://docs.nexafone.so/v1/errors/call_back_unavailable/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_back_unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Only a staff call that rang you and that you did not answer can be called back. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_back_unavailable", "title": "Conflict", "status": 409, "code": "call_back_unavailable", "detail": "Only a staff call that rang you and that you did not answer can be called back.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_not_ringing > The call_not_ringing Problem Details code: This call is not ringing for you. Source: https://docs.nexafone.so/v1/errors/call_not_ringing/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_not_ringing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This call is not ringing for you. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_not_ringing", "title": "Conflict", "status": 409, "code": "call_not_ringing", "detail": "This call is not ringing for you.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_over > The call_over Problem Details code: This call has ended. Source: https://docs.nexafone.so/v1/errors/call_over/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_over` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This call has ended. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_over", "title": "Conflict", "status": 409, "code": "call_over", "detail": "This call has ended.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_permission_already_granted > The call_permission_already_granted Problem Details code: This customer already allows your calls permanently. Source: https://docs.nexafone.so/v1/errors/call_permission_already_granted/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_permission_already_granted` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This customer already allows your calls permanently. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_permission_already_granted", "title": "Conflict", "status": 409, "code": "call_permission_already_granted", "detail": "This customer already allows your calls permanently.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_permission_missing > The call_permission_missing Problem Details code: The customer has not allowed calls from the business. Source: https://docs.nexafone.so/v1/errors/call_permission_missing/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_permission_missing` | The customer has not allowed calls from the business. Request permission first; see [WhatsApp calling](/v1/calling/). ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This customer has not allowed calls from your business yet. Ask for permission first; you can call once they tap Allow. - WhatsApp does not allow calling this customer right now. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_permission_missing", "title": "Conflict", "status": 409, "code": "call_permission_missing", "detail": "This customer has not allowed calls from your business yet. Ask for permission first; you can call once they tap Allow.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_permission_request_limit > The call_permission_request_limit Problem Details code: WhatsApp allows one call permission request a day and two a week per customer until a call connects. Source: https://docs.nexafone.so/v1/errors/call_permission_request_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_permission_request_limit` | WhatsApp allows one call permission request a day and two a week per customer until a call connects. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp allows one call permission request a day and two a week until you and the customer have a call. You can ask again after … UTC. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_permission_request_limit", "title": "Too Many Requests", "status": 429, "code": "call_permission_request_limit", "detail": "WhatsApp allows one call permission request a day and two a week until you and the customer have a call. You can ask again after … UTC.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_permission_template_missing > The call_permission_template_missing Problem Details code: More than 24 hours have passed since the customer last wrote, so only an approved template with a call permission button can ask them. Create one under Templates. Source: https://docs.nexafone.so/v1/errors/call_permission_template_missing/ | | | | --- | --- | | HTTP status | `422` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_permission_template_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - More than 24 hours have passed since the customer last wrote, so only an approved template with a call permission button can ask them. Create one under Templates. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_permission_template_missing", "title": "Unprocessable Entity", "status": 422, "code": "call_permission_template_missing", "detail": "More than 24 hours have passed since the customer last wrote, so only an approved template with a call permission button can ask them. Create one under Templates.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_recording_consent_missing > The call_recording_consent_missing Problem Details code: Pick the recording announcement in the WhatsApp call settings before recording calls. Source: https://docs.nexafone.so/v1/errors/call_recording_consent_missing/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_recording_consent_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Pick the recording announcement in the WhatsApp call settings before recording calls. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_recording_consent_missing", "title": "Conflict", "status": 409, "code": "call_recording_consent_missing", "detail": "Pick the recording announcement in the WhatsApp call settings before recording calls.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # call_recording_not_in_plan > The call_recording_not_in_plan Problem Details code: Your workspace's plan does not include call recording. Source: https://docs.nexafone.so/v1/errors/call_recording_not_in_plan/ | | | | --- | --- | | HTTP status | `403` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/call_recording_not_in_plan` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Your workspace's plan does not include call recording. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/call_recording_not_in_plan", "title": "Forbidden", "status": 403, "code": "call_recording_not_in_plan", "detail": "Your workspace's plan does not include call recording.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # client_closed_request > The client_closed_request Problem Details code: The request was cancelled. Source: https://docs.nexafone.so/v1/errors/client_closed_request/ | | | | --- | --- | | HTTP status | `499` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/client_closed_request` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request was cancelled. ## Handling it The client closed the connection before the answer; nothing to handle. ## Example ```json { "type": "https://docs.nexafone.so/errors/client_closed_request", "title": "Client Closed Request", "status": 499, "code": "client_closed_request", "detail": "The request was cancelled.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # client_msg_id_reused > The client_msg_id_reused Problem Details code: This clientMsgId was already used for another message. Use a new id for each message. Source: https://docs.nexafone.so/v1/errors/client_msg_id_reused/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/client_msg_id_reused` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This clientMsgId was already used for another message. Use a new id for each message. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/client_msg_id_reused", "title": "Conflict", "status": 409, "code": "client_msg_id_reused", "detail": "This clientMsgId was already used for another message. Use a new id for each message.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # client_not_allowed > The client_not_allowed Problem Details code: The page’s origin (or the app’s bundle id or package name) is not on the publishable key’s list. Source: https://docs.nexafone.so/v1/errors/client_not_allowed/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/client_not_allowed` | The page’s origin (or the app’s bundle id or package name) is not on the publishable key’s list. Add it to the key, or for a partner key to the partner’s domains. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Partner sessions are exchanged only on the partner's own sites. - This publishable key does not allow this client. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/client_not_allowed", "title": "Forbidden", "status": 403, "code": "client_not_allowed", "detail": "Partner sessions are exchanged only on the partner's own sites.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # conflict > The conflict Problem Details code: The request conflicts with the current state, such as a duplicate name. Source: https://docs.nexafone.so/v1/errors/conflict/ | | | | --- | --- | | HTTP status | `409` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/conflict` | The request conflicts with the current state, such as a duplicate name. Read the current state and retry. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A workspace can have at most … upcoming holidays. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/conflict", "title": "Conflict", "status": 409, "code": "conflict", "detail": "A workspace can have at most … upcoming holidays.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # coupon_code_taken > The coupon_code_taken Problem Details code: Another promotion already uses this coupon code. Source: https://docs.nexafone.so/v1/errors/coupon_code_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/coupon_code_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Another promotion already uses this coupon code. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/coupon_code_taken", "title": "Conflict", "status": 409, "code": "coupon_code_taken", "detail": "Another promotion already uses this coupon code.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # credit_limit_reached > The credit_limit_reached Problem Details code: This workspace has used its credit limit, so calls to customers are paused. Customers can still call you. Source: https://docs.nexafone.so/v1/errors/credit_limit_reached/ | | | | --- | --- | | HTTP status | `402` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/credit_limit_reached` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace has used its credit limit, so calls to customers are paused. Customers can still call you. - This workspace has used its credit limit, so paid templates are paused. Free replies inside the customer's 24-hour window still send. - This workspace reached its credit limit, so the call was ended. ## Handling it A payment is needed: top up the wallet or settle the invoice, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/credit_limit_reached", "title": "Payment Required", "status": 402, "code": "credit_limit_reached", "detail": "This workspace has used its credit limit, so calls to customers are paused. Customers can still call you.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # dashboard_sign_in_required > The dashboard_sign_in_required Problem Details code: Sign in to the dashboard to make this change. Source: https://docs.nexafone.so/v1/errors/dashboard_sign_in_required/ | | | | --- | --- | | HTTP status | `403` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/dashboard_sign_in_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Sign in to the dashboard to make this change. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/dashboard_sign_in_required", "title": "Forbidden", "status": 403, "code": "dashboard_sign_in_required", "detail": "Sign in to the dashboard to make this change.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # domain_taken > The domain_taken Problem Details code: HTTP varies Source: https://docs.nexafone.so/v1/errors/domain_taken/ | | | | --- | --- | | HTTP status | varies | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/domain_taken` | ## Example ```json { "type": "https://docs.nexafone.so/errors/domain_taken", "title": "Error", "status": 400, "code": "domain_taken", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # email_not_verified > The email_not_verified Problem Details code: Verify your email address before accepting an invitation. Source: https://docs.nexafone.so/v1/errors/email_not_verified/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/email_not_verified` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Verify your email address before accepting an invitation. - Verify your email address before you create a workspace. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/email_not_verified", "title": "Forbidden", "status": 403, "code": "email_not_verified", "detail": "Verify your email address before accepting an invitation.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # embed_origin_limit > The embed_origin_limit Problem Details code: A workspace can have at most 20 embed origins. Remove one first. Source: https://docs.nexafone.so/v1/errors/embed_origin_limit/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/embed_origin_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A workspace can have at most 20 embed origins. Remove one first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/embed_origin_limit", "title": "Conflict", "status": 409, "code": "embed_origin_limit", "detail": "A workspace can have at most 20 embed origins. Remove one first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # feature_not_in_plan > The feature_not_in_plan Problem Details code: The workspace’s plan does not include the feature the operation declares (x-feature in the API reference). Source: https://docs.nexafone.so/v1/errors/feature_not_in_plan/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/feature_not_in_plan` | The workspace’s plan does not include the feature the operation declares (`x-feature` in the [API reference](/v1/api/)). The workspace owner can change the plan. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace's plan does not include SDK embeds. - Your workspace's plan does not include SDK embeds, which publishable keys are for. - Your workspace's plan does not include call recording. - Your workspace's plan does not include calls. - Your workspace's plan does not include meetings. - Your workspace's plan does not include this feature. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/feature_not_in_plan", "title": "Forbidden", "status": 403, "code": "feature_not_in_plan", "detail": "This workspace's plan does not include SDK embeds.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # file_infected > The file_infected Problem Details code: This file was flagged as malware and can't be opened. Source: https://docs.nexafone.so/v1/errors/file_infected/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/file_infected` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This file was flagged as malware and can't be opened. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/file_infected", "title": "Forbidden", "status": 403, "code": "file_infected", "detail": "This file was flagged as malware and can't be opened.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # file_not_scanned > The file_not_scanned Problem Details code: This file has not been scanned for malware. Confirm that you want to open it anyway. Source: https://docs.nexafone.so/v1/errors/file_not_scanned/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/file_not_scanned` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This file has not been scanned for malware. Confirm that you want to open it anyway. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/file_not_scanned", "title": "Conflict", "status": 409, "code": "file_not_scanned", "detail": "This file has not been scanned for malware. Confirm that you want to open it anyway.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # forbidden > The forbidden Problem Details code: The credential is valid but may not do this: a secret key’s scopes or a member’s role lack the operation’s permission, or an IP allow-list or origin check refused the request. Source: https://docs.nexafone.so/v1/errors/forbidden/ | | | | --- | --- | | HTTP status | `403` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/forbidden` | The credential is valid but may not do this: a secret key’s scopes or a member’s role lack the operation’s permission, or an IP allow-list or origin check refused the request. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/forbidden", "title": "Forbidden", "status": 403, "code": "forbidden", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # idempotency_in_flight > The idempotency_in_flight Problem Details code: A request with the same Idempotency-Key is still running. Source: https://docs.nexafone.so/v1/errors/idempotency_in_flight/ | | | | --- | --- | | HTTP status | `409` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/idempotency_in_flight` | A request with the same `Idempotency-Key` is still running. Retry after a short wait to get its stored response. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A request with this Idempotency-Key is still being processed. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/idempotency_in_flight", "title": "Conflict", "status": 409, "code": "idempotency_in_flight", "detail": "A request with this Idempotency-Key is still being processed.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # idempotency_key_reused > The idempotency_key_reused Problem Details code: The Idempotency-Key was already used for a different request body or operation. Source: https://docs.nexafone.so/v1/errors/idempotency_key_reused/ | | | | --- | --- | | HTTP status | `422` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/idempotency_key_reused` | The `Idempotency-Key` was already used for a different request body or operation. Use a new key for each distinct request. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This Idempotency-Key was already used for a different request. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/idempotency_key_reused", "title": "Unprocessable Entity", "status": 422, "code": "idempotency_key_reused", "detail": "This Idempotency-Key was already used for a different request.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # impersonation_approval_expired > The impersonation_approval_expired Problem Details code: The approval expired … minutes after it was given. Request the session again. Source: https://docs.nexafone.so/v1/errors/impersonation_approval_expired/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/impersonation_approval_expired` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The approval expired … minutes after it was given. Request the session again. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/impersonation_approval_expired", "title": "Conflict", "status": 409, "code": "impersonation_approval_expired", "detail": "The approval expired … minutes after it was given. Request the session again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # impersonation_not_allowed > The impersonation_not_allowed Problem Details code: Exports and call credentials are not available while viewing a workspace. Source: https://docs.nexafone.so/v1/errors/impersonation_not_allowed/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/impersonation_not_allowed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Exports and call credentials are not available while viewing a workspace. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/impersonation_not_allowed", "title": "Forbidden", "status": 403, "code": "impersonation_not_allowed", "detail": "Exports and call credentials are not available while viewing a workspace.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # impersonation_read_only > The impersonation_read_only Problem Details code: You are viewing this workspace read-only. Nothing can be changed. Source: https://docs.nexafone.so/v1/errors/impersonation_read_only/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/impersonation_read_only` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You are viewing this workspace read-only. Nothing can be changed. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/impersonation_read_only", "title": "Forbidden", "status": 403, "code": "impersonation_read_only", "detail": "You are viewing this workspace read-only. Nothing can be changed.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # insufficient_balance > The insufficient_balance Problem Details code: The prepaid wallet cannot cover the charge. Source: https://docs.nexafone.so/v1/errors/insufficient_balance/ | | | | --- | --- | | HTTP status | `402` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/insufficient_balance` | The prepaid wallet cannot cover the charge. Top up the wallet and retry. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The wallet balance does not cover this charge. Top up the wallet and try again. - The wallet balance is too low to call this customer. Top up the wallet to place WhatsApp calls; customers can still call you. - The wallet balance is too low to send this paid template. Top up the wallet to send it; free replies inside the customer's 24-hour window still send. - The wallet balance ran out, so the call was ended. Top up the wallet to keep calling customers. ## Handling it A payment is needed: top up the wallet or settle the invoice, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/insufficient_balance", "title": "Payment Required", "status": 402, "code": "insufficient_balance", "detail": "The wallet balance does not cover this charge. Top up the wallet and try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # internal > The internal Problem Details code: Something failed on Nexafon’s side. Source: https://docs.nexafone.so/v1/errors/internal/ | | | | --- | --- | | HTTP status | `500` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/internal` | Something failed on Nexafon’s side. The response never contains internal details; quote its `requestId` when you contact support. Retrying with the same `Idempotency-Key` is safe. ## Handling it Retry later with backoff; quote the `requestId` if it persists. ## Example ```json { "type": "https://docs.nexafone.so/errors/internal", "title": "Internal Server Error", "status": 500, "code": "internal", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # invalid_publishable_key > The invalid_publishable_key Problem Details code: The publishable key is unknown or revoked, or the workspace or partner cannot use it. Source: https://docs.nexafone.so/v1/errors/invalid_publishable_key/ | | | | --- | --- | | HTTP status | `401` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/invalid_publishable_key` | The publishable key is unknown or revoked, or the workspace or partner cannot use it. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The publishable key is not valid. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/invalid_publishable_key", "title": "Unauthorized", "status": 401, "code": "invalid_publishable_key", "detail": "The publishable key is not valid.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # invalid_user_token > The invalid_user_token Problem Details code: The user token is not a valid HS256 JWT signed with one of the workspace’s (or partner’s) secret keys of the same mode as the publishable key, or a required claim (sub, jti, exp) is missing. Source: https://docs.nexafone.so/v1/errors/invalid_user_token/ | | | | --- | --- | | HTTP status | `401` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/invalid_user_token` | The user token is not a valid HS256 JWT signed with one of the workspace’s (or partner’s) secret keys of the same mode as the publishable key, or a required claim (`sub`, `jti`, `exp`) is missing. See [user tokens](/v1/server/user-tokens/). ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/invalid_user_token", "title": "Unauthorized", "status": 401, "code": "invalid_user_token", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # invitation_email_mismatch > The invitation_email_mismatch Problem Details code: This invitation was sent to a different email address. Sign in with that address to accept it. Source: https://docs.nexafone.so/v1/errors/invitation_email_mismatch/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/invitation_email_mismatch` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This invitation was sent to a different email address. Sign in with that address to accept it. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/invitation_email_mismatch", "title": "Forbidden", "status": 403, "code": "invitation_email_mismatch", "detail": "This invitation was sent to a different email address. Sign in with that address to accept it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # invitation_expired > The invitation_expired Problem Details code: This invitation has expired. Ask for a new one. Source: https://docs.nexafone.so/v1/errors/invitation_expired/ | | | | --- | --- | | HTTP status | `410` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/invitation_expired` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This invitation has expired. Ask for a new one. ## Handling it The resource is gone for good. ## Example ```json { "type": "https://docs.nexafone.so/errors/invitation_expired", "title": "Gone", "status": 410, "code": "invitation_expired", "detail": "This invitation has expired. Ask for a new one.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # invoice_pdf_pending > The invoice_pdf_pending Problem Details code: The invoice PDF is still being prepared. Try again in a minute. Source: https://docs.nexafone.so/v1/errors/invoice_pdf_pending/ | | | | --- | --- | | HTTP status | `409` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/invoice_pdf_pending` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The invoice PDF is still being prepared. Try again in a minute. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/invoice_pdf_pending", "title": "Conflict", "status": 409, "code": "invoice_pdf_pending", "detail": "The invoice PDF is still being prepared. Try again in a minute.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # last_owner > The last_owner Problem Details code: A workspace needs at least one owner. Make someone else an owner first. Source: https://docs.nexafone.so/v1/errors/last_owner/ | | | | --- | --- | | HTTP status | `409` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/last_owner` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A workspace needs at least one owner. Make someone else an owner first. - The store … (…) needs at least one owner, and this user is its last. Make someone else an owner first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/last_owner", "title": "Conflict", "status": 409, "code": "last_owner", "detail": "A workspace needs at least one owner. Make someone else an owner first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # last_team > The last_team Problem Details code: You can't delete the only team. Routing needs at least one. Source: https://docs.nexafone.so/v1/errors/last_team/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/last_team` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You can't delete the only team. Routing needs at least one. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/last_team", "title": "Conflict", "status": 409, "code": "last_team", "detail": "You can't delete the only team. Routing needs at least one.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # media_unavailable > The media_unavailable Problem Details code: Calls cannot connect right now. Try again in a moment. Source: https://docs.nexafone.so/v1/errors/media_unavailable/ | | | | --- | --- | | HTTP status | `409`, `503` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/media_unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calls cannot connect right now. Try again in a moment. - This file is not ready to send: finish the upload, or pick a file that passed the malware scan. - This message's file is not stored yet, or could not be copied from WhatsApp. ## Handling it The current state does not allow this; read it again before retrying. Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/media_unavailable", "title": "Conflict", "status": 409, "code": "media_unavailable", "detail": "Calls cannot connect right now. Try again in a moment.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # meeting_cancelled > The meeting_cancelled Problem Details code: This meeting was cancelled. Source: https://docs.nexafone.so/v1/errors/meeting_cancelled/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/meeting_cancelled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This meeting was cancelled. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/meeting_cancelled", "title": "Conflict", "status": 409, "code": "meeting_cancelled", "detail": "This meeting was cancelled.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # meeting_not_open > The meeting_not_open Problem Details code: This meeting has not started. Source: https://docs.nexafone.so/v1/errors/meeting_not_open/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/meeting_not_open` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This meeting has not started. - This meeting opens 10 minutes before it starts. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/meeting_not_open", "title": "Conflict", "status": 409, "code": "meeting_not_open", "detail": "This meeting has not started.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # meeting_over > The meeting_over Problem Details code: This meeting is over. Source: https://docs.nexafone.so/v1/errors/meeting_over/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/meeting_over` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This meeting is over. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/meeting_over", "title": "Conflict", "status": 409, "code": "meeting_over", "detail": "This meeting is over.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # message_not_sent > The message_not_sent Problem Details code: Only messages that reached WhatsApp can get a reaction. Source: https://docs.nexafone.so/v1/errors/message_not_sent/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/message_not_sent` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Only messages that reached WhatsApp can get a reaction. - That message has not reached WhatsApp, so it can't be quoted. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/message_not_sent", "title": "Conflict", "status": 409, "code": "message_not_sent", "detail": "Only messages that reached WhatsApp can get a reaction.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # method_not_allowed > The method_not_allowed Problem Details code: This endpoint does not support the request method. Source: https://docs.nexafone.so/v1/errors/method_not_allowed/ | | | | --- | --- | | HTTP status | `405` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/method_not_allowed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This endpoint does not support the request method. ## Example ```json { "type": "https://docs.nexafone.so/errors/method_not_allowed", "title": "Method Not Allowed", "status": 405, "code": "method_not_allowed", "detail": "This endpoint does not support the request method.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # no_free_seat > The no_free_seat Problem Details code: The store has no free seat for a new member. Source: https://docs.nexafone.so/v1/errors/no_free_seat/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/no_free_seat` | The store has no free seat for a new member. The store’s owner adds seats or turns on adding seats automatically, or the partner disables a member. See [billing](/v1/billing/). ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The store … (…) has no free seat for this user. Its owner can add seats, or the partner can free one. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/no_free_seat", "title": "Forbidden", "status": 403, "code": "no_free_seat", "detail": "The store … (…) has no free seat for this user. Its owner can add seats, or the partner can free one.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # no_transfer > The no_transfer Problem Details code: No warm transfer is in progress on this call. Source: https://docs.nexafone.so/v1/errors/no_transfer/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/no_transfer` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - No warm transfer is in progress on this call. - Nobody has answered the transfer yet. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/no_transfer", "title": "Conflict", "status": 409, "code": "no_transfer", "detail": "No warm transfer is in progress on this call.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # not_a_whatsapp_call > The not_a_whatsapp_call Problem Details code: This is not a WhatsApp call. Source: https://docs.nexafone.so/v1/errors/not_a_whatsapp_call/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/not_a_whatsapp_call` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This is not a WhatsApp call. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/not_a_whatsapp_call", "title": "Conflict", "status": 409, "code": "not_a_whatsapp_call", "detail": "This is not a WhatsApp call.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # not_acceptable > The not_acceptable Problem Details code: HTTP `406` Source: https://docs.nexafone.so/v1/errors/not_acceptable/ | | | | --- | --- | | HTTP status | `406` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/not_acceptable` | ## Example ```json { "type": "https://docs.nexafone.so/errors/not_acceptable", "title": "Not Acceptable", "status": 406, "code": "not_acceptable", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # not_found > The not_found Problem Details code: Nothing with that id exists in this workspace. Source: https://docs.nexafone.so/v1/errors/not_found/ | | | | --- | --- | | HTTP status | `404` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/not_found` | Nothing with that id exists **in this workspace**. Ids from other workspaces, and stores of other partners, also answer 404, so a 404 never reveals that something exists elsewhere. ## Handling it Check the id and the workspace the credential belongs to. ## Example ```json { "type": "https://docs.nexafone.so/errors/not_found", "title": "Not Found", "status": 404, "code": "not_found", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # not_in_call > The not_in_call Problem Details code: Only someone in the call can do this. Source: https://docs.nexafone.so/v1/errors/not_in_call/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/not_in_call` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Only someone in the call can do this. - This person is not ringing or in the call. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/not_in_call", "title": "Conflict", "status": 409, "code": "not_in_call", "detail": "Only someone in the call can do this.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # order_status_not_allowed > The order_status_not_allowed Problem Details code: Only a delivery order goes out for delivery. Source: https://docs.nexafone.so/v1/errors/order_status_not_allowed/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/order_status_not_allowed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Only a delivery order goes out for delivery. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/order_status_not_allowed", "title": "Conflict", "status": 409, "code": "order_status_not_allowed", "detail": "Only a delivery order goes out for delivery.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # partner_key_limit > The partner_key_limit Problem Details code: A partner can have … active keys. Revoke one it no longer uses first. Source: https://docs.nexafone.so/v1/errors/partner_key_limit/ | | | | --- | --- | | HTTP status | `409` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/partner_key_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A partner can have … active keys. Revoke one it no longer uses first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/partner_key_limit", "title": "Conflict", "status": 409, "code": "partner_key_limit", "detail": "A partner can have … active keys. Revoke one it no longer uses first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # partner_plan_unavailable > The partner_plan_unavailable Problem Details code: The partner's default store plan has no published version. Ask Nexafon to publish it. Source: https://docs.nexafone.so/v1/errors/partner_plan_unavailable/ | | | | --- | --- | | HTTP status | `409` | | Area | Partners | | Problem `type` | `https://docs.nexafone.so/errors/partner_plan_unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The partner's default store plan has no published version. Ask Nexafon to publish it. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/partner_plan_unavailable", "title": "Conflict", "status": 409, "code": "partner_plan_unavailable", "detail": "The partner's default store plan has no published version. Ask Nexafon to publish it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # partner_store_unknown > The partner_store_unknown Problem Details code: The partner user token lists an externalRef that is not one of the partner’s stores. Source: https://docs.nexafone.so/v1/errors/partner_store_unknown/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/partner_store_unknown` | The partner user token lists an `externalRef` that is not one of the partner’s stores. Create the store first. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The partner has no store with the externalRef …. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/partner_store_unknown", "title": "Forbidden", "status": 403, "code": "partner_store_unknown", "detail": "The partner has no store with the externalRef ….", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # partner_suspended > The partner_suspended Problem Details code: The partner is suspended, so its keys, sessions and stores’ embeds are refused until Nexafon reactivates it. Source: https://docs.nexafone.so/v1/errors/partner_suspended/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/partner_suspended` | The partner is suspended, so its keys, sessions and stores’ embeds are refused until Nexafon reactivates it. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This partner is suspended. - This store's partner is suspended. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/partner_suspended", "title": "Forbidden", "status": 403, "code": "partner_suspended", "detail": "This partner is suspended.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # payer_numbers_limited > The payer_numbers_limited Problem Details code: This workspace already asked … new numbers to approve a payment today. Pay from a number that paid before, or try again tomorrow. Source: https://docs.nexafone.so/v1/errors/payer_numbers_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/payer_numbers_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace already asked … new numbers to approve a payment today. Pay from a number that paid before, or try again tomorrow. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/payer_numbers_limited", "title": "Too Many Requests", "status": 429, "code": "payer_numbers_limited", "detail": "This workspace already asked … new numbers to approve a payment today. Pay from a number that paid before, or try again tomorrow.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # payload_too_large > The payload_too_large Problem Details code: The request body is too large. Source: https://docs.nexafone.so/v1/errors/payload_too_large/ | | | | --- | --- | | HTTP status | `413` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/payload_too_large` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request body is too large. - The request body must not be larger than … bytes. ## Handling it Send a smaller body. ## Example ```json { "type": "https://docs.nexafone.so/errors/payload_too_large", "title": "Request Entity Too Large", "status": 413, "code": "payload_too_large", "detail": "The request body is too large.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # plan_limit_reached > The plan_limit_reached Problem Details code: A limit of the workspace’s plan is reached. Source: https://docs.nexafone.so/v1/errors/plan_limit_reached/ | | | | --- | --- | | HTTP status | `403` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/plan_limit_reached` | A limit of the workspace’s plan is reached. Remove something or change the plan. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Your plan allows meetings of up to … minutes. - Your plan allows … people in one call, and this call could have …. Ring fewer people or change the plan. - Your plan allows … people in one call, and this meeting invites …. - Your plan allows … products and the store has …; this would add …. - Your plan allows … recordings. Delete one you no longer use to add another. - Your workspace's plan allows … of …, and all are in use. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/plan_limit_reached", "title": "Forbidden", "status": 403, "code": "plan_limit_reached", "detail": "Your plan allows meetings of up to … minutes.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # precondition_failed > The precondition_failed Problem Details code: HTTP `412` Source: https://docs.nexafone.so/v1/errors/precondition_failed/ | | | | --- | --- | | HTTP status | `412` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/precondition_failed` | ## Handling it A precondition failed; read the current state and retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/precondition_failed", "title": "Precondition Failed", "status": 412, "code": "precondition_failed", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # promotion_in_use > The promotion_in_use Problem Details code: A promotion uses this number list. Change or delete that promotion first. Source: https://docs.nexafone.so/v1/errors/promotion_in_use/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/promotion_in_use` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A promotion uses this number list. Change or delete that promotion first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/promotion_in_use", "title": "Conflict", "status": 409, "code": "promotion_in_use", "detail": "A promotion uses this number list. Change or delete that promotion first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # promotion_name_taken > The promotion_name_taken Problem Details code: A number list with this name already exists. Source: https://docs.nexafone.so/v1/errors/promotion_name_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/promotion_name_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A number list with this name already exists. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/promotion_name_taken", "title": "Conflict", "status": 409, "code": "promotion_name_taken", "detail": "A number list with this name already exists.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # queue_empty > The queue_empty Problem Details code: Nobody is waiting in your queues right now. Source: https://docs.nexafone.so/v1/errors/queue_empty/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/queue_empty` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Nobody is waiting in your queues right now. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/queue_empty", "title": "Conflict", "status": 409, "code": "queue_empty", "detail": "Nobody is waiting in your queues right now.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # rate_limited > The rate_limited Problem Details code: Too many requests for this IP address, user or API key. Source: https://docs.nexafone.so/v1/errors/rate_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/rate_limited` | Too many requests for this IP address, user or API key. Wait the number of seconds in the `Retry-After` header, then retry. See [rate limits](/v1/rate-limits/). ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Too many callbacks failed verification. Try again later. - Too many requests. Try again later. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/rate_limited", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "Too many callbacks failed verification. Try again later.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # rates_missing > The rates_missing Problem Details code: This can't be priced because no Meta rate is loaded for it yet. Ask Nexafon support to load the rate card. Source: https://docs.nexafone.so/v1/errors/rates_missing/ | | | | --- | --- | | HTTP status | `503` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/rates_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This can't be priced because no Meta rate is loaded for it yet. Ask Nexafon support to load the rate card. ## Handling it Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/rates_missing", "title": "Service Unavailable", "status": 503, "code": "rates_missing", "detail": "This can't be priced because no Meta rate is loaded for it yet. Ask Nexafon support to load the rate card.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # reauthentication_required > The reauthentication_required Problem Details code: Sign in again to continue. Source: https://docs.nexafone.so/v1/errors/reauthentication_required/ | | | | --- | --- | | HTTP status | `401` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/reauthentication_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Sign in again to continue. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/reauthentication_required", "title": "Unauthorized", "status": 401, "code": "reauthentication_required", "detail": "Sign in again to continue.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # recording_in_use > The recording_in_use Problem Details code: This recording is in use. Pick another recording in the places listed, then delete it. Source: https://docs.nexafone.so/v1/errors/recording_in_use/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/recording_in_use` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This recording is in use. Pick another recording in the places listed, then delete it. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/recording_in_use", "title": "Conflict", "status": 409, "code": "recording_in_use", "detail": "This recording is in use. Pick another recording in the places listed, then delete it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # recording_not_ready > The recording_not_ready Problem Details code: This recording has no audio yet. Wait until it is ready. Source: https://docs.nexafone.so/v1/errors/recording_not_ready/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/recording_not_ready` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This recording has no audio yet. Wait until it is ready. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/recording_not_ready", "title": "Conflict", "status": 409, "code": "recording_not_ready", "detail": "This recording has no audio yet. Wait until it is ready.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # retailer_id_taken > The retailer_id_taken Problem Details code: A retailer id is already used by another product of the store. Source: https://docs.nexafone.so/v1/errors/retailer_id_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/retailer_id_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A retailer id is already used by another product of the store. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/retailer_id_taken", "title": "Conflict", "status": 409, "code": "retailer_id_taken", "detail": "A retailer id is already used by another product of the store.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # role_escalation > The role_escalation Problem Details code: Only an owner can make, change or remove an owner. Source: https://docs.nexafone.so/v1/errors/role_escalation/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/role_escalation` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Only an owner can make, change or remove an owner. - You can't grant or change a role that has permissions you don't have. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/role_escalation", "title": "Forbidden", "status": 403, "code": "role_escalation", "detail": "Only an owner can make, change or remove an owner.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # routing_draft_stale > The routing_draft_stale Problem Details code: Routing was published after this draft was started. Review the draft and save it again, then publish. Source: https://docs.nexafone.so/v1/errors/routing_draft_stale/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/routing_draft_stale` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Routing was published after this draft was started. Review the draft and save it again, then publish. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/routing_draft_stale", "title": "Conflict", "status": 409, "code": "routing_draft_stale", "detail": "Routing was published after this draft was started. Review the draft and save it again, then publish.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # routing_ivr_draft_only > The routing_ivr_draft_only Problem Details code: The call menu changes only through the routing draft: save it to /routing/draft, then publish it with /routing/draft/publish. Source: https://docs.nexafone.so/v1/errors/routing_ivr_draft_only/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/routing_ivr_draft_only` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The call menu changes only through the routing draft: save it to /routing/draft, then publish it with /routing/draft/publish. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/routing_ivr_draft_only", "title": "Conflict", "status": 409, "code": "routing_ivr_draft_only", "detail": "The call menu changes only through the routing draft: save it to /routing/draft, then publish it with /routing/draft/publish.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_email_not_verified > The sdk_email_not_verified Problem Details code: The user token needs email_verified set to true the first time this user signs in: the email is how the user is matched to a member. Source: https://docs.nexafone.so/v1/errors/sdk_email_not_verified/ | | | | --- | --- | | HTTP status | `422` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_email_not_verified` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The user token needs email_verified set to true the first time this user signs in: the email is how the user is matched to a member. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_email_not_verified", "title": "Unprocessable Entity", "status": 422, "code": "sdk_email_not_verified", "detail": "The user token needs email_verified set to true the first time this user signs in: the email is how the user is matched to a member.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_email_required > The sdk_email_required Problem Details code: The user token needs an email claim, with email_verified set to true, the first time this user signs in. Source: https://docs.nexafone.so/v1/errors/sdk_email_required/ | | | | --- | --- | | HTTP status | `422` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_email_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The user token needs an email claim, with email_verified set to true, the first time this user signs in. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_email_required", "title": "Unprocessable Entity", "status": 422, "code": "sdk_email_required", "detail": "The user token needs an email claim, with email_verified set to true, the first time this user signs in.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_member_disabled > The sdk_member_disabled Problem Details code: The user’s membership was disabled. Source: https://docs.nexafone.so/v1/errors/sdk_member_disabled/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_member_disabled` | The user’s membership was disabled. Re-enable the member, or for partner stores send them in a partner user token again. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This user's membership of the workspace is disabled. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_member_disabled", "title": "Forbidden", "status": 403, "code": "sdk_member_disabled", "detail": "This user's membership of the workspace is disabled.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_member_not_found > The sdk_member_not_found Problem Details code: No member of the workspace has the token’s email, and the workspace does not add SDK users automatically. Source: https://docs.nexafone.so/v1/errors/sdk_member_not_found/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_member_not_found` | No member of the workspace has the token’s email, and the workspace does not add SDK users automatically. Invite the user first, or have the workspace turn on automatic SDK users. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - No member of this workspace has this user's email, and the workspace does not add SDK users automatically. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_member_not_found", "title": "Forbidden", "status": 403, "code": "sdk_member_not_found", "detail": "No member of this workspace has this user's email, and the workspace does not add SDK users automatically.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_role_not_allowed > The sdk_role_not_allowed Problem Details code: New SDK users can be given … or a role below it. Source: https://docs.nexafone.so/v1/errors/sdk_role_not_allowed/ | | | | --- | --- | | HTTP status | `403` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_role_not_allowed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - New SDK users can be given … or a role below it. - The role claim names no role. - The role … for the store … (…) is not one of Nexafon's roles: owner, admin, supervisor, agent or viewer. - Workspace owners cannot sign in through the SDK. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_role_not_allowed", "title": "Forbidden", "status": 403, "code": "sdk_role_not_allowed", "detail": "New SDK users can be given … or a role below it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # sdk_session_expired > The sdk_session_expired Problem Details code: The SDK session passed its refreshUntil. Source: https://docs.nexafone.so/v1/errors/sdk_session_expired/ | | | | --- | --- | | HTTP status | `401` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/sdk_session_expired` | The SDK session passed its `refreshUntil`. The SDK exchanges a new user token on its own; a custom client calls `POST /api/v1/sdk/sessions` again. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The SDK session has ended. Exchange a new user token. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/sdk_session_expired", "title": "Unauthorized", "status": 401, "code": "sdk_session_expired", "detail": "The SDK session has ended. Exchange a new user token.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # seat_limit_reached > The seat_limit_reached Problem Details code: Every seat of the plan is taken. Source: https://docs.nexafone.so/v1/errors/seat_limit_reached/ | | | | --- | --- | | HTTP status | `403` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/seat_limit_reached` | Every seat of the plan is taken. Free a seat or add seats on the billing page. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Your plan includes … seats, and all are taken by members or open invitations. Remove someone, revoke an invitation or add seats. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/seat_limit_reached", "title": "Forbidden", "status": 403, "code": "seat_limit_reached", "detail": "Your plan includes … seats, and all are taken by members or open invitations. Remove someone, revoke an invitation or add seats.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # slug_taken > The slug_taken Problem Details code: This workspace address is already taken. Source: https://docs.nexafone.so/v1/errors/slug_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/slug_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace address is already taken. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/slug_taken", "title": "Conflict", "status": 409, "code": "slug_taken", "detail": "This workspace address is already taken.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # team_name_taken > The team_name_taken Problem Details code: A team called … already exists. Source: https://docs.nexafone.so/v1/errors/team_name_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/team_name_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A team called … already exists. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/team_name_taken", "title": "Conflict", "status": 409, "code": "team_name_taken", "detail": "A team called … already exists.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # template_busy > The template_busy Problem Details code: This template is still being sent to WhatsApp. Try again in a moment. Source: https://docs.nexafone.so/v1/errors/template_busy/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/template_busy` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This template is still being sent to WhatsApp. Try again in a moment. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/template_busy", "title": "Conflict", "status": 409, "code": "template_busy", "detail": "This template is still being sent to WhatsApp. Try again in a moment.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # template_exists > The template_exists Problem Details code: A template with this name already exists in this language. Pick another name, or edit that template. Source: https://docs.nexafone.so/v1/errors/template_exists/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/template_exists` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A template with this name already exists in this language. Pick another name, or edit that template. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/template_exists", "title": "Conflict", "status": 409, "code": "template_exists", "detail": "A template with this name already exists in this language. Pick another name, or edit that template.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # template_not_approved > The template_not_approved Problem Details code: HTTP `422` Source: https://docs.nexafone.so/v1/errors/template_not_approved/ | | | | --- | --- | | HTTP status | `422` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/template_not_approved` | ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/template_not_approved", "title": "Unprocessable Entity", "status": 422, "code": "template_not_approved", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # template_not_editable > The template_not_editable Problem Details code: HTTP `409` Source: https://docs.nexafone.so/v1/errors/template_not_editable/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/template_not_editable` | ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/template_not_editable", "title": "Conflict", "status": 409, "code": "template_not_editable", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # test_mode_live > The test_mode_live Problem Details code: Test keys and test SDK sessions cannot call Meta for a live WhatsApp number (messages, WhatsApp calls, templates, number and catalog changes) or move money (payments, subscriptions, wallet settings). Source: https://docs.nexafone.so/v1/errors/test_mode_live/ | | | | --- | --- | | HTTP status | `403` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/test_mode_live` | Test keys and test SDK sessions cannot call Meta for a live WhatsApp number (messages, WhatsApp calls, templates, number and catalog changes) or move money (payments, subscriptions, wallet settings). Use a live key, or connect a WhatsApp test number for the Meta operations. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Test-mode keys and SDK sessions cannot reach WhatsApp or move money for a live account. Use a live key, or connect a WhatsApp sandbox number. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/test_mode_live", "title": "Forbidden", "status": 403, "code": "test_mode_live", "detail": "Test-mode keys and SDK sessions cannot reach WhatsApp or move money for a live account. Use a live key, or connect a WhatsApp sandbox number.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # test_mode_live_number > The test_mode_live_number Problem Details code: Test keys and test SDK sessions never message real customers. Source: https://docs.nexafone.so/v1/errors/test_mode_live_number/ | | | | --- | --- | | HTTP status | `403` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/test_mode_live_number` | Test keys and test SDK sessions never message real customers. Use a live key, or connect a WhatsApp test number. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Test-mode keys and SDK sessions cannot send real WhatsApp messages. Connect a WhatsApp test number, or use a live key. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/test_mode_live_number", "title": "Forbidden", "status": 403, "code": "test_mode_live_number", "detail": "Test-mode keys and SDK sessions cannot send real WhatsApp messages. Connect a WhatsApp test number, or use a live key.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # timeout > The timeout Problem Details code: The operation ran out of time. Source: https://docs.nexafone.so/v1/errors/timeout/ | | | | --- | --- | | HTTP status | `504` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/timeout` | The operation ran out of time. Retry with exponential backoff; with an `Idempotency-Key` a retry is safe. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request took too long to complete. ## Handling it Timed out; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/timeout", "title": "Gateway Timeout", "status": 504, "code": "timeout", "detail": "The request took too long to complete.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # transfer_in_progress > The transfer_in_progress Problem Details code: Finish or cancel the transfer in progress first. Source: https://docs.nexafone.so/v1/errors/transfer_in_progress/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/transfer_in_progress` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Finish or cancel the transfer in progress first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/transfer_in_progress", "title": "Conflict", "status": 409, "code": "transfer_in_progress", "detail": "Finish or cancel the transfer in progress first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # unauthenticated > The unauthenticated Problem Details code: The Authorization header is missing, or its key or token is unknown, revoked or of the wrong kind. Source: https://docs.nexafone.so/v1/errors/unauthenticated/ | | | | --- | --- | | HTTP status | `401` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/unauthenticated` | The `Authorization` header is missing, or its key or token is unknown, revoked or of the wrong kind. Check that you send `Authorization: Bearer ` with a live key of the right workspace; an SDK session token that expired needs a refresh. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/unauthenticated", "title": "Unauthorized", "status": 401, "code": "unauthenticated", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # unavailable > The unavailable Problem Details code: A dependency is briefly unavailable. Source: https://docs.nexafone.so/v1/errors/unavailable/ | | | | --- | --- | | HTTP status | `503` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/unavailable` | A dependency is briefly unavailable. Retry with exponential backoff. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Billing lists across workspaces are not configured. - Connecting WhatsApp is not available on this server yet. Try again later. - Embeds are unavailable right now. Try again shortly. - File storage is not configured. - Idempotent requests cannot be accepted right now. Try again later. - Meta webhooks are not configured on this server. ## Handling it Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/unavailable", "title": "Service Unavailable", "status": 503, "code": "unavailable", "detail": "Billing lists across workspaces are not configured.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # unsupported_media_type > The unsupported_media_type Problem Details code: HTTP `415` Source: https://docs.nexafone.so/v1/errors/unsupported_media_type/ | | | | --- | --- | | HTTP status | `415` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/unsupported_media_type` | ## Handling it Send `Content-Type: application/json`. ## Example ```json { "type": "https://docs.nexafone.so/errors/unsupported_media_type", "title": "Unsupported Media Type", "status": 415, "code": "unsupported_media_type", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # upgrade_required > The upgrade_required Problem Details code: Open this endpoint as a WebSocket, version 13. Source: https://docs.nexafone.so/v1/errors/upgrade_required/ | | | | --- | --- | | HTTP status | `426` | | Area | Realtime | | Problem `type` | `https://docs.nexafone.so/errors/upgrade_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Open this endpoint as a WebSocket, version 13. ## Example ```json { "type": "https://docs.nexafone.so/errors/upgrade_required", "title": "Upgrade Required", "status": 426, "code": "upgrade_required", "detail": "Open this endpoint as a WebSocket, version 13.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # upload_incomplete > The upload_incomplete Problem Details code: The file has not been uploaded yet. Upload it with the form, then confirm. Source: https://docs.nexafone.so/v1/errors/upload_incomplete/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/upload_incomplete` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The file has not been uploaded yet. Upload it with the form, then confirm. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/upload_incomplete", "title": "Conflict", "status": 409, "code": "upload_incomplete", "detail": "The file has not been uploaded yet. Upload it with the form, then confirm.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # upload_mismatch > The upload_mismatch Problem Details code: The uploaded file does not match what was announced or is not the type it claims to be. Upload it again. Source: https://docs.nexafone.so/v1/errors/upload_mismatch/ | | | | --- | --- | | HTTP status | `422` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/upload_mismatch` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The uploaded file does not match what was announced or is not the type it claims to be. Upload it again. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/upload_mismatch", "title": "Unprocessable Entity", "status": 422, "code": "upload_mismatch", "detail": "The uploaded file does not match what was announced or is not the type it claims to be. Upload it again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # use_seats_endpoint > The use_seats_endpoint Problem Details code: This add-on is the extra seats of the workspace's subscription. Change the subscription's seats instead. Source: https://docs.nexafone.so/v1/errors/use_seats_endpoint/ | | | | --- | --- | | HTTP status | `409` | | Area | Billing | | Problem `type` | `https://docs.nexafone.so/errors/use_seats_endpoint` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This add-on is the extra seats of the workspace's subscription. Change the subscription's seats instead. - This add-on is your plan's extra seats. Change the subscription's seats instead. - This add-on sells seats. Seats are bought by changing the subscription's seats. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/use_seats_endpoint", "title": "Conflict", "status": 409, "code": "use_seats_endpoint", "detail": "This add-on is the extra seats of the workspace's subscription. Change the subscription's seats instead.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # user_token_expired > The user_token_expired Problem Details code: The token’s exp has passed, or lies more than 10 minutes ahead. Source: https://docs.nexafone.so/v1/errors/user_token_expired/ | | | | --- | --- | | HTTP status | `401` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/user_token_expired` | The token’s `exp` has passed, or lies more than 10 minutes ahead. Sign a new token for every sign-in, just before the SDK asks for it. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The user token has expired. Sign a new one. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/user_token_expired", "title": "Unauthorized", "status": 401, "code": "user_token_expired", "detail": "The user token has expired. Sign a new one.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # user_token_replayed > The user_token_replayed Problem Details code: Each user token works once (its jti is remembered). Source: https://docs.nexafone.so/v1/errors/user_token_replayed/ | | | | --- | --- | | HTTP status | `401` | | Area | Keys and SDK sessions | | Problem `type` | `https://docs.nexafone.so/errors/user_token_replayed` | Each user token works once (its `jti` is remembered). Have `getUserToken` fetch a fresh token from your server every time. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This user token was already used. Sign a new one for every sign-in. ## Handling it Check the credential: send a valid, unrevoked key or a fresh token. ## Example ```json { "type": "https://docs.nexafone.so/errors/user_token_replayed", "title": "Unauthorized", "status": 401, "code": "user_token_replayed", "detail": "This user token was already used. Sign a new one for every sign-in.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # validation_failed > The validation_failed Problem Details code: The request does not match the operation’s schema or rules. Source: https://docs.nexafone.so/v1/errors/validation_failed/ | | | | --- | --- | | HTTP status | `422` | | Area | General | | Problem `type` | `https://docs.nexafone.so/errors/validation_failed` | The request does not match the operation’s schema or rules. `errors` lists each problem with its `location` (such as `body.name`) and a message. Fix the request; retrying it unchanged fails again. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/validation_failed", "title": "Unprocessable Entity", "status": 422, "code": "validation_failed", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # webhook_endpoint_disabled > The webhook_endpoint_disabled Problem Details code: The endpoint is turned off. Source: https://docs.nexafone.so/v1/errors/webhook_endpoint_disabled/ | | | | --- | --- | | HTTP status | `409` | | Area | Webhooks | | Problem `type` | `https://docs.nexafone.so/errors/webhook_endpoint_disabled` | The endpoint is turned off. Turn it back on (`PATCH` with `status: active`) before replaying events to it. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Turn the endpoint back on before replaying events to it. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/webhook_endpoint_disabled", "title": "Conflict", "status": 409, "code": "webhook_endpoint_disabled", "detail": "Turn the endpoint back on before replaying events to it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # webhook_endpoint_limit > The webhook_endpoint_limit Problem Details code: A partner can have … webhook endpoints. Delete one it no longer uses first. Source: https://docs.nexafone.so/v1/errors/webhook_endpoint_limit/ | | | | --- | --- | | HTTP status | `409` | | Area | Webhooks | | Problem `type` | `https://docs.nexafone.so/errors/webhook_endpoint_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A partner can have … webhook endpoints. Delete one it no longer uses first. - A workspace can have … webhook endpoints. Delete one you no longer use first. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/webhook_endpoint_limit", "title": "Conflict", "status": 409, "code": "webhook_endpoint_limit", "detail": "A partner can have … webhook endpoints. Delete one it no longer uses first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_byo_keys_off > The whatsapp_byo_keys_off Problem Details code: Connecting with your own keys is not offered. Connect the number with Meta's signup instead. Source: https://docs.nexafone.so/v1/errors/whatsapp_byo_keys_off/ | | | | --- | --- | | HTTP status | `403` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_byo_keys_off` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Connecting with your own keys is not offered. Connect the number with Meta's signup instead. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_byo_keys_off", "title": "Forbidden", "status": 403, "code": "whatsapp_byo_keys_off", "detail": "Connecting with your own keys is not offered. Connect the number with Meta's signup instead.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_call_country_unsupported > The whatsapp_call_country_unsupported Problem Details code: WhatsApp does not offer calls to customers from business numbers in …. Customers can still call you. Source: https://docs.nexafone.so/v1/errors/whatsapp_call_country_unsupported/ | | | | --- | --- | | HTTP status | `422` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_call_country_unsupported` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp does not offer calls to customers from business numbers in …. Customers can still call you. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_call_country_unsupported", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp_call_country_unsupported", "detail": "WhatsApp does not offer calls to customers from business numbers in …. Customers can still call you.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_call_max_duration > The whatsapp_call_max_duration Problem Details code: Calls to customers end after … minutes, so this call was ended. Source: https://docs.nexafone.so/v1/errors/whatsapp_call_max_duration/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_call_max_duration` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calls to customers end after … minutes, so this call was ended. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_call_max_duration", "title": "Conflict", "status": 409, "code": "whatsapp_call_max_duration", "detail": "Calls to customers end after … minutes, so this call was ended.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_call_not_connected > The whatsapp_call_not_connected Problem Details code: This call is not connected to the customer. Source: https://docs.nexafone.so/v1/errors/whatsapp_call_not_connected/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_call_not_connected` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This call is not connected to the customer. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_call_not_connected", "title": "Conflict", "status": 409, "code": "whatsapp_call_not_connected", "detail": "This call is not connected to the customer.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_calling_not_changed > The whatsapp_calling_not_changed Problem Details code: Meta did not change calling for this number. Check the number in WhatsApp Manager and try again. Source: https://docs.nexafone.so/v1/errors/whatsapp_calling_not_changed/ | | | | --- | --- | | HTTP status | `502` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_calling_not_changed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Meta did not change calling for this number. Check the number in WhatsApp Manager and try again. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_calling_not_changed", "title": "Bad Gateway", "status": 502, "code": "whatsapp_calling_not_changed", "detail": "Meta did not change calling for this number. Check the number in WhatsApp Manager and try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_calling_off > The whatsapp_calling_off Problem Details code: Calling is not turned on for this WhatsApp number. Source: https://docs.nexafone.so/v1/errors/whatsapp_calling_off/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_calling_off` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calling is not turned on for this WhatsApp number. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_calling_off", "title": "Conflict", "status": 409, "code": "whatsapp_calling_off", "detail": "Calling is not turned on for this WhatsApp number.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_calls_unavailable > The whatsapp_calls_unavailable Problem Details code: WhatsApp calls are not available on this server. Source: https://docs.nexafone.so/v1/errors/whatsapp_calls_unavailable/ | | | | --- | --- | | HTTP status | `503` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_calls_unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp calls are not available on this server. ## Handling it Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_calls_unavailable", "title": "Service Unavailable", "status": 503, "code": "whatsapp_calls_unavailable", "detail": "WhatsApp calls are not available on this server.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_flow_decrypt_failed > The whatsapp_flow_decrypt_failed Problem Details code: The request cannot be decrypted with the business key. Source: https://docs.nexafone.so/v1/errors/whatsapp_flow_decrypt_failed/ | | | | --- | --- | | HTTP status | `421` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_flow_decrypt_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request cannot be decrypted with the business key. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_flow_decrypt_failed", "title": "Error", "status": 421, "code": "whatsapp_flow_decrypt_failed", "detail": "The request cannot be decrypted with the business key.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_flow_not_published > The whatsapp_flow_not_published Problem Details code: This WhatsApp Flow is not published on the business number yet. Try again in a few minutes. Source: https://docs.nexafone.so/v1/errors/whatsapp_flow_not_published/ | | | | --- | --- | | HTTP status | `409` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_flow_not_published` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This WhatsApp Flow is not published on the business number yet. Try again in a few minutes. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_flow_not_published", "title": "Conflict", "status": 409, "code": "whatsapp_flow_not_published", "detail": "This WhatsApp Flow is not published on the business number yet. Try again in a few minutes.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_flow_signature_invalid > The whatsapp_flow_signature_invalid Problem Details code: The request signature is missing or does not match the body. Source: https://docs.nexafone.so/v1/errors/whatsapp_flow_signature_invalid/ | | | | --- | --- | | HTTP status | `432` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_flow_signature_invalid` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request signature is missing or does not match the body. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_flow_signature_invalid", "title": "Error", "status": 432, "code": "whatsapp_flow_signature_invalid", "detail": "The request signature is missing or does not match the body.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_flow_token_invalid > The whatsapp_flow_token_invalid Problem Details code: HTTP `427` Source: https://docs.nexafone.so/v1/errors/whatsapp_flow_token_invalid/ | | | | --- | --- | | HTTP status | `427` | | Area | Commerce | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_flow_token_invalid` | ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_flow_token_invalid", "title": "Error", "status": 427, "code": "whatsapp_flow_token_invalid", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_disconnected > The whatsapp_number_disconnected Problem Details code: Connect the business WhatsApp number before calling customers. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_disconnected/ | | | | --- | --- | | HTTP status | `409` | | Area | Calls, meetings and recordings | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_disconnected` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Connect the business WhatsApp number before calling customers. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_disconnected", "title": "Conflict", "status": 409, "code": "whatsapp_number_disconnected", "detail": "Connect the business WhatsApp number before calling customers.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_limit > The whatsapp_number_limit Problem Details code: This workspace already has a WhatsApp number. Each business connects one number; routing sends customers to teams. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_limit/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace already has a WhatsApp number. Each business connects one number; routing sends customers to teams. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_limit", "title": "Conflict", "status": 409, "code": "whatsapp_number_limit", "detail": "This workspace already has a WhatsApp number. Each business connects one number; routing sends customers to teams.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_missing > The whatsapp_number_missing Problem Details code: Connect the business WhatsApp number before managing templates. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_missing/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Connect the business WhatsApp number before managing templates. - Connect the business WhatsApp number before sending messages. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_missing", "title": "Conflict", "status": 409, "code": "whatsapp_number_missing", "detail": "Connect the business WhatsApp number before managing templates.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_not_in_account > The whatsapp_number_not_in_account Problem Details code: This phone number is not in that WhatsApp Business Account. Check the IDs in Meta's WhatsApp Manager. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_not_in_account/ | | | | --- | --- | | HTTP status | `422` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_not_in_account` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This phone number is not in that WhatsApp Business Account. Check the IDs in Meta's WhatsApp Manager. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_not_in_account", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp_number_not_in_account", "detail": "This phone number is not in that WhatsApp Business Account. Check the IDs in Meta's WhatsApp Manager.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_not_included > The whatsapp_number_not_included Problem Details code: Your plan does not include a WhatsApp number. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_not_included/ | | | | --- | --- | | HTTP status | `403` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_not_included` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Your plan does not include a WhatsApp number. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_not_included", "title": "Forbidden", "status": 403, "code": "whatsapp_number_not_included", "detail": "Your plan does not include a WhatsApp number.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_not_onboarded > The whatsapp_number_not_onboarded Problem Details code: Nexafon has no Meta access for this number, so it cannot change its calling. Connect the number again. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_not_onboarded/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_not_onboarded` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Nexafon has no Meta access for this number, so it cannot change its calling. Connect the number again. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_not_onboarded", "title": "Conflict", "status": 409, "code": "whatsapp_number_not_onboarded", "detail": "Nexafon has no Meta access for this number, so it cannot change its calling. Connect the number again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_number_taken > The whatsapp_number_taken Problem Details code: This number is already connected to another workspace. Source: https://docs.nexafone.so/v1/errors/whatsapp_number_taken/ | | | | --- | --- | | HTTP status | `409` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_number_taken` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number is already connected to another workspace. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_number_taken", "title": "Conflict", "status": 409, "code": "whatsapp_number_taken", "detail": "This number is already connected to another workspace.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_token_not_granted > The whatsapp_token_not_granted Problem Details code: HTTP `422` Source: https://docs.nexafone.so/v1/errors/whatsapp_token_not_granted/ | | | | --- | --- | | HTTP status | `422` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_token_not_granted` | ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_token_not_granted", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp_token_not_granted", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_webhooks_not_subscribed > The whatsapp_webhooks_not_subscribed Problem Details code: Meta did not confirm the webhook subscription. Try again. Source: https://docs.nexafone.so/v1/errors/whatsapp_webhooks_not_subscribed/ | | | | --- | --- | | HTTP status | `502` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_webhooks_not_subscribed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Meta did not confirm the webhook subscription. Try again. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_webhooks_not_subscribed", "title": "Bad Gateway", "status": 502, "code": "whatsapp_webhooks_not_subscribed", "detail": "Meta did not confirm the webhook subscription. Try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp_window_closed > The whatsapp_window_closed Problem Details code: WhatsApp only allows free-form messages within 24 hours of the customer’s last message. Source: https://docs.nexafone.so/v1/errors/whatsapp_window_closed/ | | | | --- | --- | | HTTP status | `422` | | Area | Messaging and templates | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp_window_closed` | WhatsApp only allows free-form messages within 24 hours of the customer’s last message. Send an approved template instead. ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - More than 24 hours have passed since the customer last wrote. Only an approved template can be sent until they write again. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp_window_closed", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp_window_closed", "detail": "More than 24 hours have passed since the customer last wrote. Only an approved template can be sent until they write again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.account_restricted > The whatsapp.account_restricted Problem Details code: The WhatsApp Business Account is locked or failed verification. Source: https://docs.nexafone.so/v1/errors/whatsapp.account_restricted/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.account_restricted` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp Business Account is locked or failed verification. - The WhatsApp Business Account is restricted for a policy violation. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.account_restricted", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.account_restricted", "detail": "The WhatsApp Business Account is locked or failed verification.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.auth_failed > The whatsapp.auth_failed Problem Details code: WhatsApp no longer accepts this connection's access token. Reconnect WhatsApp. Source: https://docs.nexafone.so/v1/errors/whatsapp.auth_failed/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.auth_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp no longer accepts this connection's access token. Reconnect WhatsApp. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.auth_failed", "title": "Bad Gateway", "status": 502, "code": "whatsapp.auth_failed", "detail": "WhatsApp no longer accepts this connection's access token. Reconnect WhatsApp.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.bad_response > The whatsapp.bad_response Problem Details code: WhatsApp sent a response Nexafon could not read. Source: https://docs.nexafone.so/v1/errors/whatsapp.bad_response/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.bad_response` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp sent a response Nexafon could not read. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.bad_response", "title": "Bad Gateway", "status": 502, "code": "whatsapp.bad_response", "detail": "WhatsApp sent a response Nexafon could not read.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.business_call_limit > The whatsapp.business_call_limit Problem Details code: This number reached the daily limit of connected business-initiated calls. Source: https://docs.nexafone.so/v1/errors/whatsapp.business_call_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.business_call_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number reached the daily limit of connected business-initiated calls. - WhatsApp allows … connected calls with this customer in this period. You can call again after … UTC. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.business_call_limit", "title": "Too Many Requests", "status": 429, "code": "whatsapp.business_call_limit", "detail": "This number reached the daily limit of connected business-initiated calls.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_already_ongoing > The whatsapp.call_already_ongoing Problem Details code: A call with this contact is already in progress. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_already_ongoing/ | | | | --- | --- | | HTTP status | `409` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_already_ongoing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A call with this contact is already in progress. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_already_ongoing", "title": "Conflict", "status": 409, "code": "whatsapp.call_already_ongoing", "detail": "A call with this contact is already in progress.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_concurrency_limit > The whatsapp.call_concurrency_limit Problem Details code: This number has too many calls in progress. Try again when one ends. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_concurrency_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_concurrency_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number has too many calls in progress. Try again when one ends. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_concurrency_limit", "title": "Too Many Requests", "status": 429, "code": "whatsapp.call_concurrency_limit", "detail": "This number has too many calls in progress. Try again when one ends.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_connection_failed > The whatsapp.call_connection_failed Problem Details code: WhatsApp could not connect the call. Try again. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_connection_failed/ | | | | --- | --- | | HTTP status | `502`, `504` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_connection_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not connect the call. Try again. - WhatsApp timed out connecting the call. Try again. - WhatsApp's call relay could not connect. Try again. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. Timed out; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_connection_failed", "title": "Bad Gateway", "status": 502, "code": "whatsapp.call_connection_failed", "detail": "WhatsApp could not connect the call. Try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_country_unsupported > The whatsapp.call_country_unsupported Problem Details code: Business-initiated WhatsApp calls are not available for this number's country. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_country_unsupported/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_country_unsupported` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Business-initiated WhatsApp calls are not available for this number's country. - WhatsApp calling is not available between this business number and the contact's country. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_country_unsupported", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.call_country_unsupported", "detail": "Business-initiated WhatsApp calls are not available for this number's country.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_media_failed > The whatsapp.call_media_failed Problem Details code: The call lost its audio: media could not be sent in time. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_media_failed/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_media_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The call lost its audio: media could not be sent in time. - The call lost its audio: no media arrived in time. - The call was answered but no audio ever flowed. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_media_failed", "title": "Bad Gateway", "status": 502, "code": "whatsapp.call_media_failed", "detail": "The call lost its audio: media could not be sent in time.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_permission_already_permanent > The whatsapp.call_permission_already_permanent Problem Details code: This contact already allows calls permanently, so no request is needed. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_permission_already_permanent/ | | | | --- | --- | | HTTP status | `409` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_permission_already_permanent` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This contact already allows calls permanently, so no request is needed. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_permission_already_permanent", "title": "Conflict", "status": 409, "code": "whatsapp.call_permission_already_permanent", "detail": "This contact already allows calls permanently, so no request is needed.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_permission_lookup_limit > The whatsapp.call_permission_lookup_limit Problem Details code: Too many call permission checks for this contact. Try again later. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_permission_lookup_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_permission_lookup_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Too many call permission checks for this contact. Try again later. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_permission_lookup_limit", "title": "Too Many Requests", "status": 429, "code": "whatsapp.call_permission_lookup_limit", "detail": "Too many call permission checks for this contact. Try again later.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_permission_missing > The whatsapp.call_permission_missing Problem Details code: This contact has not allowed calls from your business. Send a call permission request first. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_permission_missing/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_permission_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This contact has not allowed calls from your business. Send a call permission request first. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_permission_missing", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.call_permission_missing", "detail": "This contact has not allowed calls from your business. Send a call permission request first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_permission_request_limit > The whatsapp.call_permission_request_limit Problem Details code: You have sent this contact the most call permission requests allowed for now. Try again after the limit resets. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_permission_request_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_permission_request_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You have sent this contact the most call permission requests allowed for now. Try again after the limit resets. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_permission_request_limit", "title": "Too Many Requests", "status": 429, "code": "whatsapp.call_permission_request_limit", "detail": "You have sent this contact the most call permission requests allowed for now. Try again after the limit resets.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_rate_limited > The whatsapp.call_rate_limited Problem Details code: This number is placing calls too quickly. Wait before calling again. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_rate_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_rate_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number is placing calls too quickly. Wait before calling again. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_rate_limited", "title": "Too Many Requests", "status": 429, "code": "whatsapp.call_rate_limited", "detail": "This number is placing calls too quickly. Wait before calling again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_receiver_uncallable > The whatsapp.call_receiver_uncallable Problem Details code: This contact cannot receive WhatsApp calls right now. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_receiver_uncallable/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_receiver_uncallable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This contact cannot receive WhatsApp calls right now. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_receiver_uncallable", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.call_receiver_uncallable", "detail": "This contact cannot receive WhatsApp calls right now.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_setup_failed > The whatsapp.call_setup_failed Problem Details code: WhatsApp could not set up the call. Try again. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_setup_failed/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_setup_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not set up the call. Try again. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_setup_failed", "title": "Bad Gateway", "status": 502, "code": "whatsapp.call_setup_failed", "detail": "WhatsApp could not set up the call. Try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.call_sip_number > The whatsapp.call_sip_number Problem Details code: Calls on this number are handled over SIP, not the Calling API. Source: https://docs.nexafone.so/v1/errors/whatsapp.call_sip_number/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.call_sip_number` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calls on this number are handled over SIP, not the Calling API. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.call_sip_number", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.call_sip_number", "detail": "Calls on this number are handled over SIP, not the Calling API.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.calling_cannot_be_enabled > The whatsapp.calling_cannot_be_enabled Problem Details code: Calling can only be turned on for numbers with a messaging limit of at least 2,000. Source: https://docs.nexafone.so/v1/errors/whatsapp.calling_cannot_be_enabled/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.calling_cannot_be_enabled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calling can only be turned on for numbers with a messaging limit of at least 2,000. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.calling_cannot_be_enabled", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.calling_cannot_be_enabled", "detail": "Calling can only be turned on for numbers with a messaging limit of at least 2,000.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.calling_disabled_low_quality > The whatsapp.calling_disabled_low_quality Problem Details code: WhatsApp paused calling on this number for low quality. Try again when the restriction lifts. Source: https://docs.nexafone.so/v1/errors/whatsapp.calling_disabled_low_quality/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.calling_disabled_low_quality` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp paused calling on this number for low quality. Try again when the restriction lifts. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.calling_disabled_low_quality", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.calling_disabled_low_quality", "detail": "WhatsApp paused calling on this number for low quality. Try again when the restriction lifts.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.calling_not_enabled > The whatsapp.calling_not_enabled Problem Details code: Calling is not turned on for this WhatsApp number. Source: https://docs.nexafone.so/v1/errors/whatsapp.calling_not_enabled/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.calling_not_enabled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calling is not turned on for this WhatsApp number. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.calling_not_enabled", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.calling_not_enabled", "detail": "Calling is not turned on for this WhatsApp number.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.calling_prerequisites_missing > The whatsapp.calling_prerequisites_missing Problem Details code: Calling cannot be turned on until the number is subscribed to call webhooks. Source: https://docs.nexafone.so/v1/errors/whatsapp.calling_prerequisites_missing/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.calling_prerequisites_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Calling cannot be turned on until the number is subscribed to call webhooks. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.calling_prerequisites_missing", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.calling_prerequisites_missing", "detail": "Calling cannot be turned on until the number is subscribed to call webhooks.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.country_restricted > The whatsapp.country_restricted Problem Details code: This WhatsApp Business Account may not message contacts in this country. Source: https://docs.nexafone.so/v1/errors/whatsapp.country_restricted/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.country_restricted` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This WhatsApp Business Account may not message contacts in this country. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.country_restricted", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.country_restricted", "detail": "This WhatsApp Business Account may not message contacts in this country.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.deletion_pending > The whatsapp.deletion_pending Problem Details code: This number was deleted recently. Wait 5 minutes before registering it. Source: https://docs.nexafone.so/v1/errors/whatsapp.deletion_pending/ | | | | --- | --- | | HTTP status | `409` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.deletion_pending` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number was deleted recently. Wait 5 minutes before registering it. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.deletion_pending", "title": "Conflict", "status": 409, "code": "whatsapp.deletion_pending", "detail": "This number was deleted recently. Wait 5 minutes before registering it.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.deregistration_failed > The whatsapp.deregistration_failed Problem Details code: A previous deregistration of this number failed. Deregister it again first. Source: https://docs.nexafone.so/v1/errors/whatsapp.deregistration_failed/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.deregistration_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A previous deregistration of this number failed. Deregister it again first. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.deregistration_failed", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.deregistration_failed", "detail": "A previous deregistration of this number failed. Deregister it again first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.display_name_not_approved > The whatsapp.display_name_not_approved Problem Details code: This number's display name is not approved yet, so it cannot send messages. Source: https://docs.nexafone.so/v1/errors/whatsapp.display_name_not_approved/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.display_name_not_approved` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number's display name is not approved yet, so it cannot send messages. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.display_name_not_approved", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.display_name_not_approved", "detail": "This number's display name is not approved yet, so it cannot send messages.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.ecosystem_limited > The whatsapp.ecosystem_limited Problem Details code: WhatsApp did not deliver this marketing message to protect the contact's experience. Source: https://docs.nexafone.so/v1/errors/whatsapp.ecosystem_limited/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.ecosystem_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp did not deliver this marketing message to protect the contact's experience. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.ecosystem_limited", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.ecosystem_limited", "detail": "WhatsApp did not deliver this marketing message to protect the contact's experience.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.experiment_excluded > The whatsapp.experiment_excluded Problem Details code: WhatsApp held this marketing message back as part of an experiment. Source: https://docs.nexafone.so/v1/errors/whatsapp.experiment_excluded/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.experiment_excluded` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp held this marketing message back as part of an experiment. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.experiment_excluded", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.experiment_excluded", "detail": "WhatsApp held this marketing message back as part of an experiment.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.flow_blocked > The whatsapp.flow_blocked Problem Details code: The WhatsApp Flow in this message is blocked. Source: https://docs.nexafone.so/v1/errors/whatsapp.flow_blocked/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.flow_blocked` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp Flow in this message is blocked. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.flow_blocked", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.flow_blocked", "detail": "The WhatsApp Flow in this message is blocked.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.flow_throttled > The whatsapp.flow_throttled Problem Details code: The WhatsApp Flow in this message hit its hourly limit. Source: https://docs.nexafone.so/v1/errors/whatsapp.flow_throttled/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.flow_throttled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp Flow in this message hit its hourly limit. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.flow_throttled", "title": "Too Many Requests", "status": 429, "code": "whatsapp.flow_throttled", "detail": "The WhatsApp Flow in this message hit its hourly limit.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.invalid_input > The whatsapp.invalid_input Problem Details code: HTTP `422` Source: https://docs.nexafone.so/v1/errors/whatsapp.invalid_input/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.invalid_input` | ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.invalid_input", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.invalid_input", "detail": "A human-readable explanation of this occurrence.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.invalid_parameter > The whatsapp.invalid_parameter Problem Details code: The request to WhatsApp is missing a required value. Source: https://docs.nexafone.so/v1/errors/whatsapp.invalid_parameter/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.invalid_parameter` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The request to WhatsApp is missing a required value. - WhatsApp rejected a value in the request. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.invalid_parameter", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.invalid_parameter", "detail": "The request to WhatsApp is missing a required value.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.marketing_disabled > The whatsapp.marketing_disabled Problem Details code: Marketing templates are turned off for this WhatsApp Business Account. Source: https://docs.nexafone.so/v1/errors/whatsapp.marketing_disabled/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.marketing_disabled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Marketing templates are turned off for this WhatsApp Business Account. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.marketing_disabled", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.marketing_disabled", "detail": "Marketing templates are turned off for this WhatsApp Business Account.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.media_download_failed > The whatsapp.media_download_failed Problem Details code: WhatsApp could not download the contact's media. Source: https://docs.nexafone.so/v1/errors/whatsapp.media_download_failed/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.media_download_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not download the contact's media. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.media_download_failed", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.media_download_failed", "detail": "WhatsApp could not download the contact's media.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.media_too_large > The whatsapp.media_too_large Problem Details code: The media is larger than the … MB Nexafon accepts. Source: https://docs.nexafone.so/v1/errors/whatsapp.media_too_large/ | | | | --- | --- | | HTTP status | `413` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.media_too_large` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The media is larger than the … MB Nexafon accepts. ## Handling it Send a smaller body. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.media_too_large", "title": "Request Entity Too Large", "status": 413, "code": "whatsapp.media_too_large", "detail": "The media is larger than the … MB Nexafon accepts.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.media_upload_failed > The whatsapp.media_upload_failed Problem Details code: WhatsApp could not fetch or accept the media. Check its type, size and link. Source: https://docs.nexafone.so/v1/errors/whatsapp.media_upload_failed/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.media_upload_failed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not fetch or accept the media. Check its type, size and link. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.media_upload_failed", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.media_upload_failed", "detail": "WhatsApp could not fetch or accept the media. Check its type, size and link.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.messaging_limit > The whatsapp.messaging_limit Problem Details code: This number reached its messaging limit after quality issues. Source: https://docs.nexafone.so/v1/errors/whatsapp.messaging_limit/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.messaging_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number reached its messaging limit after quality issues. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.messaging_limit", "title": "Too Many Requests", "status": 429, "code": "whatsapp.messaging_limit", "detail": "This number reached its messaging limit after quality issues.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.no_call_id > The whatsapp.no_call_id Problem Details code: WhatsApp did not return an ID for the new call. Source: https://docs.nexafone.so/v1/errors/whatsapp.no_call_id/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.no_call_id` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp did not return an ID for the new call. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.no_call_id", "title": "Bad Gateway", "status": 502, "code": "whatsapp.no_call_id", "detail": "WhatsApp did not return an ID for the new call.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.not_found > The whatsapp.not_found Problem Details code: WhatsApp could not find that item. Source: https://docs.nexafone.so/v1/errors/whatsapp.not_found/ | | | | --- | --- | | HTTP status | `404` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.not_found` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not find that item. ## Handling it Check the id and the workspace the credential belongs to. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.not_found", "title": "Not Found", "status": 404, "code": "whatsapp.not_found", "detail": "WhatsApp could not find that item.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.payment_required > The whatsapp.payment_required Problem Details code: WhatsApp calls need a valid payment method on the WhatsApp Business Account. Source: https://docs.nexafone.so/v1/errors/whatsapp.payment_required/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.payment_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp calls need a valid payment method on the WhatsApp Business Account. - WhatsApp could not bill the business. Check the payment method in Meta Business Manager. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.payment_required", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.payment_required", "detail": "WhatsApp calls need a valid payment method on the WhatsApp Business Account.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.permission_missing > The whatsapp.permission_missing Problem Details code: The WhatsApp connection is missing a permission this action needs. Reconnect WhatsApp. Source: https://docs.nexafone.so/v1/errors/whatsapp.permission_missing/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.permission_missing` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp connection is missing a permission this action needs. Reconnect WhatsApp. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.permission_missing", "title": "Bad Gateway", "status": 502, "code": "whatsapp.permission_missing", "detail": "The WhatsApp connection is missing a permission this action needs. Reconnect WhatsApp.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.phone_not_registered > The whatsapp.phone_not_registered Problem Details code: This WhatsApp number is not registered with the Cloud API. Source: https://docs.nexafone.so/v1/errors/whatsapp.phone_not_registered/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.phone_not_registered` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This WhatsApp number is not registered with the Cloud API. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.phone_not_registered", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.phone_not_registered", "detail": "This WhatsApp number is not registered with the Cloud API.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.phone_number_deleted > The whatsapp.phone_number_deleted Problem Details code: This WhatsApp number has been deleted from the WhatsApp Business Account. Source: https://docs.nexafone.so/v1/errors/whatsapp.phone_number_deleted/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.phone_number_deleted` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This WhatsApp number has been deleted from the WhatsApp Business Account. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.phone_number_deleted", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.phone_number_deleted", "detail": "This WhatsApp number has been deleted from the WhatsApp Business Account.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.rate_limited > The whatsapp.rate_limited Problem Details code: The WhatsApp Business Account reached its request limit. Try again later. Source: https://docs.nexafone.so/v1/errors/whatsapp.rate_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.rate_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp Business Account reached its request limit. Try again later. - Too many requests to WhatsApp. Try again in a moment. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.rate_limited", "title": "Too Many Requests", "status": 429, "code": "whatsapp.rate_limited", "detail": "The WhatsApp Business Account reached its request limit. Try again later.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.recipient_blocked > The whatsapp.recipient_blocked Problem Details code: This contact is blocked on WhatsApp by your business. Source: https://docs.nexafone.so/v1/errors/whatsapp.recipient_blocked/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.recipient_blocked` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This contact is blocked on WhatsApp by your business. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.recipient_blocked", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.recipient_blocked", "detail": "This contact is blocked on WhatsApp by your business.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.recipient_not_allowed > The whatsapp.recipient_not_allowed Problem Details code: That number is not on the test number's allowed recipient list. Add it in WhatsApp Manager first. Source: https://docs.nexafone.so/v1/errors/whatsapp.recipient_not_allowed/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.recipient_not_allowed` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - That number is not on the test number's allowed recipient list. Add it in WhatsApp Manager first. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.recipient_not_allowed", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.recipient_not_allowed", "detail": "That number is not on the test number's allowed recipient list. Add it in WhatsApp Manager first.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.recipient_opted_out > The whatsapp.recipient_opted_out Problem Details code: This contact has stopped marketing messages from your business. Source: https://docs.nexafone.so/v1/errors/whatsapp.recipient_opted_out/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.recipient_opted_out` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This contact has stopped marketing messages from your business. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.recipient_opted_out", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.recipient_opted_out", "detail": "This contact has stopped marketing messages from your business.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.recipient_throttled > The whatsapp.recipient_throttled Problem Details code: Too many messages to this contact in a short time. Wait before sending more. Source: https://docs.nexafone.so/v1/errors/whatsapp.recipient_throttled/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.recipient_throttled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Too many messages to this contact in a short time. Wait before sending more. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.recipient_throttled", "title": "Too Many Requests", "status": 429, "code": "whatsapp.recipient_throttled", "detail": "Too many messages to this contact in a short time. Wait before sending more.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.recipient_undeliverable > The whatsapp.recipient_undeliverable Problem Details code: That number cannot receive WhatsApp messages. Check it is correct and has an up-to-date WhatsApp installed. Source: https://docs.nexafone.so/v1/errors/whatsapp.recipient_undeliverable/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.recipient_undeliverable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - That number cannot receive WhatsApp messages. Check it is correct and has an up-to-date WhatsApp installed. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.recipient_undeliverable", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.recipient_undeliverable", "detail": "That number cannot receive WhatsApp messages. Check it is correct and has an up-to-date WhatsApp installed.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.reengagement_required > The whatsapp.reengagement_required Problem Details code: More than 24 hours have passed since the contact last wrote. Send an approved template instead. Source: https://docs.nexafone.so/v1/errors/whatsapp.reengagement_required/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.reengagement_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - More than 24 hours have passed since the contact last wrote. Send an approved template instead. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.reengagement_required", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.reengagement_required", "detail": "More than 24 hours have passed since the contact last wrote. Send an approved template instead.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.registration_throttled > The whatsapp.registration_throttled Problem Details code: Too many registration attempts for this number. Try again later. Source: https://docs.nexafone.so/v1/errors/whatsapp.registration_throttled/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.registration_throttled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Too many registration attempts for this number. Try again later. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.registration_throttled", "title": "Too Many Requests", "status": 429, "code": "whatsapp.registration_throttled", "detail": "Too many registration attempts for this number. Try again later.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.rejected > The whatsapp.rejected Problem Details code: WhatsApp could not process the request. Source: https://docs.nexafone.so/v1/errors/whatsapp.rejected/ | | | | --- | --- | | HTTP status | `422`, `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.rejected` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp could not process the request. - WhatsApp rejected the request. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.rejected", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.rejected", "detail": "WhatsApp could not process the request.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.same_sender_recipient > The whatsapp.same_sender_recipient Problem Details code: A business number cannot message itself. Source: https://docs.nexafone.so/v1/errors/whatsapp.same_sender_recipient/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.same_sender_recipient` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A business number cannot message itself. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.same_sender_recipient", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.same_sender_recipient", "detail": "A business number cannot message itself.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.spam_limited > The whatsapp.spam_limited Problem Details code: WhatsApp limited sending because too many recent messages were blocked or reported. Source: https://docs.nexafone.so/v1/errors/whatsapp.spam_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.spam_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp limited sending because too many recent messages were blocked or reported. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.spam_limited", "title": "Too Many Requests", "status": 429, "code": "whatsapp.spam_limited", "detail": "WhatsApp limited sending because too many recent messages were blocked or reported.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_disabled > The whatsapp.template_disabled Problem Details code: WhatsApp disabled this template after repeated quality pauses. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_disabled/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_disabled` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp disabled this template after repeated quality pauses. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_disabled", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_disabled", "detail": "WhatsApp disabled this template after repeated quality pauses.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_field_too_long > The whatsapp.template_field_too_long Problem Details code: A template field is longer than WhatsApp allows. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_field_too_long/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_field_too_long` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A template field is longer than WhatsApp allows. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_field_too_long", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_field_too_long", "detail": "A template field is longer than WhatsApp allows.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_format_invalid > The whatsapp.template_format_invalid Problem Details code: The template body is not formatted the way WhatsApp requires. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_format_invalid/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_format_invalid` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The template body is not formatted the way WhatsApp requires. - The template footer is not formatted the way WhatsApp requires. - The template header is not formatted the way WhatsApp requires. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_format_invalid", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_format_invalid", "detail": "The template body is not formatted the way WhatsApp requires.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_limit > The whatsapp.template_limit Problem Details code: This WhatsApp Business Account has reached its template limit. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_limit/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_limit` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This WhatsApp Business Account has reached its template limit. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_limit", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_limit", "detail": "This WhatsApp Business Account has reached its template limit.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_not_found > The whatsapp.template_not_found Problem Details code: That template does not exist or is not approved in this language. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_not_found/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_not_found` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - That template does not exist or is not approved in this language. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_not_found", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_not_found", "detail": "That template does not exist or is not approved in this language.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_param_format > The whatsapp.template_param_format Problem Details code: A template variable has the wrong format. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_param_format/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_param_format` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - A template variable has the wrong format. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_param_format", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_param_format", "detail": "A template variable has the wrong format.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_param_invalid > The whatsapp.template_param_invalid Problem Details code: WhatsApp rejected one of the template's variables. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_param_invalid/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_param_invalid` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp rejected one of the template's variables. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_param_invalid", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_param_invalid", "detail": "WhatsApp rejected one of the template's variables.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_param_mismatch > The whatsapp.template_param_mismatch Problem Details code: The template's variables do not match what the template expects. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_param_mismatch/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_param_mismatch` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The template's variables do not match what the template expects. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_param_mismatch", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_param_mismatch", "detail": "The template's variables do not match what the template expects.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_param_placement > The whatsapp.template_param_placement Problem Details code: Template variables cannot be at the very start or end of the text. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_param_placement/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_param_placement` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Template variables cannot be at the very start or end of the text. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_param_placement", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_param_placement", "detail": "Template variables cannot be at the very start or end of the text.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_paused > The whatsapp.template_paused Problem Details code: WhatsApp paused this template for low quality. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_paused/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_paused` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp paused this template for low quality. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_paused", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_paused", "detail": "WhatsApp paused this template for low quality.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_policy_violation > The whatsapp.template_policy_violation Problem Details code: The template's content breaks WhatsApp policy. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_policy_violation/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_policy_violation` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The template's content breaks WhatsApp policy. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_policy_violation", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_policy_violation", "detail": "The template's content breaks WhatsApp policy.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_status_locked > The whatsapp.template_status_locked Problem Details code: This template's status cannot be changed right now. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_status_locked/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_status_locked` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This template's status cannot be changed right now. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_status_locked", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_status_locked", "detail": "This template's status cannot be changed right now.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_text_too_long > The whatsapp.template_text_too_long Problem Details code: The template text is too long once its variables are filled in. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_text_too_long/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_text_too_long` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The template text is too long once its variables are filled in. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_text_too_long", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_text_too_long", "detail": "The template text is too long once its variables are filled in.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.template_too_many_params > The whatsapp.template_too_many_params Problem Details code: The template has too many variables for its length. Source: https://docs.nexafone.so/v1/errors/whatsapp.template_too_many_params/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.template_too_many_params` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The template has too many variables for its length. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.template_too_many_params", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.template_too_many_params", "detail": "The template has too many variables for its length.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.throughput_limited > The whatsapp.throughput_limited Problem Details code: WhatsApp is sending as fast as this number allows. Try again in a moment. Source: https://docs.nexafone.so/v1/errors/whatsapp.throughput_limited/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.throughput_limited` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp is sending as fast as this number allows. Try again in a moment. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.throughput_limited", "title": "Too Many Requests", "status": 429, "code": "whatsapp.throughput_limited", "detail": "WhatsApp is sending as fast as this number allows. Try again in a moment.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.timeout > The whatsapp.timeout Problem Details code: WhatsApp did not answer in time. Try again. Source: https://docs.nexafone.so/v1/errors/whatsapp.timeout/ | | | | --- | --- | | HTTP status | `504` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.timeout` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - WhatsApp did not answer in time. Try again. ## Handling it Timed out; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.timeout", "title": "Gateway Timeout", "status": 504, "code": "whatsapp.timeout", "detail": "WhatsApp did not answer in time. Try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.token_expired > The whatsapp.token_expired Problem Details code: The WhatsApp access token has expired. Reconnect WhatsApp. Source: https://docs.nexafone.so/v1/errors/whatsapp.token_expired/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.token_expired` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp access token has expired. Reconnect WhatsApp. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.token_expired", "title": "Bad Gateway", "status": 502, "code": "whatsapp.token_expired", "detail": "The WhatsApp access token has expired. Reconnect WhatsApp.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.too_many_pin_attempts > The whatsapp.too_many_pin_attempts Problem Details code: The PIN was entered too quickly. Wait a moment and try again. Source: https://docs.nexafone.so/v1/errors/whatsapp.too_many_pin_attempts/ | | | | --- | --- | | HTTP status | `429` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.too_many_pin_attempts` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The PIN was entered too quickly. Wait a moment and try again. - Too many two-step verification PIN attempts. Try again later. ## Handling it Slow down: wait `Retry-After` seconds, or back off, then retry. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.too_many_pin_attempts", "title": "Too Many Requests", "status": 429, "code": "whatsapp.too_many_pin_attempts", "detail": "The PIN was entered too quickly. Wait a moment and try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.unavailable > The whatsapp.unavailable Problem Details code: The WhatsApp Business Account is in maintenance. Try again later. Source: https://docs.nexafone.so/v1/errors/whatsapp.unavailable/ | | | | --- | --- | | HTTP status | `502`, `503` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.unavailable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The WhatsApp Business Account is in maintenance. Try again later. - WhatsApp could not send the message for an unknown reason. Try again. - WhatsApp is not configured on this server. - WhatsApp is temporarily unavailable. Try again in a moment. - WhatsApp onboarding is not configured on this server. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. Briefly unavailable; retry with backoff. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.unavailable", "title": "Bad Gateway", "status": 502, "code": "whatsapp.unavailable", "detail": "The WhatsApp Business Account is in maintenance. Try again later.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.unreachable > The whatsapp.unreachable Problem Details code: Could not reach WhatsApp. Try again. Source: https://docs.nexafone.so/v1/errors/whatsapp.unreachable/ | | | | --- | --- | | HTTP status | `502` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.unreachable` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - Could not reach WhatsApp. Try again. ## Handling it An upstream service (such as WhatsApp) failed; retry later or follow the message. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.unreachable", "title": "Bad Gateway", "status": 502, "code": "whatsapp.unreachable", "detail": "Could not reach WhatsApp. Try again.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.unsupported_message_type > The whatsapp.unsupported_message_type Problem Details code: That number cannot receive this type of message. Source: https://docs.nexafone.so/v1/errors/whatsapp.unsupported_message_type/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.unsupported_message_type` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - That number cannot receive this type of message. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.unsupported_message_type", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.unsupported_message_type", "detail": "That number cannot receive this type of message.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.verification_required > The whatsapp.verification_required Problem Details code: This number must be verified before it can be registered. Source: https://docs.nexafone.so/v1/errors/whatsapp.verification_required/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.verification_required` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This number must be verified before it can be registered. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.verification_required", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.verification_required", "detail": "This number must be verified before it can be registered.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # whatsapp.wrong_pin > The whatsapp.wrong_pin Problem Details code: The two-step verification PIN is wrong. Source: https://docs.nexafone.so/v1/errors/whatsapp.wrong_pin/ | | | | --- | --- | | HTTP status | `422` | | Area | WhatsApp (from Meta) | | Problem `type` | `https://docs.nexafone.so/errors/whatsapp.wrong_pin` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - The two-step verification PIN is wrong. ## Handling it The request is well-formed but its values are not accepted; fix them before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/whatsapp.wrong_pin", "title": "Unprocessable Entity", "status": 422, "code": "whatsapp.wrong_pin", "detail": "The two-step verification PIN is wrong.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # workspace_limit_reached > The workspace_limit_reached Problem Details code: You already own … workspaces, the most allowed. Contact support to add another. Source: https://docs.nexafone.so/v1/errors/workspace_limit_reached/ | | | | --- | --- | | HTTP status | `409` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/workspace_limit_reached` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - You already own … workspaces, the most allowed. Contact support to add another. ## Handling it The current state does not allow this; read it again before retrying. ## Example ```json { "type": "https://docs.nexafone.so/errors/workspace_limit_reached", "title": "Conflict", "status": 409, "code": "workspace_limit_reached", "detail": "You already own … workspaces, the most allowed. Contact support to add another.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/). --- # workspace_read_only > The workspace_read_only Problem Details code: This workspace is cancelled. You can view it but not change anything. Source: https://docs.nexafone.so/v1/errors/workspace_read_only/ | | | | --- | --- | | HTTP status | `403` | | Area | Workspace | | Problem `type` | `https://docs.nexafone.so/errors/workspace_read_only` | ## What the `detail` says The `detail` explains the occurrence; it can read, for example: - This workspace is cancelled. You can view it but not change anything. - This workspace is suspended. You can view it but not change anything. ## Handling it The request is understood but refused; retrying it unchanged fails again. ## Example ```json { "type": "https://docs.nexafone.so/errors/workspace_read_only", "title": "Forbidden", "status": 403, "code": "workspace_read_only", "detail": "This workspace is cancelled. You can view it but not change anything.", "requestId": "01J9Z8Q4TQ3W6M5V2X7Y8Z9A0B" } ``` Match on `code`, never on `detail`: the wording may change. See [Problem Details](/v1/errors/).