Guides
Content
Show community content

Show community content

Requires: A publishable key with READ_PUBLIC for reads; a secret key with MEMBER_AUTH (and the member's session) for onboarding, WRITE_SALES for donations.

The reads a site makes around its events and shop: who the community is, who is in it, what it teaches and writes, and the onboarding a new member still owes. Everything here is READ_PUBLIC unless noted, so the publishable key works for the reads, and a server with the secret key works for all of it.

GET  /api/v1/communities/:communityTag/branding
GET  /api/v1/communities/:communityTag/members
GET  /api/v1/communities/:communityTag/members/:userId
GET  /api/v1/communities/:communityTag/members/:userId/membership
GET  /api/v1/communities/:communityTag/courses
GET  /api/v1/communities/:communityTag/articles
GET  /api/v1/communities/:communityTag/rules
GET  /api/v1/communities/:communityTag/places/cities
GET  /api/v1/communities/:communityTag/me/onboarding
POST /api/v1/communities/:communityTag/me/onboarding/data
POST /api/v1/communities/:communityTag/me/onboarding/rules
POST /api/v1/communities/:communityTag/products/:sku/donate
GET  /api/v1/communities/:communityTag/products/:sku/donations/summary

Look like the community

const branding = await cobuntu.branding();
// { name: "Acme", iconUrl: "https://...", theme: { ... }, communityUrl: "https://your-community.cobuntu.com" }

theme is the community's own theme document, passed through as stored. Read the tokens you know (colours, fonts) and ignore the rest; it is {} when the community has none. Branding is public by nature, so cache it:

const branding = await cobuntu.branding({ next: { revalidate: 3600, tags: ["branding"] } });

Show the members

const { members, pagination } = await cobuntu.members.list({ limit: 24, offset: 0 });
const alex = await cobuntu.members.get("alex"); // user id or usertag

Each Member carries name, usertag, profileImage, bio, pronouns, roleGroups, joinedAt, friendsCount and a profileUrl on the community app. These reads follow the community's Members page setting exactly as an anonymous visitor sees it:

Members pageWhat you get
PublicThe roster, without members who chose to hide from the directory. A hidden member's get is 404.
Members only403 MEMBERS_ONLY. For a signed-in member, use cobuntu.as(session).me.directory().
Switched off404 PAGE_DISABLED. Hide the section.
try {
  return await cobuntu.members.list();
} catch (e) {
  if (e instanceof CobuntuError && (e.code === "MEMBERS_ONLY" || e.code === "PAGE_DISABLED")) return null;
  throw e;
}

Before granting anything privileged to a person, check their standing with members.membership(userId) rather than trusting what your own session says.

Courses

const { courses, pagination } = await cobuntu.courses.list({ category, limit: 12 });
const course = await cobuntu.products.get(courses[0].id); // the detail is a product

Courses use their own taxonomy (categories({ type: "COURSE" })) and only public ones are listed. The list is 404 NOT_FOUND when the community does not run courses, so treat that as "no courses section".

Articles and rules

const { articles, pagination } = await cobuntu.articles.list({ limit: 10 });
const article = await cobuntu.articles.get("welcome-to-acme"); // or null
const rules = await cobuntu.rules(); // CommunityRule[], in display order

articles.get pages through the published list, so cache it. Each rule has a title, an optional description, and an optional linkUrl / linkLabel to a document the member must be able to open before accepting.

Onboarding a signed-in member

After sign-up the community may still need the rules accepted and a member data form filled. The read returns everything needed to draw the pending step:

const member = cobuntu.as(session);
const ob = await member.me.onboarding.get();
if (!ob.complete && ob.nextStep === "rules") {
  // render ob.rules, then:
  await member.me.onboarding.rules();
}
if (!ob.complete && ob.nextStep === "data" && ob.form) {
  await member.me.onboarding.data({ segmentId: ob.form.segmentId, answers: { [fieldId]: "Lisbon" } });
}

These need the secret key with MEMBER_AUTH and the member's session. Answers are validated against the form: an unknown field, a missing required field or a value outside a field's options is 400 INVALID_INPUT, and a tier whose form lives elsewhere is 400 FORM_NOT_NATIVE. Render every rule (and make each linked document reachable) before you call rules(): it records what the member was shown.

City autocomplete

const cities = await cobuntu.places.cities({ q: input, lang: "en" });
// [{ placeId, description: "Lisbon, Portugal", mainText: "Lisbon", secondaryText: "Portugal", countryCode: "PT" }]

Use it for the location field on a profile or sign-up form. It never fails a form: problems come back as [], a query under 2 characters answers [], and you get at most 6 suggestions. It is limited to 60 lookups a minute per visitor IP, so debounce keystrokes (about 250 ms) and pass endUserIp when you proxy it through your server. Save description (or mainText) into the profile's location with me.profile.update.

Support a maker

A donation to a product, without buying it:

const { checkoutUrl } = await cobuntu.products.donate("acme-notebook", {
  donation: { amount: 1000 }, // cents
  customer: { email },         // required without a member session
  successUrl: returnUrl(origin, "/thanks"),
  cancelUrl: returnUrl(origin, "/marketplace/acme-notebook"),
});
// redirect to checkoutUrl
 
const { total, count, currency } = await cobuntu.products.donationsSummary("acme-notebook");

Donating needs WRITE_SALES, which the default publishable key does not carry: call it from your server. Branch on INVALID_DONATION_AMOUNT; a missing email or return URL is a 400 without a code. The summary carries no donor identity. Event donations work the same way through events.donate (see Events on your site).