Subscription Lifecycle
A subscription is a recurring monthly billing record tied to a FloatMe user’s membership. Every subscription progresses through a set of statuses from the moment it is created until it is either collected, waived, or cancelled. This page covers the end-to-end lifecycle: activation, collection, manual payment, upgrade/downgrade, ban, and reactivation.
Subscription Statuses
Status Transitions
SCHEDULED ──[Collections worker: pinless debit success]──────────► COMPLETED
SCHEDULED ──[Collections worker: ACH submitted]──────────────────► ACHSENT
SCHEDULED ──[Collections worker: balance check fails / error]────► ERROR
SCHEDULED ──[Manual pay: pinless success]─────────────────────────► COMPLETED
SCHEDULED ──[Manual pay: pinless fails]───────────────────────────► SCHEDULED (status unchanged)
SCHEDULED ──[Pause event from memberships]────────────────────────► PAUSED
SCHEDULED ──[Ban: POST /ban]─────────────────────────────────────► CANCELLED
SCHEDULED ──[User not active at collection time]─────────────────► CANCELLED
SCHEDULED ──[Employee waiver in collections worker]──────────────► WAIVED
ACHSENT ──[ACH settles: COMPLETED event on payments Kinesis]───► COMPLETED
ACHSENT ──[ACH returned: FAILED event on payments Kinesis]─────► ERROR
ACHSENT ──[Chargeback: CHARGED_BACK event on payments Kinesis]─► ERROR (+ ban user)
ACHSENT ──[Refund: REFUNDED event on payments Kinesis]─────────► REFUNDED
ERROR ──[Retry worker: ACH submitted]─────────────────────────► ACHSENT
ERROR ──[Retry worker: ACH limit exceeded / blocklisted]──────► (skip, stays ERROR)
ERROR ──[Webhook income/balance worker: pinless success]──────► COMPLETED
ERROR ──[Webhook income/balance worker: pinless fails]─────────► ERROR (status unchanged)
ERROR ──[Ban: POST /ban]──────────────────────────────────────► CANCELLED
ERROR ──[Webhook worker: user not active]─────────────────────► INACTIVE
ERROR ──[Batch worker: user not active]───────────────────────► INACTIVE
PAUSED ──[Collections pause worker: skip cycle]────────────────► PAUSED_SKIPPED
(next PAUSED record scheduled; if last pause month → SCHEDULED)
PAUSED_SKIPPED (terminal for that record — a new PAUSED or SCHEDULED record is created)
COMPLETED (terminal)
CANCELLED (terminal)
WAIVED (terminal — employee-only; next SCHEDULED record is created)
INACTIVE (terminal — user account not active at time of collection)
STALE (computed read-only view: returned by GET /subscriptions/current when the
most recent record is very old and no non-stale statuses are found)
Status Reference
| Status | Description |
|---|---|
|
Initial status assigned when a subscription is first created (on activation or after a previous cycle completes). The subscription is awaiting its billing date. |
|
An ACH debit has been submitted to the payment processor. The subscription remains in this state until the ACH Handler Lambda receives a settlement event from the |
|
The subscription has been successfully collected. Set by the collections worker on a successful pinless debit, by the ACH Handler on a successful ACH settlement, or by the manual pay endpoint. |
|
A collection attempt was made but failed — insufficient balance, a returned ACH, a payment provider error, or a chargeback. The subscription will be retried by the scheduled retry worker or by the webhook workers on a qualifying income or balance event. |
|
The subscription was waived for a FloatMe employee. The collections worker detects the |
|
The subscription was cancelled because the user’s membership was cancelled (via the memberships Kinesis consumer), because the user was banned, or because the user was found to be inactive at collection time. No further collection attempts are made. |
|
The user’s membership is paused. The subscription record carries a |
|
Terminal status for an individual record within a pause window. Written by the pause worker to record that a billing cycle was skipped. The next record (PAUSED or SCHEDULED) is created atomically. |
|
Set when the webhook worker or batch worker detects that the user account is not |
|
The subscription payment was refunded after successful ACH collection. Set by the ACH Handler on a |
|
A computed, read-only status returned by |
Activation
When a new user joins FloatMe and enables their membership, the mobile app calls POST /{user_id}/subscriptions/activate. This is a lightweight idempotent endpoint — if the user already has a SCHEDULED record it returns 200 OK without creating a duplicate.
POST /{user_id}/subscriptions/activate
│
▼
List all subscriptions for user (DynamoDB)
│
├── Any record with status SCHEDULED? ──Yes──► 200 OK (already active)
│
│ No
▼
Calculate next billing date
Friday billing enabled? ──Yes──► next Friday ≥ today + 6 days (grace period)
──No───► today + 9 days (seven-day trial window)
│
▼
Create subscription record (DynamoDB)
billing_status: SCHEDULED
billing_amount: $4.99 (AmountLite)
billing_date: next billing date
billing_period: MM/YYYY of next billing date
term: MONTHLY
subscription_id: new UUID
created_date: now
│
▼
Return 200 OK
Activation does not charge the user immediately. The first charge occurs when the scheduled billing date arrives and the collections-job picks up the SCHEDULED record.
|
Upgrade / Downgrade
Upgrade
POST /{user_id}/subscriptions/upgrade changes the amount and tier of the user’s next scheduled subscription. The existing SCHEDULED record is overwritten in-place (same DynamoDB key); is_pending_downgrade is cleared.
POST /{user_id}/subscriptions/upgrade
Body: { "upgrade_tier": "plus", "upgrade_tier_version": "v1" }
│
▼
Get recent subscriptions for user (DynamoDB)
│
├── No SCHEDULED record found? ──Yes──► 404 Not Found
│
│ SCHEDULED record found → nextSub
▼
Validate tier + version against GrowthBook (global.tiers.config)
│
├── Unknown tier ──────────────► 400 Bad Request
├── Unknown version ───────────► 400 Bad Request
│
│ Valid tier
▼
Update nextSub in-place:
billing_amount: tier.Price.Monthly
is_pending_downgrade: false
term: MONTHLY
receipt_tier_name: "tier:version"
last_run_date: now
│
▼
Save updated record (DynamoDB — overwrites existing SCHEDULED record)
│
▼
Return 201 Created (updated subscription)
| The new amount takes effect at the next billing cycle. If the current billing cycle is already in-flight (ACHSENT or COMPLETED), the upgrade applies to the record that follows it. |
Downgrade
POST /{user_id}/subscriptions/downgrade works identically to upgrade but sets is_pending_downgrade = true on the record and accepts PAUSED records in addition to SCHEDULED ones (for users in a pause window).
POST /{user_id}/subscriptions/downgrade
Body: { "downgrade_tier": "base", "downgrade_tier_version": "v0" }
│
▼
Get recent subscriptions for user (DynamoDB)
│
├── No SCHEDULED or PAUSED record found? ──Yes──► 404 Not Found
│
│ SCHEDULED or PAUSED record found → nextSub
▼
Validate tier + version against GrowthBook (global.tiers.config)
│
├── Unknown tier or version ──► 400 Bad Request
│
│ Valid
▼
Update nextSub in-place:
billing_amount: tier.Price.Monthly
is_pending_downgrade: true
term: MONTHLY
receipt_tier_name: "tier:version"
last_run_date: now
│
▼
Save updated record (DynamoDB)
│
▼
Return 201 Created (updated subscription)
When is_pending_downgrade = true, the collections worker calls FinalizeDowngradeMembershipTierWithResponse on the User Service at the start of the next billing cycle before charging the user, officially completing the tier change on the membership side.
|
Manual Pay
A user can manually pay a due subscription via POST /{user_id}/subscriptions/{subscription_id}/pay. Only subscriptions with status SCHEDULED or ERROR that are not older than OldSubscriptionDays are eligible.
POST /{user_id}/subscriptions/{subscription_id}/pay
│
▼
Get user from User Service
│
├── User status != ACTIVE? ──Yes──► 409 Conflict
│
│ Active
▼
Get subscription record by ID (DynamoDB)
│
├── Status not in [SCHEDULED, ERROR]? ──Yes──► 409 Conflict
├── Subscription too old? ───────────────Yes──► 409 Conflict
│
│ Eligible
▼
Acquire distributed lock (DynamoDB)
│
▼
Get debit card for user (Payments Service)
│
├── Card not valid? ──Yes──► error → 409 Conflict
│
│ Valid card
▼
Submit pinless debit (Payments Service)
│
├── Payment response = COMPLETED
│ │
│ ├── Status was SCHEDULED?
│ │ └── Schedule next SCHEDULED subscription (DynamoDB)
│ │
│ ├── Update subscription status → COMPLETED (process: MANUAL_REPAYMENT)
│ ├── Send receipt (Segment via DebitReceiptTopic)
│ ├── Send AppsFlyer ad_subscription_payment_completed event
│ └── Trigger underwriting recalculation (Underwriting Service)
│
├── Payment response != COMPLETED (failed)
│ │
│ ├── Update subscription status → unchanged (error recorded in usio_error)
│ └── Send AppsFlyer ad_subscription_payment_failed event
│
▼
Return 201 Created (success) or 409 Conflict (payment failed)
The manual pay endpoint uses pinless debit only — it never submits an ACH. If the pinless attempt fails, the subscription status remains as it was (SCHEDULED or ERROR); the usio_error field is updated with the failure message.
|
Ban
POST /{user_id}/subscriptions/ban is called by other internal services (typically the ACH Handler or the User Service) to cancel all pending subscription records when a user is banned. Only records with status SCHEDULED or ERROR are affected; records in terminal states are left unchanged.
POST /{user_id}/subscriptions/ban
│
▼
List all subscriptions for user (DynamoDB)
│
▼
For each subscription:
│
├── Status = SCHEDULED or ERROR?
│ │
│ └── Update record:
│ billing_status: CANCELLED
│ updated_event: "user_banned"
│ last_run_date: now
│
└── Any other status → skip (no change)
│
▼
Save all updated records (DynamoDB — overwrites with Add, creates history entries)
│
├── No records needed updating? ──Yes──► 200 OK (warn logged)
│
▼
Return 200 OK
The ACH Handler also calls the User Service ban endpoint directly when it processes a CHARGED_BACK or bannable FAILED payment event (e.g. ACH return codes R05, R07, R08, R10, R11, R29). In that path the subscription status is set to ERROR first by the ACH Handler, and the downstream ban call then cancels the record.
|
Reactivate
POST /{user_id}/subscriptions/reactivate is called when a previously cancelled or lapsed user resumes their FloatMe membership. It immediately charges the user for the current month and creates a new SCHEDULED record for the next billing cycle.
POST /{user_id}/subscriptions/reactivate
Body: { "tier": "base", "version": "v0" } (optional; defaults to base:v0)
│
▼
Get membership tier config from GrowthBook (global.tiers.config)
│
├── Error getting config ─────────────► 500 Internal Server Error
│
▼
Parse and validate request body
│
├── tier XOR version provided ────────► 400 Bad Request
│
▼
Validate tier + version against GrowthBook config
│
├── Unknown tier or version ──────────► 400 Bad Request
│
▼
List all subscriptions for user (DynamoDB)
│
├── SCHEDULED record already exists? ──Yes──► 200 OK (already active)
│
│ No active subscription
▼
Get debit card for user (Payments Service)
│
├── Card is invalid? ─────────────────► 409 Conflict
│
│ Valid card
▼
Submit pinless debit for current month (Payments Service)
amount: tier.Price.Monthly
new subscription_id: UUID
│
├── Payment provider error ───────────► 500 Internal Server Error
├── Payment returned IsFailed ────────► 402 Payment Required
│
│ Payment success: returns confirmationID + masked card number
▼
Write COMPLETED subscription record (DynamoDB)
billing_status: COMPLETED
billing_amount: tier.Price.Monthly
billing_date: now
updated_event: "user-reactivated"
term: MONTHLY
│
▼
Write next SCHEDULED subscription record (DynamoDB)
billing_status: SCHEDULED
billing_date: first billing date ~1 month from now
(next Friday if Friday billing enabled)
billing_amount: tier.Price.Monthly
term: MONTHLY
│
▼
Return 201 Created
{ subscription: <new SCHEDULED record>, mask: <masked card number> }
Reactivation charges the user synchronously before returning. If the DynamoDB write for the COMPLETED record fails after a successful charge, the payment has moved but no subscription record exists — the same race condition noted in float-service disbursement. The SCHEDULED record write happens only after the COMPLETED record is saved.
|
Related Pages
-
Collections — Scheduled, retry, pause, webhook, and ACH collection flows
-
ACH Processing — ACH settlement callbacks and status transitions from the payments Kinesis stream
-
Memberships — Kinesis consumer that drives PAUSE, UNPAUSE, CANCEL, and other membership lifecycle events
-
API Endpoints — Full endpoint reference including request/response shapes
-
DynamoDB Schema — billing-activity table attributes, GSIs, and access patterns