BettahLife API Documentation

REST API Reference — v1.0

Base URL

https://your-domain.com/api

All endpoints listed below are relative to this base URL.

Authentication

The API uses Laravel Sanctum token authentication. After logging in or registering, include the token in subsequent requests:

Authorization: Bearer {api_token}

Endpoints marked Auth Required need this header. Endpoints marked Public do not.

Error Handling

Standard HTTP status codes are used. Validation errors return 422 with a JSON body:

{
    "message": "The given data was invalid.",
    "errors": {
        "field": ["Error message"]
    }
}
CodeMeaning
200Success
201Created
204No Content (successful delete)
401Unauthenticated
403Forbidden
404Not Found
410Gone (expired resource)
422Validation Error
500Server Error

Auth

POST /login Public

Authenticate a user and receive an API token.

ParameterTypeRules
usernamestringrequired — email or phone number
passwordstringrequired
// Response 200
{
    "data": {
        "id": 1,
        "firstname": "John",
        "lastname": "Doe",
        "email": "john@example.com",
        "api_token": "1|abc123...",
        ...
    }
}

POST /register Public

Create a new user account. Awards 5 BettahPoints on registration.

ParameterTypeRules
firstnamestringrequired
lastnamestringrequired
emailstringrequired — unique
passwordstringrequired — confirmed, min:8
password_confirmationstringrequired
date_of_birthdaterequired
genderstringrequired — male|female
country_codestringrequired
nicknamestringoptional
occupationstringoptional

GET /user Auth Required

Get the currently authenticated user with phones, addresses, and coach info.

POST /logout Auth Required

Revoke the current API token.

Password Reset

POST /password_reset/request_code Public

Send a 6-digit OTP to the user's phone via Twilio.

ParameterTypeRules
usernamestringrequired — email or phone

POST /password_reset/verify_code Public

Verify the OTP code.

ParameterTypeRules
usernamestringrequired
codeintegerrequired — 6 digits

POST /password_reset/new_password Public

Set a new password after OTP verification.

ParameterTypeRules
usernamestringrequired
passwordstringrequired — confirmed
password_confirmationstringrequired

Users

All user endpoints require Auth Required.

GET /users

Paginated list of users (50 per page).

POST /users

Create a user (admin use).

ParameterTypeRules
emailstringrequired — unique
firstnamestringrequired
lastnamestringrequired
genderstringrequired — male|female
country_codestringrequired
date_of_birthdaterequired
passwordstringrequired — confirmed
nicknamestringoptional
occupationstringoptional
national_idstringoptional — unique

GET /users/{user}

Get a single user with phones, addresses, coach info, and BettahPoints balance.

PUT /users/{user}

Update a user. Awards 2 BettahPoints on first profile completion (firstname, lastname, date_of_birth, gender, country_code all filled).

DELETE /users/{user}

Soft-delete a user. Cannot delete your own account. Returns 204.

User Photo

Direct upload endpoints are preferred for mobile clients. A user may upload their own photo; BettahLife staff may upload for another user.

POST /user/photo/presign

Create a signed upload URL for the authenticated user's profile photo.

ParameterTypeRules
filenamestringrequired — max:255
content_typestringrequiredimage/jpeg or image/png
sizeintegerrequired — bytes, max photo upload limit

POST /user/photo/confirm

Confirm a direct upload for the authenticated user's profile photo. Replaces the old photo and deletes the old object.

ParameterTypeRules
keystringrequired — key returned by presign
filenamestringrequired
content_typestringrequired
sizeintegerrequired

POST /users/{user}/photo/presign

Create a signed upload URL for a route user's profile photo. The route user must be the authenticated user unless the requester is BettahLife staff.

POST /users/{user}/photo/confirm

Confirm a direct upload for a route user's profile photo. Same body as /user/photo/confirm. Returns UserResource.

POST /users/{user}/photo

Legacy multipart profile photo upload. Field: file. Accepted: jpg, jpeg, png. Max size: 10M.

DELETE /users/{user}/photo

Remove the profile photo.

User Data Deletion

POST /users/{user}/deletion-request

Request deletion of the authenticated user's data. The authenticated user must match {user}. The account is marked for deletion and scheduled for permanent deletion after 30 days by the deletion job.

// Response 200
{
    "message": "Account deletion scheduled.",
    "account_deletion_requested_at": "2026-05-17T00:00:00Z",
    "account_deletion_scheduled_at": "2026-06-16T00:00:00Z"
}

Notification Preferences

PUT /users/{user}/notification_preferences

Update notification settings.

ParameterTypeRules
event_remindersbooleanrequired
sms_notificationsbooleanrequired
push_notificationsbooleanrequired
email_notificationsbooleanrequired
newsletter_subscriptionbooleanrequired
marketing_sms_notificationsbooleanrequired
marketing_email_notificationsbooleanrequired
marketing_push_notificationsbooleanrequired

FCM Tokens

POST /users/{user}/fcm_tokens

Register or update the authenticated user's mobile FCM token. Only the owner of {user} may manage tokens.

ParameterTypeRules
tokenstringrequired — max:512, unique token is upserted
platformstringrequired — ios|android
device_idstringoptional
app_versionstringoptional

