Take a fiat card payment from a buyer with no Nevermined account, no API key, no wallet.
- Setup: the merchant must have a Nevermined organization account with a Stripe account linked to it (Stripe Connect) — that's what authorizes it to take card payments and receive the funds
- The organization creates a payable Order server-side with its API key — POST /api/v1/orders
- The org key stays in the merchant backend; the browser only ever calls your own /api/orders
- The buyer pays a hosted Stripe checkout embedded as an iframe — no login, no crypto
- The chat trusts the iframe's nvm:success message only after checking event.origin and the envelope version
One-time setup: the merchant registers a Nevermined organization and links a Stripe account to it (Stripe Connect onboarding), which is what lets the org take card payments and receive the funds.
Then, per purchase: the shopper picks a trip; the app calls its own backend, which calls the Nevermined Orders API with the organization's key and gets back an orderId (the price is looked up server-side, so a tampered client can't name its own amount). The chat mounts the hosted Stripe checkout for that order in an iframe. The buyer pays with a test card; the iframe postMessages nvm:success, the chat verifies event.origin and version === '1', and shows a booked confirmation. clientSecret is never forwarded to the browser — the hosted checkout fetches the order itself.
The only holder of the org key — a Next.js server route. The browser calls this, never the Orders API directly.
export async function POST(req: Request) {
const { packageId } = await req.json()
const pkg = getPackage(packageId) // price is OURS, not the client's
const res = await fetch(`${NVM_API_BASE_URL}/api/v1/orders`, {
method: 'POST',
headers: {
Authorization: `Bearer ${NVM_ORDER_API_KEY}`, // secret — server only
'Content-Type': 'application/json',
},
body: JSON.stringify({
amountMinor: pkg.amountMinor, currency: 'usd', description: pkg.name,
}),
})
const { orderId } = await res.json() // clientSecret NOT forwarded
return Response.json({ orderId })
}Mount the hosted checkout in an iframe; trust nvm:success only from the embed origin and only version 1.
// the hosted Stripe checkout for this order, embedded inline
<iframe src={`${embedBase}/checkout/order/${orderId}` +
`?parentOrigin=${encodeURIComponent(location.origin)}`} />
// the confirmation is gated on a verified message
window.addEventListener('message', (e) => {
if (e.origin !== embedBase) return // only the embed origin
if (e.data?.type !== 'nvm:success') return // only our event
if (e.data?.version !== '1') return // only the envelope we understand
showBooked(e.data.payload) // { orderId, paymentIntent }
})| src/app/api/orders/route.ts | merchant backend — the only holder of the org key |
| src/app/chat.tsx | the chat UI, the checkout iframe, and the verified postMessage listener |
| src/lib/packages.ts | server-owned catalog — prices live here, not on the client |
Live Orders flow — selecting a trip sends a real POST /api/v1/orders to the backend (the org key stays server-side), then the hosted Stripe checkout runs in the iframe above. Pay with test card 4242 4242 4242 4242; no real money moves. Runs against the deployed Nevermined Orders sandbox (epic #3238) with an org key — no local stack needed. The full source is in fiat-checkout-chat/.