Guide

Manage memberships

Sell a recurring membership to a customer, collect a direct-debit mandate, charge the joining fee, and keep the lifecycle clean as payments succeed, fail, and the member eventually cancels. This guide covers both paths into a membership — direct creation and the basket- backed purchase flow — and every state transition you'll need to handle afterwards.

Before you start

  • A personal access token with permission to manage memberships on the target site. See authentication for the header format.
  • At least one configured MembershipType on the site, with one or more MembershipRates. The Trybe back office is the source of truth for type and rate configuration; this guide is about consuming them.
  • A Customer to sign up. The customer must belong to the same brand as the membership type. If you don't have one yet, use createCustomer first or pass customer_data inline (see step 2).
  • A Stripe-configured site if you're collecting direct-debit mandates through Stripe Checkout. Mandates and joining-fee checkout sessions are Stripe-backed; sites without a Stripe configuration must use the manual-payment path instead.
  • A webhook configuration listening for membership.confirmed, membership.charged, membership.cancelled, and membership.expired. See the webhooks guide.

How a membership flows through the API

A Membership always belongs to one Customer (the lead member), is attached to one MembershipType and one MembershipRate, and lives at one Site. Its lifecycle has five states you'll encounter:

  1. reserved — created but not yet paid for. New memberships always land here, regardless of whether you created them directly or through a basket.
  2. needs_dd_mandate — confirmation has been requested but a direct- debit mandate hasn't yet been collected. The customer must complete Stripe Checkout to advance.
  3. needs_attention — something requires operator intervention. The most common attention_reason is payment_failed; another is chargeback notification.
  4. active — fully signed up, mandate (if required) is in place, billing is running on schedule.
  5. cancelled / expired — terminal states. cancelled is the result of explicit cancellation; expired is reached when an end-dated membership runs out the clock.

Each state corresponds to webhook events that fire on entry: membership.confirmed when the membership reaches active, membership.charged for each recurring payment, membership.cancelled on explicit cancellation, and membership.expired when an end-date membership runs out. Hook into these to drive your downstream systems (welcome emails, churn lists, access control).

1. Discover the available types

Surface the membership types configured for the site so the customer can pick one. listMembershipTypes returns the catalogue filtered by brand or site:

curl "https://api.playground.try.be/customers/membership-types?site_id=00000000-0000-0000-0000-111111111111" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"
const res = await fetch(
  'https://api.playground.try.be/customers/membership-types?' +
    new URLSearchParams({
      site_id: '00000000-0000-0000-0000-111111111111',
    }),
  {
    headers: { Authorization: `Bearer ${process.env.TRYBE_API_KEY}` },
  },
)
const { data: types } = await res.json()

The response includes the embedded rates for each type — the different prices and billing frequencies a customer can choose between (monthly, annual, family, single, etc.). The combination of membership_type_id and membership_rate_id is what every subsequent call needs.

If the type is configured as private, or the rate is non-default, the caller must supply a valid MembershipSignupToken to sign somebody up to it. The token is generated through the back-office self-signup flow. See listMembershipTypes.

2. Sign the customer up

There are two paths into a new membership. Pick based on whether the customer needs to pay anything now — a joining fee, a pro-rata payment — or just needs to sign a mandate for future billing.

Path A: direct creation

For free-to-join memberships, monthly-charge-from-day-one memberships, and any signup where there is no up-front basket to settle, create the membership directly. The customer goes straight from reserved to needs_dd_mandate (or active if no mandate is required) as soon as you confirm:

curl https://api.playground.try.be/customers/memberships \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "5f8a1b2c-9d3e-4a5b-8c6d-7e8f9a0b1c2d",
    "site_id": "00000000-0000-0000-0000-111111111111",
    "membership_type_id": "00000000-0000-0000-0000-222222222222",
    "membership_rate_id": "00000000-0000-0000-0000-333333333333",
    "start_date": "2026-06-01",
    "end_date": null
  }'
$response = Http::asJson()
    ->withToken(config('services.trybe.token'))
    ->post('https://api.playground.try.be/customers/memberships', [
        'customer_id'        => '5f8a1b2c-9d3e-4a5b-8c6d-7e8f9a0b1c2d',
        'site_id'            => '00000000-0000-0000-0000-111111111111',
        'membership_type_id' => '00000000-0000-0000-0000-222222222222',
        'membership_rate_id' => '00000000-0000-0000-0000-333333333333',
        'start_date'         => '2026-06-01',
        'end_date'           => null,
    ]);

$membership = $response->json('data');

You can attach the membership to a brand-new customer inline by passing customer_data instead of customer_id; it accepts the same shape as createCustomer. Pass null for end_date to create an evergreen membership that runs until cancelled, or an explicit date for a fixed- term contract.

