Skip to main content
The quickstart is written for Express. On Next.js only route registration differs, and @paymanai/connect-next does it.
1

Install

Shell
2

Create the client and routes once

lib/payman.ts
Use getPaymanConnect(), never module-scope new PaymanConnect(). Next compiles every route handler into its own bundle, so a module-level new makes one client per route instead of one per process. The symptoms: “not connected” right after a successful consent, and a consent return that hangs.currentUser receives the inbound Request; in App Router you will usually ignore it and call your own session helper, as above.
3

Re-export GET from three route files

app/payman/callback/route.ts
app/payman/approvals/[ref]/stream/route.ts
app/payman/resume/stream/route.ts
That is the whole server side. Register http://localhost:3000/payman/callback as a redirect URI in the developer console, matched byte for byte, port included; payman.callbackPath() returns what the SDK expects.The startup notice prints once per route file. It counts mounts, not clients, so three notices are correct.

Write the two settings yourself

Next reads route segment config by static analysis at build time, so a re-exported constant silently does nothing. Write both in each route file:

The app key at build time

next build evaluates route modules, so the client is constructed during the build and throws without PAYMAN_APP_KEY. Give the build step a key, or construct the client lazily. For a CI build with no secrets, a syntactically valid placeholder is enough; it authenticates nothing.

The client component

app/Chat.tsx
app/layout.tsx
"use client" is required: these mount listeners and open windows. The components ship no CSS; without the stylesheet the connect control renders as a bare hyperlink. See styling the components.
openConnect() and openApproval() must run synchronously inside the click handler. Any await before window.open gets the popup blocked and resolves { state: "popup_blocked" }.

Deploying

  • Serverless means many instances. The default store is in-process, so a connection saved by one instance is missing from the next. Move to sqlStore() (@paymanai/connect/stores/sql) or redisStore() (@paymanai/connect/stores/redis) before you ship.
  • Session cookie must be SameSite=Lax. Strict works on localhost and drops the cookie on the production consent return; mount() prints a reminder, and nothing can check it for you.
  • A pending approval can hold a stream open for up to 16 minutes, longer than most serverless function limits. The browser reconnects on its own, but on per-invocation billing that is many invocations per approval.

Writing the routes by hand

@paymanai/connect-next needs Next 15. On Next 14, or to skip the dependency, @paymanai/connect/fetch inside the core package has the same three handlers; see any other framework. Keep one process-wide client even here.
lib/payman-handlers.ts
The callback and resume handlers re-export directly (export const GET = handlers.callback). The approval handler does not: wrap it, and type the context exactly.
app/payman/approvals/[ref]/stream/route.ts
next build validates each handler against its generated RouteContext. An optional context argument fails with Expected "RouteContext", got "undefined", and a union type is rejected too. Neither check runs under next dev; encoding this is half of what @paymanai/connect-next exists for.