DELETE /users/{user}/fcm_tokens

Delete the authenticated user's token by token or device_id. Returns 204.

Medical Info

PUT /users/{user}/medical_info

Update medical information.

ParameterTypeRules
heightnumericoptional
weightnumericoptional
allergiesstringoptional
blood_typestringoptional
medicationstringoptional
known_ailmentsstringoptional
favorite_sportstringoptional

Platform Subscription

POST /users/{user}/subscription

Start a BettahLife platform subscription checkout for the user. Returns a hosted checkout URL and payment reference. Platform subscription does not itself grant course or event access.

{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}

User Phones

GET /users/{user}/phones

Paginated list (50/page).

POST /users/{user}/phones

ParameterTypeRules
typestringrequired
numberstringrequired — unique
labelstringoptional
is_defaultbooleanoptional
country_codestringoptional
has_whatsappbooleanoptional

GET /users/{user}/phones/{phone}

Show a single phone record.

PUT /users/{user}/phones/{phone}

Update a phone record.

DELETE /users/{user}/phones/{phone}

Delete a phone record. Returns 204.

User Addresses

GET /users/{user}/addresses

Paginated list. Filter with ?type=home|work|other.

POST /users/{user}/addresses

ParameterTypeRules
typestringrequired — home|work|other
street1stringrequired
citystringrequired
statestringrequired
postal_codestringrequired
country_codestringrequired
street2stringoptional
labelstringoptional
is_defaultbooleanoptional

GET /users/{user}/addresses/{address}

Show a single address.

PUT /users/{user}/addresses/{address}

Update an address.

DELETE /users/{user}/addresses/{address}

Delete an address. Returns 204.

Coaches

Coach types: individual (sole coach) and organization (company/group). BettahLife itself is an organization identified by is_platform = true.

GET /coaches

Paginated list (15/page) with admins, users, owner, and documents.

POST /coaches

ParameterTypeRules
user_idintegerrequired
namestringrequired
emailstringrequired — unique
descriptionstringrequired
addressstringrequired
phonestringrequired — unique
typestringrequiredindividual or organization
subscription_monthly_pricenumericoptional — monthly paid coach subscription amount
subscription_annual_pricenumericoptional — annual paid coach subscription amount
subscription_currencystringoptional — 3-letter currency code, defaults to NGN
subscription_pricenumericoptional — legacy monthly price fallback
tagsstringoptional
secondary_phonestringoptional
x, tiktok, website, youtube, facebook, linkedin, instagramstringoptional — social links

GET /coaches/{coach}

Full coach details with admins, subscribers, documents, subscriber counts, active subscription information for the authenticated user, and subscription_options for monthly/annual pricing.

// Excerpt
{
    "id": 12,
    "is_subscribed": false,
    "active_subscription": null,
    "subscription_options": {
        "monthly": {
            "plan_id": 101,
            "price": 2000,
            "currency": "NGN",
            "billing_interval": "monthly",
            "providers": {
                "apple": { "product_id": "com.bettahme.coach.monthly" },
                "google": { "product_id": "coach_monthly" },
                "monnify": { "product_code": "coach-12-monthly" }
            }
        },
        "annual": null
    }
}

PUT /coaches/{coach}

Update coach details.

DELETE /coaches/{coach}

Delete a coach.

POST /coaches/{coach}/in_progress

Set coach status to pending (in progress).

Coach Documents

Direct upload endpoints require the authenticated user to be the coach owner or an assigned coach admin.

POST /coaches/{coach}/documents/presign

Create a signed upload URL for a coach document.

ParameterTypeRules
typestringrequired — letters, numbers, underscore, and dash only
filenamestringrequired — max:255
content_typestringrequired — max:255
sizeintegerrequired — bytes, max document upload limit

POST /coaches/{coach}/documents/confirm

Confirm a direct document upload. Replaces the existing document row and object for the same type. Returns UserResource, matching the legacy document upload endpoint.

POST /coaches/{coach}/documents?type={document_type}

Legacy multipart document upload. Field: file. Max size: 25M. Replaces existing document of the same type.

DELETE /coaches/{coach}/documents/{document}

Delete a document.

Coach Invitations

Only organization type coaches can send invitations. Invitations expire after 7 days.

GET /coaches/{coach}/invitations

List all invitations for a coach.

POST /coaches/{coach}/invitations

Send an invitation.

ParameterTypeRules
emailstringrequired without phone
phonestringrequired without email
rolestringrequired — admin|subscriber
// Response 200
{
    "id": 1,
    "role": "subscriber",
    "email": "user@example.com",
    "phone": null,
    "token": "uuid-token",
    "expires_at": "2026-03-26T00:00:00Z"
}

GET /coaches/{coach}/invitations/{token}

View invitation details by token.

POST /coaches/{coach}/invitations/{token}/accept

Accept an invitation. Admin invites set user.coach_id; subscriber invites add a subscriber relation. Returns 410 if expired.

DELETE /coaches/{coach}/invitations/{token}

Decline an invitation. Returns 204.

Coach Subscriptions

POST /users/{user}/coaches/{coach}/subscribe

Subscribe a user to a coach. Free subscriptions grant access immediately and return the updated coach resource. Paid Monnify subscriptions return checkout details. Apple/Google subscriptions return store product metadata; the mobile client must then complete the store purchase and verify it with the subscription verification endpoint.

