Skip to content

Express

Express integrations usually start with a small request helper. The goal is to resolve customerId once and keep route code focused on product behavior.

Terminal window
npm install @guapocado/sdk express
npm install --save-dev @types/express
import { createGuapocadoClient } from "@guapocado/sdk";
import express from "express";
const app = express();
app.use(express.json());
function createBilling(customerId: string) {
return createGuapocadoClient({
apiKey: process.env.GUAPOCADO_API_KEY!,
customerId,
});
}
app.get("/features/:feature", async (req, res, next) => {
try {
const customerId = String(req.query.customerId ?? "");
if (!customerId) return res.status(400).json({ error: "customerId required" });
const guap = createBilling(customerId);
const hasAccess = await guap.has(req.params.feature);
return res.json({ feature: req.params.feature, hasAccess });
} catch (error) {
next(error);
}
});

In real apps, do not read the customer from a query string. Resolve it from auth or your active organization middleware.

import type { GuapocadoClient } from "@guapocado/sdk";
import { createGuapocadoClient } from "@guapocado/sdk";
import type { RequestHandler } from "express";
declare global {
namespace Express {
interface Request {
customerId?: string;
guap?: GuapocadoClient;
}
}
}
export const attachBilling: RequestHandler = (req, res, next) => {
const orgId = req.header("x-active-org-id");
if (!orgId) return res.status(401).json({ error: "organization required" });
req.customerId = `org_${orgId}`;
req.guap = createGuapocadoClient({
apiKey: process.env.GUAPOCADO_API_KEY!,
customerId: req.customerId,
});
next();
};

Then use it on protected routes:

app.post("/reports", attachBilling, async (req, res, next) => {
try {
if (!(await req.guap!.has("advanced-analytics"))) {
return res.status(403).json({ error: "Upgrade required" });
}
const report = await createReport(req.customerId!);
return res.json({ report });
} catch (error) {
next(error);
}
});
app.post("/ai/summarize", attachBilling, async (req, res, next) => {
try {
const input = String(req.body.text ?? "");
const credits = Math.ceil(input.length / 4);
await req.guap!.usage.consume("ai-credits", credits);
try {
const summary = await summarize(input);
return res.json({ summary, credits });
} catch (error) {
await req.guap!.usage.refund("ai-credits", credits);
throw error;
}
} catch (error) {
next(error);
}
});
app.post("/billing/checkout/:productKey", attachBilling, async (req, res, next) => {
try {
await req.guap!.customers.create({
id: req.customerId,
email: req.header("x-user-email") ?? undefined,
});
const checkout = await req.guap!.checkout.create({
productKey: req.params.productKey,
successUrl: `${process.env.APP_URL}/billing/success`,
cancelUrl: `${process.env.APP_URL}/billing`,
});
return res.redirect(checkout.url);
} catch (error) {
next(error);
}
});

The SDK throws typed errors for failed Guapocado API responses:

import {
GuapocadoAuthError,
GuapocadoError,
GuapocadoRateLimitError,
} from "@guapocado/sdk";
app.use((error, req, res, next) => {
if (error instanceof GuapocadoRateLimitError) {
return res.status(429).json({ error: error.message });
}
if (error instanceof GuapocadoAuthError) {
return res.status(401).json({ error: error.message });
}
if (error instanceof GuapocadoError) {
return res.status(error.status).json({ error: error.message });
}
return res.status(500).json({ error: "Internal server error" });
});