Checkout & payments
One-click checkout with @hyperyai/sdk — BuyButton, SubscribeButton and useCheckout run login → charge → add-card → retry as a popup or redirect, for marketplace purchases, subscriptions and AI-credit top-ups.
@hyperyai/sdk ships a seamless auth + charge flow: a single action drives the
whole chain — press buy → log in (if needed) → charge → add a card (if needed) →
retry → done. An unauthenticated visitor can click Buy and be walked through
sign-in, card entry, and payment without you writing any of that glue.
It comes in these forms:
<BuyButton>— a drop-in "buy with Hypery" button for one-click marketplace purchases.<SubscribeButton>— a drop-in button that subscribes the user to one of your app's subscription plans, monthly or annual.useAppSubscription()— plans, the user's subscription, remaining credit, and subscribe / cancel / resume / switch actions.useCheckout()— the hook underneath, for programmatic checkout (purchases, subscriptions and AI-credit top-ups) on your own UI.useMarketplace()— a single marketplace charge with no login or card-entry steps, for when you handle those yourself.planPriceCents()— read a plan's price for an interval.
Each interactive step (login, card entry) runs as a popup or a redirect,
chosen by interactionMode — so the flow completes in-page on desktop and stays
robust on mobile.
Setup
Checkout reads everything it needs from HyperyProvider — no extra provider. The
only new option is interactionMode:
// app/layout.tsx
import { HyperyProvider } from "@hyperyai/sdk";
export default function RootLayout({ children }) {
return (
<HyperyProvider
config={{
clientId: process.env.NEXT_PUBLIC_HYPERY_CLIENT_ID!,
redirectUri: "https://yourapp.com/callback",
gatewayUrl: "https://hypery.ai",
interactionMode: "auto", // 'auto' | 'popup' | 'redirect'
}}
>
{children}
</HyperyProvider>
);
}interactionMode defaults to auto: popup on desktop, redirect on mobile
or when a popup is blocked. For popup mode, redirectUri must be same-origin
as your app (the popup posts the result back to the opener).
BuyButton
Drop-in checkout for a marketplace purchase. On click it runs the full flow via
useCheckout and credits the sale to the seller's app (a Stripe Connect direct
charge; your platform fee is applied automatically).
import { BuyButton } from "@hyperyai/sdk";
<BuyButton
appId="app_123"
amountCents={499}
description="Pro upgrade"
onSuccess={(r) => console.log("paid", r.paymentIntentId)}
onError={(e) => toast.error(e.message)}
/>;By default the button is two-click — the first click arms it
(Confirm · $4.99 + Cancel), the second charges — because the card is charged
off-session and the buyer should give a clear "yes". Set requireConfirmation={false}
to charge on a single click.
onSuccess receives { ok, paymentIntentId, status, amountCents, applicationFeeCents }.
The button never throws — a redirecting (navigating away) or cancelled
(user closed a popup) result stays quiet, so no callback fires.
SubscribeButton
Subscribes the user to one of your app's plans. Like BuyButton, it logs the user
in if needed, uses the card they already have on Hypery, asks for a card only if
there is none, and asks for a confirming second click first.
import { SubscribeButton } from "@hyperyai/sdk";
<SubscribeButton
planId="66f…"
interval="year" // "month" (default) or "year" — the plan must offer it
priceCents={10000} // display only; Hypery charges the plan's price
onSuccess={(r) => toast.success(r.alreadySubscribed ? "already subscribed" : "subscribed")}
onError={(e) => toast.error(e.message)}
/>The button never throws; redirecting and cancelled results fire no callback.
useAppSubscription
Everything a pricing or account page needs for your app's plans.
import { useAppSubscription, planPriceCents } from "@hyperyai/sdk";
function Plans({ appId }: { appId: string }) {
const {
plans, isSubscribed, activeSubscription, interval, pendingInterval,
nextGrantAt, remainingCreditUsd, subscribe, cancel, resume, switchInterval,
} = useAppSubscription(appId);
if (!isSubscribed) {
return plans.map((p) => (
<button key={p.id} onClick={() => subscribe(p.id, { interval: "year" })}>
{p.name} · ${(planPriceCents(p, "year") ?? 0) / 100}/yr
</button>
));
}
return (
<div>
<p>${remainingCreditUsd.toFixed(2)} credit left · next credit {nextGrantAt}</p>
{interval === "month" && <button onClick={() => switchInterval(undefined, "year")}>switch to yearly</button>}
{interval === "year" && !pendingInterval && (
<button onClick={() => switchInterval(undefined, "month")}>switch to monthly at renewal</button>
)}
{activeSubscription?.cancelAtPeriodEnd
? <button onClick={() => resume()}>resume</button>
: <button onClick={() => cancel()}>cancel</button>}
</div>
);
}useCheckout
The hook behind BuyButton and SubscribeButton. Use it directly when you want checkout on your own
button, or for AI-credit top-ups (which BuyButton doesn't cover).
import { useCheckout } from "@hyperyai/sdk";
function AddCredits() {
const { checkout, status, isRunning } = useCheckout();
const buy = async () => {
const result = await checkout({ kind: "topup", usdAmount: 5 });
if (result.status === "success") {
// balance is funded
}
};
return (
<button onClick={buy} disabled={isRunning}>
{status === "charging" ? "charging…" : "add $5 of credits"}
</button>
);
}useCheckout() returns:
checkout(input) takes one of three shapes:
// marketplace purchase (Stripe Connect)
checkout({ kind: "purchase", appId: "app_123", amountCents: 499, description: "Pro" });
// buy AI credits for the signed-in user's balance
checkout({ kind: "topup", usdAmount: 5 });
// subscribe to one of your app's plans (monthly by default)
checkout({ kind: "subscription", planId: "66f…", interval: "year" });Purchases and subscriptions are idempotent: a stable idempotencyKey is generated (or pass your
own) so a resumed or retried charge is never double-billed.
Popup vs redirect
interactionMode controls how the login and add-card steps appear.
Each step opens a centered popup and the chain completes in-page — no
navigation (like PayPal / Firebase signInWithPopup). Best on desktop.
Requires a same-origin redirectUri.
Each step is a full-page redirect. The pending checkout is persisted to
localStorage and auto-resumes when the user returns — robust on mobile and
when popups are blocked.
The default. Uses popup on desktop and falls back to redirect on mobile or when a popup is blocked. Pick this unless you have a reason not to.
In redirect mode the flow can navigate away mid-checkout and resume on return —
so treat checkout() returning redirecting as "in progress", not "done".
Resume runs automatically once per page load.
Handling the result
checkout() resolves to a CheckoutResult:
const result = await checkout({ kind: "topup", usdAmount: 5 });
switch (result.status) {
case "success": /* result.data has the charge */ break;
case "error": /* result.error is a ParsedError */ break;
case "cancelled": /* user closed a popup — no-op */ break;
case "redirecting": /* navigating away; resumes on return */ break;
}Errors are already classified (ParsedError), so a
payment decline or a spending-limit hit can drive
error UI without extra parsing. On an error result, data
carries the raw response body (e.g. a subscription's 3-D Secure clientSecret).
A few details of the flow:
- A card is requested only for a 402 that a card can fix — not for
PAYMENT_DECLINEDorPAYMENT_INCOMPLETE, which come back as errors. - The charge is retried once after a card is added; still failing returns a
PAYMENT_METHOD_REQUIREDerror. - Calling
checkout()while one is already running returns{ status: 'error' }with noerror. - A redirect-mode resume gives up after 3 attempts.
useMarketplace
A single marketplace charge (POST /api/marketplace/checkout) through
authenticatedFetch. Unlike useCheckout it doesn't log the user in or ask for a
card — it tells you when either is needed. It never throws for billing errors.
import { useMarketplace, useBuyerWallet } from "@hyperyai/sdk";
function Buy({ orderId }: { orderId: string }) {
const { buy, isBuying } = useMarketplace();
const { addCard } = useBuyerWallet();
const onClick = async () => {
const r = await buy({ appId: "app_123", amountCents: 499, description: "Pro", idempotencyKey: orderId });
if (r.ok) return toast.success(`paid ${r.paymentIntentId}`);
if (r.needsPaymentMethod) return addCard();
toast.error(r.error.message);
};
return <button onClick={onClick} disabled={isBuying}>Buy</button>;
}buy(input: BuyInput):
Returns:
Marketplace charges require the seller app to have finished Stripe Connect onboarding. See Marketplace.
planPriceCents
planPriceCents(plan, interval = 'month'): number | null returns a plan's price in
cents for an interval. It reads plan.prices when present and otherwise falls back
to the legacy priceCents / interval fields. Returns null when the plan doesn't
offer that interval.
import { planPriceCents } from "@hyperyai/sdk";
const yearly = planPriceCents(plan, "year");
if (yearly !== null) label = `$${(yearly / 100).toFixed(2)}/yr`;