ParameterTypeRules
billing_intervalstringoptionalmonthly or yearly; default monthly
providerstringoptionalapple, google, or monnify; default monnify
channelstringoptionalweb, admin, manual, or mobile
// Free-subscription response
{
    "subscribed": true,
    "coach": { ... }
}
// Monnify paid-subscription response
{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}
// Apple/Google mobile response
{
    "requires_store_payment": true,
    "provider": "apple",
    "plan_id": 101,
    "product_id": "com.bettahme.coach.monthly",
    "billing_interval": "monthly"
}

POST /users/{user}/coaches/{coach}/unsubscribe

Remove a user's manual/direct subscription to a coach. Store-managed subscriptions must be cancelled in Apple/Google; this endpoint does not cancel store billing and only removes manual/direct local enrollment.

Events

GET /events

Paginated list (15/page) with extensive filtering.

Query ParamDescription
subscribedFilter to user's subscribed events only
statusdraft, published, or canceled
periodupcoming, ongoing, or past
coach_idFilter by coach
event_type_idFilter by event type
event_category_idFilter by category
event_frequency_idFilter by frequency
locationWildcard search on address
searchwordWildcard search on title, description, tags
start_date / end_dateDate range filter

By default, draft events are only included when they belong to the authenticated user's coach. Other users only see published or canceled events.

POST /events

ParameterTypeRules
coach_idintegeroptional when the authenticated user already belongs to a coach; otherwise required
created_byintegerset automatically from the authenticated user
event_type_idintegerrequired
event_category_idintegerrequired
event_frequency_idintegerrequired
titlestringrequired
descriptionstringrequired
start_datedaterequired
end_datedateoptional
goal, link, address, tagsstringoptional
latitude, longitudenumericoptional
expected_attendanceintegeroptional
leaderboard_typestringoptional
statusstringoptionaldraft, published, or canceled. Omitted status creates a draft event.

GET /events/{event}

Single event with coach, files, and subscriber info.

PUT /events/{event}

Update an event. Optional status values are draft, published, and canceled. Omitted status preserves the current lifecycle status. Past events cannot be edited. An event is past when its end_date has passed, or when it has no end_date and its start_date has passed.

DELETE /events/{event}

Delete an event.

Event Files

Direct upload endpoints require the authenticated user to be the event coach owner or an assigned coach admin.

GET /events/{event}/files

List all files for an event. Access is enforced by the event entitlement rules.

POST /events/{event}/files/presign

Create a signed upload URL for an event file.

ParameterTypeRules
filenamestringrequired — max:255
content_typestringrequired — max:255
sizeintegerrequired — bytes, max document upload limit

POST /events/{event}/files/confirm

Confirm a direct upload after the client uploads to the signed URL. Creates a new event_files row and returns EventFileResource.

ParameterTypeRules
keystringrequired — key returned by presign
filename, content_type, sizemixedrequired — same values used for presign

POST /events/{event}/files

Legacy multipart event file upload. Field: file. Max size: 25M.

GET /events/{event}/files/{eventFile}

Show a single event file. Nested access is scoped to the parent event and protected by event entitlement checks.

PUT /events/{event}/files/{eventFile}

Update an event file's metadata within the parent event scope.

DELETE /event_files/{eventFile}

Delete a file. Returns 204.

Event Subscriptions

GET /users/{user}/events

List events the user is directly subscribed to. Each event includes access metadata such as can_access and access_reason.

POST /users/{user}/events/{event}/subscribe

Subscribe to an event. If the user already has access through direct enrollment, coach subscription, or coach admin access, the endpoint returns immediate success. Free events attach the user directly. Paid events return checkout details instead of immediate enrollment.

// Immediate-access response
{
    "subscribed": true,
    "can_access": true,
    "access_reason": "direct_event_enrollment"
}
// Paid-event response
{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}

POST /users/{user}/events/{event}/unsubscribe

Unsubscribe from an event. Returns the user's remaining directly subscribed events as an EventResource collection.

Leaderboard

GET /events/{event}/leaderboard

Get the event leaderboard. Access is enforced by the event entitlement rules. Returns 404 if the event has no leaderboard configured.

Query ParamDefaultDescription
periodoveralloverall, daily, or weekly
datetodayYYYY-MM-DD for daily; YYYY-W## for weekly
limit100Max entries (max 500)
// Response 200
{
    "data": [
        { "rank": 1, "user_id": 5, "name": "Jane", "score": 52340 },
        ...
    ],
    "meta": {
        "event_id": 1,
        "period": "overall",
        "leaderboard_type": "steps",
        "total_participants": 128,
        "my_rank": 12,
        "my_score": 34500
    }
}

Event Attributes

GET /event_attributes

Lookup data: event_locations, event_categories, event_frequencies.

Courses

GET /courses

Paginated list (15/page). Filter: ?subscribed, ?coach_id, ?searchword.

POST /courses

ParameterTypeRules
coach_idintegeroptional when the authenticated user already belongs to a coach; otherwise required
titlestringrequired — max 255
descriptionstringrequired
goalstringoptional
tagsstringoptional
pricenumericoptional — leave empty for a free course
currencystringoptional — 3-letter code, defaults to NGN
statusstringoptionaldraft, published, or archived. Omitted status creates a draft course.