See createMembership.

Path B: basket-backed purchase

When the signup involves taking a payment at the point of sale — a joining fee on a card, a pro-rata payment for the rest of the month — go through the basket flow. createMembershipOrder creates the basket and the membership in one call:

curl https://api.playground.try.be/shop/memberships \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "5f8a1b2c-9d3e-4a5b-8c6d-7e8f9a0b1c2d",
    "site_id": "00000000-0000-0000-0000-111111111111",
    "membership_type_id": "00000000-0000-0000-0000-222222222222",
    "membership_rate_id": "00000000-0000-0000-0000-333333333333",
    "start_date": "2026-06-01",
    "source": "self_signup"
  }'

The response is the new Membership in reserved state along with a basket_id you can drive through the standard checkout flow — see the basket and checkout guide. After the basket settles, call actionConfirmMembership (step 4) to advance the membership state.

See createMembershipOrder.

Picking between A and B

Choose path A when the membership has no joining fee or the joining fee is collected outside Trybe (e.g. an existing recurring direct debit at the customer's bank). Choose path B when the storefront needs a basket to take the joining payment with a card, voucher, or customer credit.

3. Collect the direct-debit mandate

If the rate is billed by direct debit (the common case for monthly memberships), the customer needs to sign a mandate via Stripe Checkout before they can be charged. There are two API touch points.

Create the Stripe Checkout session

When the customer is ready to sign the mandate, create the Checkout session and use the returned stripe_session_id with Stripe.js to redirect them:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/checkout-session/direct-debit" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"
const res = await fetch(
  `https://api.playground.try.be/customers/memberships/${membershipId}/checkout-session/direct-debit`,
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.TRYBE_API_KEY}` },
  },
)

const { data } = await res.json()
// Pass data.stripe_session_id to Stripe.js

The Stripe page collects the bank details and confirms the mandate; on return, Trybe's webhook handler advances the membership state automatically. You don't need to do anything else server-side.

See createDirectDebitCheckoutSession.

Email the customer a mandate request

For self-signup flows where the storefront isn't capturing payment details in line, you can ask Trybe to email the lead member the mandate request directly. The email contains a link to the same Stripe Checkout session:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/request-mandate" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint returns 400 if the membership doesn't actually require a mandate — it must be in needs_dd_mandate or needs_attention for the email to be sent. See actionRequestMandate.

4. Collect the joining fee

If the rate has a joining fee that must be paid up front (and you're not using the basket-backed path B from step 2), create a one-shot Stripe Checkout session for it:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/checkout-session/joining-fee" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The flow is identical to the mandate-collection flow — pass the returned stripe_session_id to Stripe.js and let Trybe's webhook handlers process the result. See createJoiningFeeCheckoutSession.

5. Confirm the membership

After the up-front payment has been collected and (where required) the mandate has been signed, advance the membership state by calling confirm. This is the call that transitions the membership out of reserved:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/confirm" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint sets the membership to needs_attention if a mandate is still pending, or active if everything is in place. active triggers the membership.confirmed webhook event.

See actionConfirmMembership.

6. Run the recurring billing

Once a membership is active, Trybe runs the billing schedule for you. Charges are generated against the rate's billing frequency (monthly, quarterly, annual) and attempted via the customer's direct-debit mandate. Each successful charge fires membership.charged.

You'll only interact with the billing surface directly in three situations: when a payment has failed and you want to retry; when a payment was taken outside Trybe and you need to record it; and when an operator has changed the rate and you need to regenerate pending charges.

Retry a failed payment

If the most recent charge failed, the membership moves to needs_attention with attention_reason: payment_failed. After the customer has fixed the underlying problem (updated their bank details, topped up the account, etc.), retry the charge:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/retry-payment" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint returns 400 if the membership isn't actually in a retry-eligible state — for example if a charge is already processing or the next billing date is in the future. See actionRetryPayment.

Record a manual payment

When a payment was collected outside Trybe — cash at the counter, a bank transfer, a gift card — record it so the current billing period is marked as paid:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/add-payment" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "processor_type_id": "5f8a1b2c-9d3e-4a5b-8c6d-7e8f9a0b1c2d"
  }'

processor_type_id is the ID of a custom payment-processor configured on the site (cash, bank transfer, gift card). Manual payments are grouped under that processor in revenue reports without ever going through a real card processor. See actionAddManualPayment.

Regenerate pending charges

If an operator has changed the rate or billing schedule, the pending charges may now be wrong. Regenerate them — pending charges are discarded and recreated against the current configuration:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/regenerate-charges" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint is asynchronous; you'll see the new charges appear shortly. Settled charges are never touched. See actionRegenerateMembershipCharges.

7. Issue benefits

Many membership types issue credits to members on a schedule — a monthly allowance, an annual top-up. The schedule runs automatically, but you can also issue credits on demand (e.g. as part of a goodwill gesture or a manual onboarding step):

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/issue-credits" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint queues a background job that issues credits to every member of the membership according to the rate's credit rules; it returns 204 and the credits arrive on the customer's account shortly. See actionIssueMembershipCredits.

8. Manage members

A membership can include additional members beyond the lead — most commonly a family membership covering a spouse and children, or a corporate membership covering several employees.

Add a member

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/members" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "5fbbbbbbbbbbbbbbbbbbbbbb"
  }'

The added customer must belong to the same brand as the membership; cross-brand additions return 422. See createMembershipMember.

Remove a member

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/members/5fbbbbbbbbbbbbbbbbbbbbbb" \
  -X DELETE \
  -H "Authorization: Bearer $TRYBE_API_KEY"

Removing a member doesn't cancel the membership — only the lead member's record affects billing — but it does remove that customer's access to the membership's benefits going forward. See deleteMembershipMember.

List members

listMembershipMembers returns the full list, including the lead member, each member's customer record, and their lead-member status. The membership object's members_count is a denormalised count for list views; use the full list when you need details.

9. Cancel or report a chargeback

End the membership when the customer leaves. actionCancelMembership transitions it to cancelled, stops further billing, and emits membership.cancelled:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/cancel" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

See actionCancelMembership.

If a payment was charged back at the bank and the chargeback is tied to a membership setup fee, mark the membership accordingly. This is typically done by a webhook handler subscribed to your card processor's chargeback notifications:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/report-membership-setup-fee-chargeback" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint updates the status and attention_reason so the operator-facing dashboards reflect the chargeback. See actionReportMembershipFeeChargeback.

10. Resend the confirmation email

If the customer didn't receive (or has lost) the original confirmation email, ask Trybe to resend it:

curl "https://api.playground.try.be/customers/memberships/$MEMBERSHIP_ID/send-confirmation-email" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The endpoint dispatches the email asynchronously and returns 204. See actionSendMembershipConfirmationEmail.

Subscribing to outcomes

Hook into the membership lifecycle via webhooks. The events to subscribe to are:

  • membership.confirmed — fires when a membership reaches active. Use this to send your own welcome email, provision access to a members-only area of your site, etc.
  • membership.charged — fires for each successful recurring payment. Use this to keep your own ledger in sync.
  • membership.cancelled — fires when a membership is cancelled (either via actionCancelMembership or because the rate's cancellation policy was triggered).
  • membership.expired — fires when an end-dated membership runs out the clock without being cancelled or renewed.

All four payloads carry the full Membership shape as returned by getMembership. See the webhooks guide.

Going further

Production checklist

  • Make the create-confirm sequence idempotent. A flaky network between createMembership and actionConfirmMembership should never result in two reserved memberships for the same customer. Either use an external_ref and check whether a membership with that ref already exists before re-creating, or hold the membership ID in your own database between the two calls so a retry resumes at the same point.
  • Reconcile failed charges. Subscribe to membership.charged and cross-reference against your own ledger; payments that don't appear by their expected billing date are the ones you need to chase, not the ones that do. The Trybe back office dashboards surface needs_attention memberships, but for high-volume operators a periodic reconciliation job is cheaper than manual triage.
  • Distinguish member from customer. A Customer is the underlying identity (one per person per brand); a MembershipMember is a join row associating a customer with a membership. Don't conflate the two in your data model — one customer can be a member of multiple memberships, and one membership can have multiple members.
  • Retry only safe operations on 5xx. Reads (getMembership, listMembershipMembers) are safe with exponential backoff. The action endpoints aren't — re-issuing credits or re-cancelling can produce duplicate ledger entries. Surface the error and let the operator decide.
  • Verify webhook signatures. Every webhook delivery carries a signature header. Reject unsigned or mis-signed deliveries; see the webhooks guide.
  • Don't store mandate details client-side. The mandate lives in Stripe; Trybe only stores the reference. Never log or persist the customer's bank details — your application never sees them.
  • Test cancellations end-to-end. It's easy to wire up signup and forget to wire up cancellation. A membership that can be created but not cancelled is a churn-and-retention nightmare. Cancel a membership in playground as part of your acceptance suite.
  • Handle the needs_attention state explicitly. It's the catch-all for "this needs a human". Surface it on your operator dashboard with the attention_reason; don't hide it behind "active" filters.