Add payments to your app
in minutes, not weeks
PayCan gives your application a unified API for one-time and recurring payments through Stripe and PayPal — plus drop-in web components for checkout, plans, orders and subscriptions. Everything on this page uses the real SDK.
Up and running in 3 steps
One npm package (or a hosted ES module), one tiny backend endpoint, one line to open a checkout. Each step is tagged with where the code runs: Frontend · browser or Backend · your server.
Don't have a PayCan instance yet?
Clone the repo, run composer install && npm install, then php artisan paycan:install
(or visit /install in your browser) — the wizard sets up the database and admin account for you.
Install the SDK
Frontend · browser<script type="module">
import { PayCan } from 'https://pay.yourapp.com/sdk/paycan-sdk.js';
</script>Add a token endpoint to your backend
Backend · your serverThis is the only backend code you need. It runs on your server, verifies the user against your login session, and exchanges them for a PayCan user token using your secret API key — which never leaves the server. Never trust a user ID from the request body.
app.post('/api/paycan/token', requireAuth, async (req, res) => {
const response = await fetch(`${process.env.PAYCAN_URL}/api/admin/users/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.PAYCAN_API_SECRET, // secret key: backend only!
},
body: JSON.stringify({
user_id: req.session.userId, // from YOUR session — not the request body
user: { name: req.user.name, email: req.user.email },
}),
});
const { token } = await response.json();
res.json({ token }); // return ONLY the token to the browser
});import { auth } from '@/lib/auth'; // your auth helper (NextAuth, Clerk, …)
export async function POST() {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthenticated' }, { status: 401 });
}
const response = await fetch(`${process.env.PAYCAN_URL}/api/admin/users/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.PAYCAN_API_SECRET, // secret key: backend only!
},
body: JSON.stringify({
user_id: session.user.id, // from YOUR session — not the request body
user: { name: session.user.name, email: session.user.email },
}),
});
const { token } = await response.json();
return Response.json({ token }); // return ONLY the token to the browser
}// routes/api.php
Route::post('/paycan/token', [PayCanController::class, 'getToken'])
->middleware('auth:sanctum');
// app/Http/Controllers/PayCanController.php
public function getToken(Request $request)
{
$user = $request->user(); // from auth middleware — not the request body
$response = Http::withHeaders([
'X-API-Key' => config('services.paycan.secret'), // secret key: backend only!
])->post(config('services.paycan.url').'/api/admin/users/sync', [
'user_id' => $user->id,
'user' => ['name' => $user->name, 'email' => $user->email],
]);
// return ONLY the token to the browser
return response()->json(['token' => $response->json('token')]);
}@login_required
def paycan_token(request):
user = request.user # from the auth decorator — not the request body
response = requests.post(
f"{os.environ['PAYCAN_URL']}/api/admin/users/sync",
headers={'X-API-Key': os.environ['PAYCAN_API_SECRET']}, # backend only!
json={
'user_id': str(user.id),
'user': {'name': user.get_full_name(), 'email': user.email},
},
)
# return ONLY the token to the browser
return JsonResponse({'token': response.json()['token']})Initialize and sell
Frontend · browserPoint the SDK at your PayCan instance, fetch a token from the endpoint you built in Step 2, and open a checkout with one call. Guests can check out too — no token required.
import { PayCan } from '@paycan/sdk';
// apiUrl = your PayCan instance (where the payment platform is hosted)
const paycan = new PayCan({ apiUrl: 'https://pay.yourapp.com' });
// The token comes from YOUR backend — the endpoint you built in Step 2.
// The browser only ever sees this short-lived user token, never the secret key.
// Optional: without it, checkouts still work as guest (email is asked instead).
const { token } = await (await fetch('/api/paycan/token')).json();
paycan.setUserToken(token);
// One line to sell something
paycan.openCheckoutModal('prod_pro', {
onSuccess: (checkoutUrl) => (window.location.href = checkoutUrl),
});fetch('/api/paycan/token') calls your own backend (Step 2), which authenticates
the user via your normal session, then asks PayCan for a user-scoped token using the secret key.
The flow is: browser → your backend → PayCan → your backend → browser.
The token only grants access to that user's own data and refreshes automatically before it expires.
PayCan instance once, at app startup.
Call setUserToken() right after your login flow succeeds — or on page load when the
user already has a session. It must run before any account action (orders, subscriptions,
transactions); doing it eagerly at login is better than lazily before a payment, so the token is
ready the moment the user clicks "Buy". If they aren't signed in, skip it — the checkout modal
falls back to guest mode and asks for an email instead.
Drop-in UI — try each one live Frontend · browser
Framework-agnostic modals rendered in a Shadow DOM, so your page styles are never affected.
Fully responsive, with light, dark and auto themes.
CheckoutModal
guest OKPrice + gateway selection and checkout session creation for a product. Shows an email field automatically for guests.
paycan.openCheckoutModal('prod_pro', {
theme: 'auto',
onSuccess: (url) => (location.href = url),
});ProductsModal
guest OKBrowse products or plans and jump straight into checkout. Filter by type: subscription, digital, service, physical.
paycan.openProductsModal({
type: 'subscription', // optional filter
theme: 'auto',
});SubscriptionsModal
authList, cancel, resume and change plans. Try cancelling the active plan — the mock data updates live.
import { SubscriptionsModal } from '@paycan/sdk';
new SubscriptionsModal(paycan, { theme: 'auto' }).open();OrdersModal
authOrder history with status, totals and gateway. Complete a demo checkout first and your new order appears here.
import { OrdersModal } from '@paycan/sdk';
new OrdersModal(paycan, { theme: 'auto' }).open();TransactionsModal
authPayment history — charges and refunds across all gateways, linked to their orders.
import { TransactionsModal } from '@paycan/sdk';
new TransactionsModal(paycan, { theme: 'auto' }).open();Prefer your own UI? Use the API client Frontend · browser
Every modal above is built on the same typed API client you get with the SDK — filtering, sorting, includes and pagination included. Click a call to run it against the mock API.
const products = await paycan.products.list({ include: 'prices' });const orders = await paycan.orders.list({ per_page: 5, sort: '-created_at' });const active = await paycan.subscriptions.listActive();
const hasPremium = active.some((sub) => sub.product?.name === 'Pro Plan');const preview = await paycan.checkout.preview({
product_id: 'prod_pro',
quantity: 2,
});const session = await paycan.checkout.create({
product_id: 'prod_ebook',
product_price_id: 'price_ebook',
gateway: 'stripe',
billing_email: 'guest@example.com',
});
// then: window.location.href = session.checkout_url;// Click "Run" to execute the selected call against the mock API
Did this user buy it? Check access in one call Frontend · browser
The most common thing to build right after checkout: gate content behind a purchase.
One-time products (e-books, services, physical goods) are unlocked by a completed order;
plans are unlocked by an active subscription. Both checks are cheap enough to run on every page load.
async function ownsProduct(productId) {
const { data: orders } = await paycan.orders.list({
filter: { status: 'completed' },
include: 'product,productPrice',
});
return orders.some((order) => order.product?.id === productId);
}
if (await ownsProduct('prod_ebook')) {
unlockDownload();
} else {
showBuyButton();
}async function hasActivePlan(productId) {
const active = await paycan.subscriptions.listActive();
return active.some((sub) => sub.product?.id === productId);
}
if (await hasActivePlan('prod_pro')) {
showPremiumContent();
} else {
showUpgradeModal();
}PayCan is open source — and stars keep it moving ⭐
If this demo saved you an afternoon of payment plumbing, a GitHub star is the single most helpful thing you can do. It's how new developers find the project and how we prioritize the roadmap.
PayCan