GET /courses/{course}

Course with coach, items, users, and access metadata such as can_access and access_reason.

PUT /courses/{course}

Update a course. Optional status values are draft, published, and archived. Once a course is published or archived, it cannot be returned to draft; retiring a course should use archived. Omitted status preserves the current lifecycle status.

DELETE /courses/{course}

Delete a course. Returns 204.

Course Items

GET /courses/{course}/course_items

List all items in a course, ordered by position. Items include access metadata and media URLs are only returned when the user can access them.

POST /courses/{course}/course_items

If the parent course is free, course items are forced to free access and is_free=false is ignored.

ParameterTypeRules
titlestringrequired
descriptionstringoptional
typestringrequired
is_freebooleanoptional — automatically treated as true when the course is free

GET /courses/{course}/course_items/{course_item}

Show a single course item with is_free, can_access, and access_reason.

PUT /courses/{course}/course_items/{course_item}

Update a course item. If the parent course is free, is_free is forced to true.

DELETE /courses/{course}/course_items/{course_item}

Delete a course item and its media. Returns 204.

POST /courses/{course}/course_items/{course_item}/file

Multipart fallback for course item media. Field: file. Max size: 500M. For large media, use the direct upload flow below.

POST /courses/{course}/course_items/{course_item}/media/presign

Create a short-lived signed DigitalOcean Spaces upload URL for course item media. The course item must belong to the parent course.

ParameterTypeRules
filenamestringrequired — max:255
content_typestringrequired — common video, audio, image, or PDF MIME type
sizeintegerrequired — bytes, max 500M
// Response 200
{
    "key": "bettahlife/production/courses/course_items/10/uuid.mp4",
    "upload_url": "https://...",
    "headers": {
        "Content-Type": "video/mp4",
        "x-amz-acl": "public-read"
    },
    "expires_at": "2026-05-06T12:00:00Z",
    "public_url": "https://..."
}

POST /courses/{course}/course_items/{course_item}/media/confirm

Confirm a direct upload after the client uploads to upload_url with PUT and the returned headers. Verifies the object exists, checks the uploaded object size matches size, validates MIME type, then saves the media fields on the course item.

ParameterTypeRules
keystringrequired — key returned by presign
filenamestringrequired
content_typestringrequired
sizeintegerrequired — bytes, max 500M

Course Subscriptions

GET /users/{user}/courses

List courses the user is directly subscribed to. Each course includes access metadata such as can_access and access_reason.

POST /users/{user}/courses/{course}/subscribe

Subscribe to a course. If the user already has access through direct enrollment, coach subscription, or coach admin access, the endpoint returns immediate success. Free courses attach the user directly. Paid courses return checkout details instead of immediate enrollment.

// Immediate-access response
{
    "subscribed": true,
    "can_access": true,
    "access_reason": "direct_course_enrollment"
}
// Paid-course response
{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}

POST /users/{user}/courses/{course}/unsubscribe

Unsubscribe from a course. Returns the user's remaining directly subscribed courses as a CourseResource collection.

Steps

GET /users/{user}/steps

List step records (latest first). Auth user sees own steps only. Supports ?from, ?to, and ?per_page.

POST /users/{user}/steps

Record steps. Updates event leaderboard if active. Awards 1 BettahPoint daily bonus if total exceeds 15,000 steps. Awards 5 BettahPoints for first 10,000 step milestone.

ParameterTypeRules
recorded_atdaterequired
countintegerrequired — min:0
unitstringoptional — default: steps
sourcestringoptional
devicestringoptional

GET /users/{user}/steps/{step}

Show a single step record.

POST /users/{user}/steps/bulk

Bulk upsert step records for the user. Returns the number of synced records.

GET /users/{user}/steps/summary

Get daily total steps for a date range. Supports ?from and ?to.

DELETE /users/{user}/steps/{step}

Delete a step record. Returns 204.

Distances

GET /users/{user}/distances

List distance records (latest first). Supports ?from, ?to, and ?per_page.

POST /users/{user}/distances

Record distance. Awards 5 BettahPoints for first 10km lifetime milestone.

ParameterTypeRules
recorded_atdaterequired
amountnumericrequired — min:0
unitstringoptional — km|miles (default: km)
sourcestringoptional
devicestringoptional
activity_typestringoptional

GET /users/{user}/distances/{distance}

Show a single distance record.

POST /users/{user}/distances/bulk

Bulk upsert distance records for the user. Returns the number of synced records.

GET /users/{user}/distances/summary

Get daily total distance for a date range. Supports ?from and ?to.

DELETE /users/{user}/distances/{distance}

Delete a distance record. Returns 204.

Water

GET /users/{user}/waters

List water intake records. Supports ?from, ?to, and ?per_page.

POST /users/{user}/waters

Create a water intake record.

GET /users/{user}/waters/{water}

Show a single water record.

PUT /users/{user}/waters/{water}

Update a water record.

DELETE /users/{user}/waters/{water}

Delete a water record. Returns 204.

Weight

GET /users/{user}/weights

