Guides
Members
Build a public apply form

Build a public apply form

Requires: A publishable key with READ_PUBLIC and WRITE_APPLICATIONS for visitors; a secret key with MEMBER_AUTH and the member's session for signed-in members.

This guide builds a "Join" page on your site: the tiers, the form each tier asks, and the submit, for visitors and for members who are already signed in.

GET  /api/v1/communities/:communityTag/segments/public
GET  /api/v1/communities/:communityTag/segments/:segmentId/form
GET  /api/v1/communities/:communityTag/segments/:segmentId/check-email
POST /api/v1/communities/:communityTag/segments/:segmentId/submit
POST /api/v1/communities/:communityTag/apply/checkout
POST /api/v1/communities/:communityTag/me/segments/:segmentId/apply
GET  /api/v1/communities/:communityTag/me/applications
POST /api/v1/communities/:communityTag/me/applications/:requestId/checkout

Reads need READ_PUBLIC, the submit needs WRITE_APPLICATIONS; both come with the publishable key. The me/* routes need a secret key with MEMBER_AUTH and the member's session.

1. The tiers

const tiers = await cobuntu.segments.public();
// [{ id: "seg_pro", name: "Pro", description, color, hasForm: true,
//    priceMonthly: 900, priceYearly: 9000, priceOneTime: null, currency: "EUR", ... }]

Prices are per cadence, in cents; null means that cadence is not offered. A tier with every price null is free. Hidden (invite-only) tiers are not listed.

2. The form

const form = await cobuntu.segments.form("seg_pro");
// { segmentId, title, description, formSource, fields: [{ id: "f_city", type: "TEXT", label: "City", required: true }] }

Read the fields rather than hard-coding them: the form changes when a leader edits it. A hidden tier's form is served only with the invitation:

const form = await cobuntu.segments.form("seg_founders", { invitationToken });

Without a valid token a hidden tier answers 404, the same as a tier that does not exist.

3. Check the email first

const { status } = await cobuntu.segments.checkEmail("seg_pro", email);
statusDo
newContinue with the form.
has_accountThey have a Cobuntu account: offer to sign in, then use me.segments.apply.
existing_memberAlready a member: send them to sign in.
pending_applicationThey applied already: tell them it is under review.

4. Submit

const r = await cobuntu.segments.submit("seg_pro", {
  answers: { email, name, f_city: "Lisbon" },
  billingCycle: "MONTHLY",
  source: "your_site",
  // invitationToken, for a hidden tier
});

Then branch on the answer:

if (r.paymentRequired) {
  const { checkoutUrl } = await cobuntu.apply.checkout({
    requestId: r.requestId,
    successUrl: returnUrl(origin, "/join/thanks"),
    cancelUrl: returnUrl(origin, "/join"),
  });
  return redirect(checkoutUrl);
}
if (r.awaitingEmailConfirmation) return show("Check your inbox to confirm your email.");
if (r.autoApproved) return show("Welcome in. Sign in to continue.");
return show("Thanks, your application is under review.");
  • awaitingEmailConfirmation: a publishable key cannot prove the applicant owns the address, so a submit that would be admitted at once stays pending until they click the emailed link (valid 48 hours). Submit from your server with a secret key to skip it: you vouch for the address. See Security model.
  • paymentRequired: a paid tier. The application is held until payment. successUrl and cancelUrl must be both or neither, on the community's site.
  • source labels where the application came from in the community's review list.
  • Every hidden-tier failure answers the same 404 "Segment not found".

Submits are limited to 10 per hour per IP.

Signed-in members

A member who is signed in on your site applies with their session instead, no email step:

const member = cobuntu.as(session);
const r = await member.me.segments.apply("seg_pro", { answers, billingCycle: "MONTHLY" });
 
switch (r.status) {
  case "JOINED": return show("You are in.");
  case "PENDING": return show("Your application is under review.");
  case "PAYMENT_REQUIRED": {
    const { checkoutUrl } = await member.me.applications.checkout(r.requestId, { successUrl, cancelUrl });
    return redirect(checkoutUrl);
  }
}

me.applications.list() returns the member's applications, newest first, each with its status (PENDING, PAYMENT_PENDING, RESERVED, APPROVED, REJECTED). Offer "Complete payment" for PAYMENT_PENDING with me.applications.checkout(requestId, { successUrl, cancelUrl }).