BirrJS
Concepts

Subscriptions

The subscription lifecycle — from pending to active to canceled or expired.

Lifecycle States

                  ┌──► failed (after 60 min pending)
pending ──► active ──► expired

               └──► canceled (at period end)

none ──► (no subscription record exists)
StateDescription
pendingCreated but payment not yet confirmed
activePayment confirmed, customer has access
expiredPeriod ended without renewal
canceledManually canceled (immediate or at period end)
failedPending subscription that never activated (effective status)
noneCustomer has no subscription at all (effective status)

Effective Status

BirrJS computes a runtime effective status that may differ from the stored database status:

StoredConditionEffective
activeWithin periodactive
activeexpiresAt passedexpired
pendingCreated > 60 min agofailed
canceledPeriod not yet endedactive (until period end)

This is handled by getEffectiveStatus():

const effective = getEffectiveStatus(subscription);
// "pending" | "active" | "canceled" | "failed" | "expired" | "none"

Subscription Flow

Subscribe

Call subscribe() with the target plan ID. BirrJS creates a subscription in pending status and initializes a transaction with the payment provider. For paid plans, the result includes a checkoutUrl to redirect the user.

const result = await client.subscribe({ planId: "pro" });
// result.checkoutUrl → redirect to Chapa checkout
const result = await birrjs.subscribe({
  customerId: "user_123",
  planId: "pro",
});
// result.checkoutUrl → return to client for redirect

Payment confirmed (webhook)

When the provider sends a charge.success webhook, BirrJS transitions the subscription to active, sets startedAt, and calculates expiresAt.

Renewal

On the next charge.success webhook, the subscription is renewed: expiresAt extends from its current value.

Cancel

Cancellation behavior depends on the subscription state:

// Active subscription: cancel at period end (keeps access until paid period ends)
await client.cancelSubscription({ subscriptionId });
// status stays "active", endedAt = expiresAt

// Pending subscription (never activated): cancels immediately
// status → "canceled", endedAt = now

Expiration

If the period ends without renewal, the subscription transitions to expired. This can happen via:

  • The expiry sweep cron job (every 10 minutes by default)
  • getEffectiveStatus() on read

Subscribe Input

FieldRequiredDescription
planIdYesThe target plan ID. Must be a valid plan from your config.
customerIdServer onlyThe customer to subscribe. Not needed on the client (resolved from identify).

Result Shape

FieldDescription
checkoutUrlThe payment provider's checkout URL. Redirect the user here to complete payment.
subscriptionIdThe ID of the created subscription record.
customerIdThe customer ID the subscription was created for.

If checkoutUrl is set, the subscription stays in pending until the provider confirms payment via webhook.

Reminders

Subscriptions approaching expiry can trigger reminders via the "subscription.reminder" event. A daily cron sweep checks for subscriptions expiring on the exact days listed in reminderLeadDays and fires the event. A dedup table prevents duplicate sends.

A subscription expiring June 20 with reminderLeadDays: [7, 3, 1] gets reminders on June 13, June 17, and June 19. Each event includes daysUntilExpiry so your handler can customize per day.

createBirr({
  scheduling: {
    reminderSweepCron: "0 8 * * *",   // daily at 8am
    reminderLeadDays: [7, 3, 1],      // send 7, 3, and 1 day before
  },
  on: {
    "subscription.reminder": async (payload) => {
      await resend.emails.send({
        to: payload.customerEmail,
        subject: `Your ${payload.planName} expires in ${payload.daysUntilExpiry} days`,
        text: `Renew: https://myapp.com/renew?sid=${payload.subscriptionId}`,
      });
    },
  },
  plugins: [
    afromessage({
      messages: {
        subscriptionReminder: "{daysUntil} days left. Renew now!",
      },
    }),
  ],
});

Reminder records are automatically cleared when a subscription is reactivated, so renewed subscriptions get fresh reminders for the new period.

Cron Sweeps

SweepIntervalAction
ReminderEvery day at 8amFires subscription.reminder events for expiring subs
PendingEvery 5 minMarks stuck pending subscriptions as failed
ExpiryEvery 10 minMarks expired active subscriptions as expired

The scheduler runs automatically by default (mode: "auto"). You can set mode: "external" and call the cron endpoints manually, or mode: "manual" to never run cron.

Webhook Idempotency

Webhooks are deduplicated using a unique index on (providerId, providerReferenceId). If a webhook is received twice, the second one is skipped.