List weight records. Supports ?from, ?to, and ?per_page.

POST /users/{user}/weights

Create a weight record.

GET /users/{user}/weights/{weight}

Show a single weight record.

PUT /users/{user}/weights/{weight}

Update a weight record.

DELETE /users/{user}/weights/{weight}

Delete a weight record. Returns 204.

Blood Pressure

GET /users/{user}/blood_pressures

List blood pressure records. Supports ?from, ?to, and ?per_page.

POST /users/{user}/blood_pressures

Create a blood pressure record.

GET /users/{user}/blood_pressures/{blood_pressure}

Show a single blood pressure record.

PUT /users/{user}/blood_pressures/{blood_pressure}

Update a blood pressure record.

DELETE /users/{user}/blood_pressures/{blood_pressure}

Delete a blood pressure record. Returns 204.

Medications

GET /users/{user}/medications

Paginated list (50/page) with reminders.

POST /users/{user}/medications

Create a medication and auto-generate reminders.

ParameterTypeRules
titlestringrequired — max 255
start_datedaterequired
end_datedaterequired — after or equal to start_date
everyintegerrequired — min:1
unitstringrequired — minutes|hours|days|weeks|months
quantityintegerrequired — min:1
descriptionstringoptional — max 1000

GET /users/{user}/medications/{medication}

Show medication with reminders.

PUT /users/{user}/medications/{medication}

Update medication. Recreates reminders based on new schedule.

DELETE /users/{user}/medications/{medication}

Delete medication. Fails if reminders exist. Returns 204.

POST /medications/reminders/{medicationReminder}/mark_as_taken

Mark a medication reminder as taken. Returns 204.

User Points (BettahPoints)

Automatic point awards: Registration (5 pts), Profile Completion (2 pts), First 10k Steps (5 pts), First 10km Distance (5 pts), Daily Step Bonus 15k+ (1 pt), First Transaction (5 pts), Subscription Payment (2 pts). All amounts configurable via admin settings.
These mobile user endpoints are self-service. The authenticated user's ID must match {user}; otherwise the API returns 403.

GET /users/{user}/points Auth Required

Get the user's BettahPoints balance, lifetime totals, current tier, and earning breakdown.

// Response 200
{
    "data": {
        "balance": 250,
        "lifetime_earned": 300,
        "lifetime_redeemed": 50,
        "tier": {
            "id": 1,
            "name": "Bronze",
            "min_points": 0,
            "multiplier": 1
        },
        "breakdown": {
            "by_type": {
                "registration": 5,
                "profile_completion": 2,
                "daily_step_bonus": 43,
                "event_award": 200
            },
            "by_event": [
                { "event_id": 5, "total": 200 }
            ]
        }
    }
}

GET /users/{user}/points/transactions Auth Required

Paginated transaction history (50/page). Filter: ?type=registration|daily_step_bonus|...

// Response 200
{
    "data": [
        {
            "id": 42,
            "type": "event_award",
            "amount": 100,
            "balance_after": 250,
            "description": "Awarded by Coach FitLife",
            "created_at": "2026-03-15T10:30:00Z",
            "event": { "id": 5, "title": "Spring Challenge" },
            "coach": { "id": 2, "name": "Coach FitLife" }
        }
    ],
    "links": { ... },
    "meta": { ... }
}

Coach Points

Coaches purchase point bundles, store them in their wallet, and award them to subscribers — either ad-hoc or as event prizes.

GET /coaches/{coach}/points

Get the coach's point wallet balance. Platform/BettahLife coaches return is_unlimited=true; their stored numeric balance is kept for compatibility and should not be shown as a spending limit.

// Response 200
{
    "data": {
        "balance": 5000,
        "is_unlimited": false,
        "lifetime_earned": 10000,
        "lifetime_redeemed": 0
    }
}

GET /coaches/{coach}/points/transactions

Paginated transaction history. Filter: ?type=coach_purchase|event_escrow|...

POST /coaches/{coach}/points/purchase

Initialize Monnify checkout for a coach point bundle purchase. This is the web/admin checkout path. Only the coach owner or an assigned coach admin may initialize checkout. The bundle must be active. Fulfillment happens through the Monnify webhook after payment confirmation.

ParameterTypeRules
bundle_idintegerrequired — must exist in point_bundles and be active
// Response 200
{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}

POST /coaches/{coach}/points/apple/verify

Verify an Apple in-app consumable point bundle purchase and credit the coach wallet exactly once. Only the coach owner or an assigned coach admin may verify a purchase for the coach.

ParameterTypeRules
bundle_idintegerrequired — active point bundle whose apple_product_id matches the transaction product
transaction_idstringRequired when signed_transaction_info is omitted
signed_transaction_infostringRequired when transaction_id is omitted
// Response 200
{
    "provider": "apple",
    "transaction": {
        "id": 99,
        "type": "coach_purchase",
        "amount": 1000,
        "balance_after": 5000,
        "metadata": {
            "provider": "apple",
            "provider_reference": "apple-transaction-id"
        }
    },
    "balance": { "data": { "balance": 5000, "is_unlimited": false } }
}

POST /coaches/{coach}/points/google/verify

Verify a Google Play consumable point bundle purchase and credit the coach wallet exactly once. The backend consumes the purchase when Google reports it as unconsumed.

