Make sure your API key is only used server side. It should never be used client side.
Be sure to read Linking Stripe Customers to learn how to link Stripe customers to your own internal ID.
Install
Create an API key in the PriceOS dashboard and add to your environment variables.
.env
PRICEOS_API_KEY=pos_ab234cdef...
npm i priceos
pnpm add priceos
yarn add priceos
bun add priceos
Add server side handlers
- Next.js
- Other backends
// app/api/priceos/[...path]/route.ts
import { priceosHandler } from "priceos/next";
export const { GET, POST } = priceosHandler({
identifyCustomer: async (req: Request) => {
// Return a customer identity from your server
},
});
// pages/api/priceos/[...path].ts
import { priceosPagesHandler, type PriceOSPagesRequest } from "priceos/next";
export default priceosPagesHandler({
identifyCustomer: async (req: PriceOSPagesRequest) => {
// Return a customer identity from your server
},
});
If you need to include an auth token from the client, you can do so on the Provider.
We are working on adding more helpers for other backends. If there’s a backend you would like us to create helpers for, please reach out and let us know.
| Hook | Endpoint(s) you must add |
|---|---|
useCustomer* | GET /priceos/v1/customer |
useFeatureAccess | GET /priceos/v1/feature-access |
useTrackUsage | POST /priceos/v1/usage |
useUsageEvents | GET /priceos/v1/usage-events |
useCheckout | POST /priceos/v1/checkout |
useCustomerPortal | POST /priceos/v1/customer-portal |
* If you use
trackUsage from useCustomer, add POST /priceos/v1/usage. If you use checkout from useCustomer, add POST /priceos/v1/checkout.Make sure you resolve
customerId from your server-side auth/session. If you need to include an auth token from the client, you can do so on the Provider.
useCustomer()
Requires aGET /priceos/v1/customer endpoint.app.get("/priceos/v1/customer", async (req, res) => {
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.customers.get(customerId);
res.json(data);
});
app.get("/priceos/v1/customer", async (request, reply) => {
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS(`/v1/customers/${encodeURIComponent(customerId)}`);
reply.send(await upstream.json());
});
@app.get("/priceos/v1/customer")
async def get_customer(request: Request):
customer_id = await identify_customer_from_session(request)
return await call_priceos("GET", f"/v1/customers/{customer_id}")
useFeatureAccess()
Requires aGET /priceos/v1/feature-access endpoint.app.get("/priceos/v1/feature-access", async (req, res) => {
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.features.getAccess(customerId);
res.json(data);
});
app.get("/priceos/v1/feature-access", async (request, reply) => {
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS(
`/v1/feature-access?customerId=${encodeURIComponent(customerId)}`
);
reply.send(await upstream.json());
});
@app.get("/priceos/v1/feature-access")
async def get_feature_access(request: Request):
customer_id = await identify_customer_from_session(request)
return await call_priceos("GET", f"/v1/feature-access?customerId={customer_id}")
useTrackUsage()
Requires aPOST /priceos/v1/usage endpoint.app.post("/priceos/v1/usage", async (req, res) => {
const featureKey = req.body?.featureKey;
const amount = req.body?.amount ?? 1;
if (amount <= 0) // Return an error
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.usage.track({
customerId,
featureKey,
amount,
eventKey: req.body.eventKey,
occurredAt: req.body.occurredAt,
metadata: req.body.metadata,
});
res.json(data);
});
app.post("/priceos/v1/usage", async (request, reply) => {
const body = request.body as any;
const featureKey = body?.featureKey;
const amount = body?.amount ?? 1;
if (amount <= 0) // Return an error
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS("/v1/usage", {
method: "POST",
body: JSON.stringify({
customerId,
featureKey,
amount,
eventKey: body.eventKey,
occurredAt: body.occurredAt,
metadata: body.metadata,
}),
});
reply.send(await upstream.json());
});
@app.post("/priceos/v1/usage")
async def track_usage(request: Request):
body = await request.json()
feature_key = body.get("featureKey")
amount = body.get("amount", 1)
if amount <= 0:
pass # Return an error
customer_id = await identify_customer_from_session(request)
return await call_priceos(
"POST",
"/v1/usage",
{
"customerId": customer_id,
"featureKey": feature_key,
"amount": amount,
"eventKey": body.get("eventKey"),
"occurredAt": body.get("occurredAt"),
"metadata": body.get("metadata"),
},
)
Don’t allow negative numbers from client for security reasons.
useUsageEvents()
Requires aGET /priceos/v1/usage-events endpoint.app.get("/priceos/v1/usage-events", async (req, res) => {
const featureKey = req.query.featureKey
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.usage.listEvents({
customerId,
featureKey,
period: req.query.period,
offset: req.query.offset,
limit: req.query.limit,
customRange:
req.query.start && req.query.end
? { start: req.query.start, end: req.query.end }
: undefined,
});
res.json(data);
});
app.get("/priceos/v1/usage-events", async (request, reply) => {
const query = request.query as any;
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS("/v1/usage/events", {
method: "POST",
body: JSON.stringify({
customerId,
featureKey: query.featureKey,
period: query.period,
offset: query.offset,
limit: query.limit,
customRange:
query.start && query.end
? { start: query.start, end: query.end }
: undefined,
}),
});
reply.send(await upstream.json());
});
@app.get("/priceos/v1/usage-events")
async def list_usage_events(request: Request):
q = request.query_params
customer_id = await identify_customer_from_session(request)
return await call_priceos(
"POST",
"/v1/usage/events",
{
"customerId": customer_id,
"featureKey": q.get("featureKey"),
"period": q.get("period"),
"offset": q.get("offset"),
"limit": q.get("limit"),
"customRange": {"start": q.get("start"), "end": q.get("end")}
if q.get("start") and q.get("end")
else None,
},
)
useCheckout()
Requires aPOST /priceos/v1/checkout endpoint.app.post("/priceos/v1/checkout", async (req, res) => {
const body = req.body ?? {};
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.customers.createCheckout(customerId, {
productKey: body.productKey,
stripePriceId: body.stripePriceId,
successUrl: body.successUrl,
cancelUrl: body.cancelUrl,
metadata: body.metadata,
customerInfo: body.customerInfo,
checkoutParams: body.checkoutParams,
});
res.json(data);
});
app.post("/priceos/v1/checkout", async (request, reply) => {
const body = request.body as any;
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS(`/v1/customers/${encodeURIComponent(customerId)}/checkout`, {
method: "POST",
body: JSON.stringify({
productKey: body.productKey,
stripePriceId: body.stripePriceId,
successUrl: body.successUrl,
cancelUrl: body.cancelUrl,
metadata: body.metadata,
customerInfo: body.customerInfo,
checkoutParams: body.checkoutParams,
}),
});
reply.send(await upstream.json());
});
@app.post("/priceos/v1/checkout")
async def checkout(request: Request):
body = await request.json()
customer_id = await identify_customer_from_session(request)
return await call_priceos(
"POST",
f"/v1/customers/{customer_id}/checkout",
{
"productKey": body.get("productKey"),
"stripePriceId": body.get("stripePriceId"),
"successUrl": body.get("successUrl"),
"cancelUrl": body.get("cancelUrl"),
"metadata": body.get("metadata"),
"customerInfo": body.get("customerInfo"),
"checkoutParams": body.get("checkoutParams"),
},
)
useCustomerPortal()
Requires aPOST /priceos/v1/customer-portal endpoint.app.post("/priceos/v1/customer-portal", async (req, res) => {
const customerId = await identifyCustomerFromSession(req);
const data = await priceos.customers.createPortal(customerId);
res.json(data);
});
app.post("/priceos/v1/customer-portal", async (request, reply) => {
const customerId = await identifyCustomerFromSession(request);
const upstream = await callPriceOS(
`/v1/customers/${encodeURIComponent(customerId)}/customer_portal`,
{ method: "POST" }
);
reply.send(await upstream.json());
});
@app.post("/priceos/v1/customer-portal")
async def customer_portal(request: Request):
customer_id = await identify_customer_from_session(request)
return await call_priceos("POST", f"/v1/customers/{customer_id}/customer_portal")
Add provider component
// layout.tsx or wherever your providers are
import { PriceOSProvider } from "priceos/react";
<PriceOSProvider>
{/* children */}
</PriceOSProvider>;
import { PriceOSProvider } from "priceos/react";
<PriceOSProvider backendUrl="https://api.yourapp.com/priceos">
{/* app */}
</PriceOSProvider>;
Generate types (optional, but highly recommended)
npx priceos generate-types
pnpm dlx priceos generate-types
yarn dlx priceos generate-types
bunx priceos generate-types
--help to see more configuration options.Remember to regenerate types any time you create a new feature.
Read feature access
import { useFeatureAccess } from "priceos/react";
import type { MyFeatures } from "./priceos.types";
export function PrioritySupportButton() {
const { hasAccess } = useFeatureAccess<MyFeatures>("priority_support");
const onClick = () => {
if (hasAccess){
// Allow access
}
}
return <button onClick={onClick}>Contact support</button>;
}
import { useFeatureAccess } from "priceos/react";
import type { MyFeatures } from "./priceos.types";
export function AddTeamSeatsButton() {
const { hasAccess, usage } = useFeatureAccess<MyFeatures>("team_seats");
const onClick = () => {
const hasSeatAvailable = hasAccess && usage?.hasReachedLimit !== true;
if (hasSeatAvailable) {
// Allow access
}
};
return <button onClick={onClick}>Add teammate</button>;
}
Track usage (optional)
Only if you’re using PriceOS to track your customers’ usage for limit features.For more info see Tracking usage.
import { useTrackUsage } from "priceos/react";
import type { MyFeatures } from "./priceos.types";
export function SeatButton() {
const { trackUsage } = useTrackUsage<MyFeatures>();
async function handleClick() {
// Add teammate first
await trackUsage({
featureKey: "team_seats",
amount: 1,
eventKey: `add_team_seat_${Date.now()}`,
});
}
return <button onClick={handleClick}>Add teammate</button>;
}
If the action happens on your backend (for example in a server action, webhook, or background job), it usually makes more sense to track usage on the backend instead.
useTrackUsage with the built-in Next.js handlers (priceos/next) only accepts positive amount values (so that clients can’t reverse their usage). For negative adjustments (for example when removing a team seat), track usage from your backend via the Node.js SDK or REST API endpoints.