JavaScript client
Package: @cobuntu/embed.
A small, typed client for the public API v1. It mirrors the API one-for-one:
every method is one route under /api/v1/communities/:tag/..., with the same
body and the same answer. It has no runtime dependencies and uses the global
fetch (Node 18+, Next.js, Bun, Deno, Cloudflare Workers).
Server only. The keys this client is built for (
MEMBER_AUTH,WRITE_SALES) can sign members in and take payments. Use it from route handlers, server actions or API routes. It refuses to start in a browser.
Install
npm install @cobuntu/embedSetup
// lib/cobuntu.ts (server only)
import { createCobuntu } from "@cobuntu/embed";
export const cobuntu = createCobuntu({
apiKey: process.env.COBUNTU_API_KEY!, // secret key, server env only
community: "your-community", // your community tag
// baseUrl: "https://api.cobuntu.com", // default
// fetch: customFetch, // optional
});| Option | Required | Notes |
|---|---|---|
apiKey | yes | A community key (ck_...). The scopes it holds decide which methods work. |
community | yes | The community tag. |
baseUrl | no | Default https://api.cobuntu.com. |
fetch | no | Defaults to the global fetch. |
allowBrowser | no | Skips the browser guard. Only ever with the publishable key. |
Every call sends X-API-Key and asks for cache: "no-store", so Next.js
never caches a visitor's answer.
Caching (Next.js)
By default every request is sent with cache: "no-store". For public reads
you can opt into Next.js caching, per client or per call (per call wins):
const cobuntu = createCobuntu({
apiKey: process.env.COBUNTU_API_KEY!,
community: "your-community",
fetchOptions: { next: { revalidate: 300 } }, // every call
});
await cobuntu.events.list({}, { next: { revalidate: 60, tags: ["events"] } }); // this call
await cobuntu.rules({ cache: "no-store" }); // opt back outnext is typed loosely, so the package does not depend on next; other
runtimes ignore it. Calls on a client.as(session) client are always
no-store and drop next: they return one member's data (prices, me.*),
and a cache keyed by URL would hand it to the next visitor.
Server only
createCobuntu calls assertServerOnly(), which throws when window and
document exist. Publishable and secret keys both start with ck_, so the
guard can't tell them apart: it refuses any key in a browser unless you pass
allowBrowser: true. You can also call assertServerOnly() yourself at the
top of a module that must never be bundled for the client.
Sessions
Member sign-in returns a 30-day embed session token, bound to this one
community (it is refused by every other Cobuntu API; see
Security model). Store it in
your own httpOnly cookie and act for the member with client.as():
const { token, user, newMember } = await cobuntu.auth.signIn({ email, password });
// set-cookie: cobuntu_session=<token>; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000
const member = cobuntu.as(token); // sends X-Cobuntu-Auth-Token
const tickets = await member.me.tickets();
const event = await member.events.get("supper-club"); // memberPrice now filled inas() returns a new client. The original stays anonymous.
Continue with Google
Google sign-in uses the same Cobuntu account as the community app. It is two server routes on your site plus a PKCE pair:
import { createPkcePair } from "@cobuntu/embed";
// GET /api/auth/google/start
const { codeVerifier, codeChallenge } = await createPkcePair();
const state = (await createPkcePair()).codeVerifier; // any random value
// set-cookie: google_oauth=<state>.<codeVerifier>; HttpOnly; Secure; SameSite=Lax; Max-Age=600
return redirect(cobuntu.auth.googleStartUrl({
returnUrl: `${origin}/api/auth/google/callback`,
codeChallenge,
state,
}));
// GET /api/auth/google/callback?code=&state= (or ?error=)
// 1. compare `state` with the cookie, then clear the cookie
// 2. on ?error= (NOT_MEMBER, APPLICATION_PENDING, account_suspended) show it on your login page
const { token, user, isSignUp } = await cobuntu.auth.exchangeOAuthCode({ code, codeVerifier });
// set the session cookie exactly as after signIn()returnUrlmust be on the community's embed site origin.http://localhostandhttp://127.0.0.1on any port are accepted for local development.- The
codelives 60 seconds and works once. The exchange needs the secret key and the verifier, so a code copied from the address bar is useless. - No membership is created. Who may sign in follows the community's access rules, the same as Google sign-in in the community app.
The visitor's IP
Every call comes from your one server, so Cobuntu can't see who the visitor is.
Rate limits that are per person (sign-up) read X-End-User-IP. Every method
takes a last options argument:
await cobuntu.auth.signUp({ name, email, password }, { endUserIp: visitorIp });options.signal (an AbortSignal) is accepted too.
Errors
Every non-2xx answer throws a CobuntuError:
import { CobuntuError } from "@cobuntu/embed";
try {
await cobuntu.events.checkout("supper-club", { tierId, successUrl, cancelUrl });
} catch (e) {
if (e instanceof CobuntuError) {
e.status; // 409
e.code; // "TIER_FULL"
e.message; // human text, do not branch on it
e.details; // the rest of the body, e.g. { missingFields: ["Diet"] }
}
}Branch on code. When the API sends no code (the API-key middleware,
some older routes), the client derives one from the status: BAD_REQUEST,
UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED,
INTERNAL. A request that never got an answer is status: 0,
code: "NETWORK_ERROR". The CobuntuErrorCode type lists every documented
code for autocomplete.
Helpers
| Helper | Does |
|---|---|
formatMoney(cents, currency, locale?) | formatMoney(2500, "EUR", "pt-PT") gives 25,00 €. Every amount in the API is in cents. |
toCents(amount) | 12.5 to 1250. |
isOnSale(tier) | The tier's schedule is open and it isn't sold out. |
returnUrl(origin, path, params?) | An absolute successUrl / cancelUrl / returnUrl on your site. Keeps {CHECKOUT_SESSION_ID} intact for Stripe and refuses a path that leaves the origin. |
assertServerOnly() | Throws in a browser. |
createPkcePair() | Promise<{ codeVerifier, codeChallenge }> (S256) for Continue with Google and Connect Google. Server side. |
buildDirectoryQuery(input) | Form values or URLSearchParams to me.directory() params: trims, drops empty values and unknown keys. |
matchesMemberSearch(member, q) | The directory search, for refining a loaded page in the browser: every term must match, case- and accent-insensitive. |
Methods by area
Paths are below /api/v1/communities/:tag. "Session" means the method needs
client.as(token).
Routes the client does not wrap
Operator and app-platform routes (tier edits, broadcasts, article writes,
host refunds, subscription admin, event edits, rules acceptance by email, the
app handoff, member tokens for apps, per-member app data, notifications, feed
posts, the event bridge, the map) are called with fetch and a secret
key. They are documented on Server and app platform and
the other Endpoints pages, with full schemas in the
API reference.