ParameterTypeRules
bundle_idintegerrequired — active point bundle whose google_product_id matches product_id
purchase_tokenstringrequired
product_idstringrequired
package_namestringrequired — must match configured Google Play package

POST /coaches/{coach}/points/award

Ad-hoc award points from coach wallet to a subscriber. Regular coaches are debited atomically. Platform/BettahLife coaches are unlimited and can award points without reducing stored balance or creating coach debit transactions.

ParameterTypeRules
user_idintegerrequired — must exist
amountintegerrequired — min:1
event_idintegeroptional — associate with an event
descriptionstringoptional

Point Bundles

GET /point-bundles

List all active point bundles available for coach purchase.

// Response 200
{
    "data": [
        {
            "id": 1,
            "name": "Starter Pack",
            "points": 1000,
            "price": "9.99",
            "currency": "USD",
            "apple_product_id": "com.bettahme.points.starter",
            "google_product_id": "points_starter"
        },
        {
            "id": 2,
            "name": "Pro Pack",
            "points": 5000,
            "price": "39.99",
            "currency": "USD",
            "apple_product_id": "com.bettahme.points.pro",
            "google_product_id": "points_pro"
        }
    ]
}

Point Tiers

GET /point-tiers

List all point tiers in display order.

GET /point-tiers/{pointTier}

Show a single point tier.

Event Prizes

Escrow flow: When prizes are configured, the total point value is debited from the coach's wallet and held in escrow. After the event ends, prizes are distributed to leaderboard winners. Unclaimed prizes (e.g., fewer participants than prize slots) are refunded to the coach.

GET /events/{event}/prizes

List prize configuration for an event, ordered by place.

// Response 200
{
    "data": [
        { "id": 1, "place": 1, "points": 500, "label": "Gold" },
        { "id": 2, "place": 2, "points": 300, "label": "Silver" },
        { "id": 3, "place": 3, "points": 100, "label": "Bronze" }
    ]
}

POST /events/{event}/prizes

Set prizes for an event. Replaces all existing prizes. Escrows total from the coach's wallet.

ParameterTypeRules
prizesarrayrequired — min 1 item
prizes.*.placeintegerrequired — min:1
prizes.*.pointsintegerrequired — min:1
prizes.*.labelstringoptional — e.g., "Gold", "Silver"
// Request body
{
    "prizes": [
        { "place": 1, "points": 500, "label": "Gold" },
        { "place": 2, "points": 300, "label": "Silver" },
        { "place": 3, "points": 100, "label": "Bronze" }
    ]
}

PUT /events/{event}/prizes

Update prizes. Adjusts escrow difference (additional debit or partial refund).

POST /events/{event}/prizes/distribute

Distribute escrowed prizes to leaderboard winners after the event has ended. Returns 422 if the event has not ended yet.

// Response 200
{
    "message": "Prizes distributed successfully."
}

Partner Businesses

GET /partner-businesses

List all active partner businesses with their active offers.

// Response 200
{
    "data": [
        {
            "id": 1,
            "name": "Amazon",
            "description": "Online marketplace",
            "logo": "https://storage.example.com/logos/amazon.png",
            "website": "https://amazon.com",
            "is_active": true,
            "offers": [
                {
                    "id": 1,
                    "name": "$10 Gift Card",
                    "category": "gift_card",
                    "points_cost": 2000,
                    "value_description": "$10 Amazon Gift Card",
                    "in_stock": true
                }
            ]
        }
    ]
}

GET /partner-businesses/{partnerBusiness}

Show a partner business with all active offers.

Redemption Offers

Categories: gift_card — Digital codes delivered in-app/email • discount — Discount codes for partner stores • merchandise — Digital receipt/voucher to present at partner location

GET /redemption-offers

Browse all active offers (paginated, 50/page).

Query ParamDescription
categoryFilter: gift_card, discount, or merchandise
partner_business_idFilter by specific partner
// Response 200
{
    "data": [
        {
            "id": 1,
            "name": "$10 Amazon Gift Card",
            "category": "gift_card",
            "points_cost": 2000,
            "value_description": "$10 Amazon Gift Card",
            "monetary_value": "10.00",
            "image": "https://storage.example.com/offers/amazon-10.png",
            "in_stock": true,
            "terms": "Valid for 12 months from redemption date.",
            "partner_business": {
                "id": 1,
                "name": "Amazon"
            }
        }
    ]
}

GET /redemption-offers/{redemptionOffer}

Show full offer details with partner business info.

Redemptions

These mobile user endpoints are self-service. The authenticated user's ID must match {user}; otherwise the API returns 403.
Users must meet the minimum redemption threshold (default: 1,000 points) to redeem offers. The offer's points_cost is deducted immediately, then the API issues a pending voucher code and verification token for partner/staff fulfillment.

GET /users/{user}/redemptions Auth Required

User's redemption history (paginated, 50/page).

POST /users/{user}/redemptions Auth Required

Redeem an active offer. Deducts points immediately and generates a pending voucher code/receipt.

