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"]
}
}
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
204 | No Content (successful delete) |
401 | Unauthenticated |
403 | Forbidden |
404 | Not Found |
410 | Gone (expired resource) |
422 | Validation Error |
500 | Server Error |
Auth
POST /login Public
Authenticate a user and receive an API token.
| Parameter | Type | Rules |
|---|---|---|
username | string | required — email or phone number |
password | string | required |
// 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.
| Parameter | Type | Rules |
|---|---|---|
firstname | string | required |
lastname | string | required |
email | string | required — unique |
password | string | required — confirmed, min:8 |
password_confirmation | string | required |
date_of_birth | date | required |
gender | string | required — male|female |
country_code | string | required |
nickname | string | optional |
occupation | string | optional |
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.
| Parameter | Type | Rules |
|---|---|---|
username | string | required — email or phone |
POST /password_reset/verify_code Public
Verify the OTP code.
| Parameter | Type | Rules |
|---|---|---|
username | string | required |
code | integer | required — 6 digits |
POST /password_reset/new_password Public
Set a new password after OTP verification.
| Parameter | Type | Rules |
|---|---|---|
username | string | required |
password | string | required — confirmed |
password_confirmation | string | required |
Users
GET /users
Paginated list of users (50 per page).
POST /users
Create a user (admin use).
| Parameter | Type | Rules |
|---|---|---|
email | string | required — unique |
firstname | string | required |
lastname | string | required |
gender | string | required — male|female |
country_code | string | required |
date_of_birth | date | required |
password | string | required — confirmed |
nickname | string | optional |
occupation | string | optional |
national_id | string | optional — 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
POST /user/photo/presign
Create a signed upload URL for the authenticated user's profile photo.
| Parameter | Type | Rules |
|---|---|---|
filename | string | required — max:255 |
content_type | string | required — image/jpeg or image/png |
size | integer | required — 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.
| Parameter | Type | Rules |
|---|---|---|
key | string | required — key returned by presign |
filename | string | required |
content_type | string | required |
size | integer | required |
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.
| Parameter | Type | Rules |
|---|---|---|
event_reminders | boolean | required |
sms_notifications | boolean | required |
push_notifications | boolean | required |
email_notifications | boolean | required |
newsletter_subscription | boolean | required |
marketing_sms_notifications | boolean | required |
marketing_email_notifications | boolean | required |
marketing_push_notifications | boolean | required |
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.
| Parameter | Type | Rules |
|---|---|---|
token | string | required — max:512, unique token is upserted |
platform | string | required — ios|android |
device_id | string | optional |
app_version | string | optional |
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.
| Parameter | Type | Rules |
|---|---|---|
height | numeric | optional |
weight | numeric | optional |
allergies | string | optional |
blood_type | string | optional |
medication | string | optional |
known_ailments | string | optional |
favorite_sport | string | optional |
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
| Parameter | Type | Rules |
|---|---|---|
type | string | required |
number | string | required — unique |
label | string | optional |
is_default | boolean | optional |
country_code | string | optional |
has_whatsapp | boolean | optional |
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
| Parameter | Type | Rules |
|---|---|---|
type | string | required — home|work|other |
street1 | string | required |
city | string | required |
state | string | required |
postal_code | string | required |
country_code | string | required |
street2 | string | optional |
label | string | optional |
is_default | boolean | optional |
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
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
| Parameter | Type | Rules |
|---|---|---|
user_id | integer | required |
name | string | required |
email | string | required — unique |
description | string | required |
address | string | required |
phone | string | required — unique |
type | string | required — individual or organization |
subscription_monthly_price | numeric | optional — monthly paid coach subscription amount |
subscription_annual_price | numeric | optional — annual paid coach subscription amount |
subscription_currency | string | optional — 3-letter currency code, defaults to NGN |
subscription_price | numeric | optional — legacy monthly price fallback |
tags | string | optional |
secondary_phone | string | optional |
x, tiktok, website, youtube, facebook, linkedin, instagram | string | optional — 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 Logo & Cover Photo
POST /coaches/{coach}/media/presign
Create a signed upload URL for a coach logo or cover photo.
| Parameter | Type | Rules |
|---|---|---|
type | string | required — logo or cover_photo; legacy cover_photos is normalized to cover_photo |
filename | string | required — max:255 |
content_type | string | required — image/jpeg, image/png, or image/webp |
size | integer | required — bytes, max photo upload limit |
POST /coaches/{coach}/media/confirm
Confirm a direct upload for coach logo or cover photo. Replaces the previous media object of the same type. Returns CoachResource.
| Parameter | Type | Rules |
|---|---|---|
key | string | required — key returned by presign |
type, filename, content_type, size | mixed | required — same values used for presign |
POST /coaches/{coach}/logo?type={logo|cover_photo}
Legacy multipart upload for a logo or cover photo. Field: file. Accepted: jpg, jpeg, png, webp. Max size: 10M. The legacy cover_photos query value is also accepted.
DELETE /coaches/{coach}/logo?type={logo|cover_photo}
Remove logo or cover photo. Returns 204.
Coach Documents
POST /coaches/{coach}/documents/presign
Create a signed upload URL for a coach document.
| Parameter | Type | Rules |
|---|---|---|
type | string | required — letters, numbers, underscore, and dash only |
filename | string | required — max:255 |
content_type | string | required — max:255 |
size | integer | required — 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
GET /coaches/{coach}/invitations
List all invitations for a coach.
POST /coaches/{coach}/invitations
Send an invitation.
| Parameter | Type | Rules |
|---|---|---|
email | string | required without phone |
phone | string | required without email |
role | string | required — 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.
| Parameter | Type | Rules |
|---|---|---|
billing_interval | string | optional — monthly or yearly; default monthly |
provider | string | optional — apple, google, or monnify; default monnify |
channel | string | optional — web, 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 Param | Description |
|---|---|
subscribed | Filter to user's subscribed events only |
status | draft, published, or canceled |
period | upcoming, ongoing, or past |
coach_id | Filter by coach |
event_type_id | Filter by event type |
event_category_id | Filter by category |
event_frequency_id | Filter by frequency |
location | Wildcard search on address |
searchword | Wildcard search on title, description, tags |
start_date / end_date | Date 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
| Parameter | Type | Rules |
|---|---|---|
coach_id | integer | optional when the authenticated user already belongs to a coach; otherwise required |
created_by | integer | set automatically from the authenticated user |
event_type_id | integer | required |
event_category_id | integer | required |
event_frequency_id | integer | required |
title | string | required |
description | string | required |
start_date | date | required |
end_date | date | optional |
goal, link, address, tags | string | optional |
latitude, longitude | numeric | optional |
expected_attendance | integer | optional |
leaderboard_type | string | optional |
status | string | optional — draft, 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
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.
| Parameter | Type | Rules |
|---|---|---|
filename | string | required — max:255 |
content_type | string | required — max:255 |
size | integer | required — 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.
| Parameter | Type | Rules |
|---|---|---|
key | string | required — key returned by presign |
filename, content_type, size | mixed | required — 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 Param | Default | Description |
|---|---|---|
period | overall | overall, daily, or weekly |
date | today | YYYY-MM-DD for daily; YYYY-W## for weekly |
limit | 100 | Max 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
| Parameter | Type | Rules |
|---|---|---|
coach_id | integer | optional when the authenticated user already belongs to a coach; otherwise required |
title | string | required — max 255 |
description | string | required |
goal | string | optional |
tags | string | optional |
price | numeric | optional — leave empty for a free course |
currency | string | optional — 3-letter code, defaults to NGN |
status | string | optional — draft, 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.
| Parameter | Type | Rules |
|---|---|---|
title | string | required |
description | string | optional |
type | string | required |
is_free | boolean | optional — 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.
| Parameter | Type | Rules |
|---|---|---|
filename | string | required — max:255 |
content_type | string | required — common video, audio, image, or PDF MIME type |
size | integer | required — 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.
| Parameter | Type | Rules |
|---|---|---|
key | string | required — key returned by presign |
filename | string | required |
content_type | string | required |
size | integer | required — 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.
| Parameter | Type | Rules |
|---|---|---|
recorded_at | date | required |
count | integer | required — min:0 |
unit | string | optional — default: steps |
source | string | optional |
device | string | optional |
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.
| Parameter | Type | Rules |
|---|---|---|
recorded_at | date | required |
amount | numeric | required — min:0 |
unit | string | optional — km|miles (default: km) |
source | string | optional |
device | string | optional |
activity_type | string | optional |
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.
| Parameter | Type | Rules |
|---|---|---|
title | string | required — max 255 |
start_date | date | required |
end_date | date | required — after or equal to start_date |
every | integer | required — min:1 |
unit | string | required — minutes|hours|days|weeks|months |
quantity | integer | required — min:1 |
description | string | optional — 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)
{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
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.
| Parameter | Type | Rules |
|---|---|---|
bundle_id | integer | required — 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.
| Parameter | Type | Rules |
|---|---|---|
bundle_id | integer | required — active point bundle whose apple_product_id matches the transaction product |
transaction_id | string | Required when signed_transaction_info is omitted |
signed_transaction_info | string | Required 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.
| Parameter | Type | Rules |
|---|---|---|
bundle_id | integer | required — active point bundle whose google_product_id matches product_id |
purchase_token | string | required |
product_id | string | required |
package_name | string | required — 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.
| Parameter | Type | Rules |
|---|---|---|
user_id | integer | required — must exist |
amount | integer | required — min:1 |
event_id | integer | optional — associate with an event |
description | string | optional |
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
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.
| Parameter | Type | Rules |
|---|---|---|
prizes | array | required — min 1 item |
prizes.*.place | integer | required — min:1 |
prizes.*.points | integer | required — min:1 |
prizes.*.label | string | optional — 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
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 Param | Description |
|---|---|
category | Filter: gift_card, discount, or merchandise |
partner_business_id | Filter 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
{user}; otherwise the API returns 403.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.
| Parameter | Type | Rules |
|---|---|---|
redemption_offer_id | integer | required — 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
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 Param | Description |
|---|---|
entitlement_type | Optional filter: platform, coach, course, or event |
provider | Optional filter: apple, google, monnify, or manual |
billing_interval | Optional filter: monthly, yearly, or lifetime |
subject_type | Optional filter: coach, course, or event |
subject_id | Optional 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.
| Parameter | Type | Rules |
|---|---|---|
plan_id | integer | required — active plan |
subject_type | string | optional — coach, course, or event; required when the plan entitlement is not platform |
subject_id | integer | optional — required when the plan entitlement is not platform |
channel | string | optional — web, admin, or manual; default web |
// Response 200
{
"checkout_url": "https://checkout.example.com/...",
"payment_reference": "uuid-reference"
}
Mobile Store Verification
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.
| Parameter | Type | Rules |
|---|---|---|
transaction_id | string | Required when signed_transaction_info is omitted |
signed_transaction_info | string | Required when transaction_id is omitted |
signed_renewal_info | string | optional |
plan_id | integer | optional — must be an active plan whose apple_product_id matches the Apple product |
subject_type | string | optional — coach, course, or event; validates product ownership for subject-scoped plans |
subject_id | integer | optional — 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.
| Parameter | Type | Rules |
|---|---|---|
purchase_token | string | required |
product_id | string | required — must match an active plan's google_product_id |
package_name | string | required — must match configured app package |
plan_id | integer | optional — active plan id |
subject_type | string | optional — coach, course, or event; validates product ownership for subject-scoped plans |
subject_id | integer | optional — required with subject_type |
Returns SubscriptionResource. The backend acknowledges pending purchases where required.
Payments
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
Client Polling Map
| Flow | Initial Action | Track With | Terminal/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
| Table | What It Tracks | Important Keys |
|---|---|---|
plans | Canonical product catalog for platform, coach, course, and event entitlements. | slug, entitlement_type, billing_interval, store_product_type, subscribable_type, subscribable_id |
subscriptions | Current entitlement lifecycle across Apple, Google, Monnify, and manual grants. | provider, provider_subscription_id, status, expires_at, grace_ends_at, subscribable_type, subscribable_id |
payments | Payment records linked to plans and subscriptions. | provider, provider_reference, status, paid_at |
subscription_events | Provider webhook/event audit trail for subscriptions. | provider, provider_event_id, event_type, processed_at |
apple_transactions | Apple transaction payloads and renewal linkage. | transaction_id, original_transaction_id, product_id, environment |
google_play_purchases | Google subscription purchase payloads and acknowledgement state. | purchase_token, latest_order_id, product_id, subscription_state |
store_point_purchases | Apple/Google point bundle purchase tracking and idempotency. | provider, provider_reference, point_transaction_id, status |
monnify_transactions | Monnify checkout, webhook, verification, and fulfillment state. | payment_reference, transaction_reference, type, status, fulfilled_at, verified_at |
point_transactions | User and coach point ledger. | type, amount, balance_after, idempotency_key, metadata |
coach_user, course_user, event_user | Legacy access/enrollment mirrors. | access_source, subscription_id |
Idempotency Rules
| Provider | Idempotency Key | Behavior |
|---|---|---|
| Apple subscriptions | originalTransactionId for subscription, transactionId for payment | Repeated verification updates the same subscription/payment and does not duplicate entitlement grants. |
| Google subscriptions | purchase_token for subscription, order id when available for payment | Repeated verification refreshes state from Google and preserves one entitlement record. |
| Apple point bundles | transactionId | Repeated verification returns the existing coach_purchase transaction. |
| Google point bundles | order id when available, otherwise purchase_token | Repeated verification returns the existing coach_purchase transaction. |
| Monnify | payment_reference | Webhook 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
| Type | Direction | Description |
|---|---|---|
registration | +user | Awarded on account registration |
profile_completion | +user | Awarded when profile is fully completed |
first_steps_milestone | +user | First time reaching 10,000 lifetime steps |
first_transaction | +user | First payment transaction completed |
daily_step_bonus | +user | Daily bonus for exceeding step threshold |
subscription_payment | +user | Points for subscription payment |
event_award | +user / -coach | Coach awards points to subscriber |
coach_purchase | +coach | Coach purchases a point bundle |
event_escrow | -coach | Points escrowed for event prizes |
event_escrow_release | +user | Escrowed points distributed to winner |
event_escrow_refund | +coach | Unclaimed escrow returned to coach |
redemption | -user | Points spent on a redemption offer |
admin_adjustment | ± | Manual adjustment by admin |