ParameterTypeRules
redemption_offer_idintegerrequired — must exist and be active
// Response 201
{
    "data": {
        "id": 1,
        "points_spent": 2000,
        "status": "pending",
        "code": "GC-A1B2C3D4",
        "verification_token": "uuid-token",
        "redeemed_at": "2026-03-19T15:00:00Z",
        "expires_at": null,
        "offer": {
            "id": 1,
            "name": "$10 Amazon Gift Card",
            "category": "gift_card",
            "partner_business": {
                "id": 1,
                "name": "Amazon"
            }
        }
    }
}
// Response 422 examples
{ "message": "This offer is out of stock." }
{ "message": "Minimum balance of 1000 points required for redemption." }
{ "message": "Insufficient points balance." }

GET /users/{user}/redemptions/{redemption} Auth Required

Show full redemption details with offer and partner info, including code and verification token. The redemption must belong to {user}; otherwise the API returns 404.

Subscriptions

Entitlement source of truth: premium access is determined from the subscriptions table. Apple and Google are the payment providers for in-app mobile subscriptions. Monnify is for web, admin, manual, and other off-app payments.

GET /subscription/plans Auth Required

List active subscription plans. Plans can represent platform premium, coach, course, or event entitlements.

Query ParamDescription
entitlement_typeOptional filter: platform, coach, course, or event
providerOptional filter: apple, google, monnify, or manual
billing_intervalOptional filter: monthly, yearly, or lifetime
subject_typeOptional filter: coach, course, or event
subject_idOptional filter paired with subject_type
// Response 200
{
    "data": [
        {
            "id": 1,
            "name": "Premium Monthly",
            "slug": "premium-monthly",
            "description": "BettahMe premium access",
            "entitlement_type": "platform",
            "billing_interval": "monthly",
            "store_product_type": "subscription",
            "subscribable_type": null,
            "subscribable_id": null,
            "price": 9.99,
            "currency": "USD",
            "apple_product_id": "com.bettahme.premium.monthly",
            "google_product_id": "premium_monthly",
            "monnify_product_code": "premium-monthly",
            "metadata": null
        }
    ]
}

GET /subscription/status Auth Required

Return the authenticated user's current platform premium entitlement and the stable account token the mobile client must pass to Apple/Google purchase flows.

// Response 200
{
    "has_premium_access": true,
    "subscription_account_token": "uuid-account-token",
    "subscription": {
        "id": 12,
        "provider": "apple",
        "status": "active",
        "plan": "Premium Monthly",
        "plan_id": 1,
        "subscribable_type": null,
        "subscribable_id": null,
        "starts_at": "2026-05-17T00:00:00.000000Z",
        "renews_at": "2026-06-17T00:00:00.000000Z",
        "expires_at": "2026-06-17T00:00:00.000000Z",
        "cancelled_at": null,
        "in_grace_period": false
    }
}

POST /payments/monnify/initialize Auth Required

Initialize an off-app Monnify checkout for an active plan. This should not be used to unlock digital subscription features inside iOS/Android in-app purchase flows.

ParameterTypeRules
plan_idintegerrequired — active plan
subject_typestringoptionalcoach, course, or event; required when the plan entitlement is not platform
subject_idintegeroptional — required when the plan entitlement is not platform
channelstringoptionalweb, admin, or manual; default web
// Response 200
{
    "checkout_url": "https://checkout.example.com/...",
    "payment_reference": "uuid-reference"
}

Mobile Store Verification

The mobile app must not trust client-side subscription status. Send store purchase data to these endpoints; the backend verifies ownership, product mapping, environment/package/bundle, stores raw provider payloads, and updates the user's subscription entitlement.

POST /subscription/apple/verify Auth Required

Verify an Apple App Store subscription transaction. The Apple purchase should include subscription_account_token from /subscription/status as the StoreKit appAccountToken.

ParameterTypeRules
transaction_idstringRequired when signed_transaction_info is omitted
signed_transaction_infostringRequired when transaction_id is omitted
signed_renewal_infostringoptional
plan_idintegeroptional — must be an active plan whose apple_product_id matches the Apple product
subject_typestringoptionalcoach, course, or event; validates product ownership for subject-scoped plans
subject_idintegeroptional — required with subject_type

Returns SubscriptionResource. Validation errors return 422 for unrecognized product IDs, bundle/environment mismatch, expired/refunded transactions, or account token ownership mismatch.

POST /subscription/google/verify Auth Required

Verify a Google Play Billing subscription purchase. The Google purchase should use subscription_account_token from /subscription/status as the obfuscated account id.

ParameterTypeRules
purchase_tokenstringrequired
product_idstringrequired — must match an active plan's google_product_id
package_namestringrequired — must match configured app package
plan_idintegeroptional — active plan id
subject_typestringoptionalcoach, course, or event; validates product ownership for subject-scoped plans
subject_idintegeroptional — required with subject_type

Returns SubscriptionResource. The backend acknowledges pending purchases where required.

Payments

Clients should treat payment and entitlement state as asynchronous. After checkout or store verification, read the tracking endpoints below until the expected terminal state is visible.

GET /payments/{paymentReference}

Get the current status of a payment reference belonging to the authenticated user or their coach. Pending Monnify transactions are verified on read when possible.

{
    "payment_reference": "uuid-reference",
    "type": "event_subscription",
    "status": "pending",
    "amount": "5000.00",
    "currency": "NGN",
    "fulfilled_at": null
}

Tracking & Audit

Every payment path writes a durable tracking record. Use provider references for idempotency, API polling endpoints for client state, and provider webhook/event rows for audit history.

Client Polling Map

FlowInitial ActionTrack WithTerminal/Useful States
Monnify checkout /payments/monnify/initialize or resource-specific checkout route GET /payments/{paymentReference} pending, paid, overpaid, failed, expired, cancelled
Platform subscription /subscription/apple/verify or /subscription/google/verify GET /subscription/status active, grace_period, cancelled, expired, failed
Coach subscription /users/{user}/coaches/{coach}/subscribe, then store verify for Apple/Google GET /coaches/{coach} and GET /subscription/plans?subject_type=coach&subject_id={coach} is_subscribed, active_subscription.status, current subscription_options
Coach point bundle /coaches/{coach}/points/purchase for Monnify, or store verify for Apple/Google GET /coaches/{coach}/points and GET /coaches/{coach}/points/transactions Updated balance and a coach_purchase transaction
Course/event/coach access Free/manual enrollment, Monnify fulfillment, or store subscription verification Resource response fields and entitlement checks have_subscribed, is_subscribed, and access decision reasons

Backend Tracking Records

TableWhat It TracksImportant Keys
plansCanonical product catalog for platform, coach, course, and event entitlements.slug, entitlement_type, billing_interval, store_product_type, subscribable_type, subscribable_id
subscriptionsCurrent entitlement lifecycle across Apple, Google, Monnify, and manual grants.provider, provider_subscription_id, status, expires_at, grace_ends_at, subscribable_type, subscribable_id
paymentsPayment records linked to plans and subscriptions.provider, provider_reference, status, paid_at
subscription_eventsProvider webhook/event audit trail for subscriptions.provider, provider_event_id, event_type, processed_at
apple_transactionsApple transaction payloads and renewal linkage.transaction_id, original_transaction_id, product_id, environment
google_play_purchasesGoogle subscription purchase payloads and acknowledgement state.purchase_token, latest_order_id, product_id, subscription_state
store_point_purchasesApple/Google point bundle purchase tracking and idempotency.provider, provider_reference, point_transaction_id, status
monnify_transactionsMonnify checkout, webhook, verification, and fulfillment state.payment_reference, transaction_reference, type, status, fulfilled_at, verified_at
point_transactionsUser and coach point ledger.type, amount, balance_after, idempotency_key, metadata
coach_user, course_user, event_userLegacy access/enrollment mirrors.access_source, subscription_id

Idempotency Rules

ProviderIdempotency KeyBehavior
Apple subscriptionsoriginalTransactionId for subscription, transactionId for paymentRepeated verification updates the same subscription/payment and does not duplicate entitlement grants.
Google subscriptionspurchase_token for subscription, order id when available for paymentRepeated verification refreshes state from Google and preserves one entitlement record.
Apple point bundlestransactionIdRepeated verification returns the existing coach_purchase transaction.
Google point bundlesorder id when available, otherwise purchase_tokenRepeated verification returns the existing coach_purchase transaction.
Monnifypayment_referenceWebhook retries and payment status polling do not duplicate fulfillment.

Access Revocation

Subscription-backed access is mirrored into legacy pivot tables with access_source=subscription and subscription_id. When a subscription expires, is refunded, or is revoked, only that subscription-attributed access is removed. Manual, free, direct, or invitation-based access remains intact.

If a user has more than one active subscription for the same subject, such as monthly and annual coach subscriptions during an upgrade, access remains granted until no active subscription remains.

Webhooks

POST /webhooks/monnify No Auth

Monnify webhook receiver. Monnify must send the monnify-signature header; the API validates the HMAC signature, verifies successful transactions when configured, updates matching payment references, mirrors subscription entitlements into payments/subscriptions, and preserves existing point/course/event/coach fulfillment.

POST /webhooks/apple/subscriptions No Auth

Apple App Store Server Notifications endpoint. Processes subscription renewals, cancellations, expirations, refunds, grace period, and billing retry notifications idempotently using Apple notification identifiers.

POST /webhooks/google/subscriptions No Auth

Google Play Real-time Developer Notifications endpoint. If GOOGLE_PLAY_PUBSUB_VERIFICATION_TOKEN is configured, pass it as ?token=.... The handler decodes Pub/Sub payloads, refreshes purchase state from Google, and records subscription events idempotently.

Point Transaction Types Reference

TypeDirectionDescription
registration+userAwarded on account registration
profile_completion+userAwarded when profile is fully completed
first_steps_milestone+userFirst time reaching 10,000 lifetime steps
first_transaction+userFirst payment transaction completed
daily_step_bonus+userDaily bonus for exceeding step threshold
subscription_payment+userPoints for subscription payment
event_award+user / -coachCoach awards points to subscriber
coach_purchase+coachCoach purchases a point bundle
event_escrow-coachPoints escrowed for event prizes
event_escrow_release+userEscrowed points distributed to winner
event_escrow_refund+coachUnclaimed escrow returned to coach
redemption-userPoints spent on a redemption offer
admin_adjustment±Manual adjustment by admin