BettahMe 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 BettahCoins on registration.
| Parameter | Type | Rules |
|---|---|---|
firstname | string | required |
lastname | string | required |
email | string | required — unique |
phone | string | optional — unique, saved as the user's default mobile phone when provided |
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. Phone usernames are matched against saved phone numbers, ignoring common formatting characters.
| 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 | string | required — exactly 6 digits, including any leading zero |
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 BettahCoins balance.
PUT /users/{user}
Update a user. Awards 2 BettahCoins 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 |
engagement_notifications | boolean | required. Controls nudges, milestones, medication reminders, inactivity prompts, and weekly summaries. |
Notification Center
GET /users/{user}/notifications
List the authenticated user's non-dismissed notification center items. Supports Laravel pagination with optional per_page. The authenticated user must match {user}.
// Response 200
{
"data": [
{
"id": 42,
"coach": { "id": 1, "name": "BettahLife Living" },
"title": "Have you completed the BettahLife@well-being Survey?",
"body": "Your voice matters!",
"image_url": "https://cdn.example.com/notification-center/images/survey.jpg",
"action_label": "Click Link",
"action_url": "https://bit.ly/wellbeingatwork26",
"data": { "deep_link": "/survey" },
"read_at": null,
"published_at": "2026-07-17T10:00:00Z",
"created_at": "2026-07-17T09:55:00Z"
}
],
"meta": {
"unread_count": 1
}
}
GET /users/{user}/notifications/{notification}
View one notification center item. Returns 404 if the item is dismissed, unpublished, not assigned to the user, or the user is no longer a member of the coach.
POST /users/{user}/notifications/{notification}/read
Mark one notification as read. Returns the updated notification resource.
POST /users/{user}/notifications/read-all
Mark all currently visible notification center items as read.
// Response 200
{
"marked_read": 3
}
DELETE /users/{user}/notifications/{notification}
Dismiss one notification from the user's notification center. Returns 204.
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
{user}, and {phone} must belong to that user.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
{user}, and {address} must belong to that user.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 |
one_time_payment_fee | numeric | optional — one-time membership fee; null means unavailable, 0 means free |
monthly_subscription_fee | numeric | optional — monthly membership fee; null means unavailable, 0 means free |
annual_subscription_fee | numeric | optional — annual membership fee; null means unavailable, 0 means free |
membership_currency | string | optional — 3-letter currency code, defaults to NGN |
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, members, followers, documents, member/follower counts, active membership information for the authenticated user, and membership_options for one-time, monthly, and annual pricing.
// Excerpt
{
"id": 12,
"is_following": true,
"is_member": false,
"active_membership": null,
"followers_count": 145,
"members_count": 38,
"one_time_payment_fee": null,
"monthly_subscription_fee": 2000,
"annual_subscription_fee": 20000,
"membership_currency": "NGN",
"membership_options": {
"one_time": null,
"monthly": {
"plan_id": 101,
"price": 2000,
"currency": "NGN",
"payment_type": "monthly",
"billing_interval": "monthly",
"providers": {
"apple": { "product_id": "com.bettahme.coach.monthly" },
"google": { "product_id": "coach_monthly" },
"monnify": { "product_code": "coach-12-monthly" }
}
},
"annual": {
"plan_id": 102,
"price": 20000,
"currency": "NGN",
"payment_type": "annual",
"billing_interval": "yearly",
"providers": { ... }
}
}
}
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|member |
// Response 200
{
"id": 1,
"role": "member",
"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; member invites add a coach membership. Returns 410 if expired.
DELETE /coaches/{coach}/invitations/{token}
Decline an invitation. Returns 204.
Coach Follow & Membership
POST /users/{user}/coaches/{coach}/follow
Follow a coach for free. The authenticated user must match {user}. The endpoint is idempotent and returns the updated coach resource. The first follow queues push notifications for the coach owner and assigned coach admins.
// Response 200
{
"following": true,
"coach": { ... }
}
DELETE /users/{user}/coaches/{coach}/follow
Unfollow a coach. BettahLife/platform coach follow is locked and returns 403.
// Response 200
{
"following": false,
"coach": { ... }
}
POST /users/{user}/coaches/{coach}/memberships
Create a coach membership. If all coach fees are null, membership is free. When any fee is configured, the client must choose a payment_type. A selected fee of 0 creates the membership immediately; a positive fee returns payment or store product details and does not create membership until payment succeeds. When a new membership is created, including after successful Monnify or Apple/Google payment verification, coach owner and assigned coach admins receive push notifications.
| Parameter | Type | Rules |
|---|---|---|
payment_type | string | optional — required when fees are configured; one_time, monthly, or annual |
provider | string | optional — apple, google, or monnify; default monnify |
channel | string | optional — web, admin, manual, or mobile |
// Free-membership response
{
"member": true,
"coach": { ... }
}
// Monnify paid-membership 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",
"payment_type": "monthly"
}
DELETE /users/{user}/coaches/{coach}/memberships
Remove a user's manual/direct membership to a coach. Store-managed memberships must be cancelled in Apple/Google; this endpoint does not cancel store billing and only removes manual/direct local membership rows.
Events
GET /events
Paginated list (15/page) with extensive filtering.
| Query Param | Description |
|---|---|
registered | Filter to user's registered 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 registration 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 Registration
GET /users/{user}/events
List events the user is directly registered for. Each event includes access metadata such as can_access and access_reason.
POST /users/{user}/events/{event}/register
Register for an event. If the user already has access through direct registration, coach membership, or coach admin access, the endpoint returns immediate success. Free events attach the user directly. Paid events return checkout details instead of immediate registration.
// Immediate-access response
{
"registered": true,
"can_access": true,
"access_reason": "direct_event_registration"
}
// Paid-event response
{
"checkout_url": "https://checkout.example.com/...",
"payment_reference": "uuid-reference"
}
POST /users/{user}/events/{event}/unregister
Unregister from an event. Returns the user's remaining directly registered 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: ?enrolled, ?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, enrollment counts, 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 Enrollment
GET /users/{user}/courses
List courses the user is directly enrolled in. Each course includes access metadata such as can_access and access_reason.
POST /users/{user}/courses/{course}/enroll
Enroll in a course. If the user already has access through direct enrollment, coach membership, 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
{
"enrolled": 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}/unenroll
Unenroll from a course. Returns the user's remaining directly enrolled courses as a CourseResource collection.
Steps
{user}, and {step} must belong to that user.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 BettahCoin daily bonus if total exceeds 15,000 steps. Awards 5 BettahCoins 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
{user}, and {distance} must belong to that user.GET /users/{user}/distances
List distance records (latest first). Supports ?from, ?to, and ?per_page.
POST /users/{user}/distances
Record distance. Awards 5 BettahCoins 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 Coins (BettahCoins)
{user}; otherwise the API returns 403.
GET /users/{user}/coins Auth Required
Get the user's BettahCoins 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_coins": 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}/coins/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 Coins
GET /coaches/{coach}/coins
Get the coach's coin 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}/coins/transactions
Paginated transaction history. Filter: ?type=coach_purchase|event_escrow|...
POST /coaches/{coach}/coins/purchase
Initialize Monnify checkout for a coach coin 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 coin_bundles and be active |
// Response 200
{
"checkout_url": "https://checkout.example.com/...",
"payment_reference": "uuid-reference"
}
POST /coaches/{coach}/coins/apple/verify
Verify an Apple in-app consumable coin 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 coin 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}/coins/google/verify
Verify a Google Play consumable coin 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 coin 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}/coins/award
Ad-hoc award coins from coach wallet to a member. Regular coaches are debited atomically. Platform/BettahLife coaches are unlimited and can award coins 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 |
Coin Bundles
GET /coin-bundles
List all active coin bundles available for coach purchase.
// Response 200
{
"data": [
{
"id": 1,
"name": "Starter Pack",
"coins": 1000,
"price": "9.99",
"currency": "USD",
"apple_product_id": "com.bettahme.coins.starter",
"google_product_id": "coins_starter"
},
{
"id": 2,
"name": "Pro Pack",
"coins": 5000,
"price": "39.99",
"currency": "USD",
"apple_product_id": "com.bettahme.coins.pro",
"google_product_id": "coins_pro"
}
]
}
Coin Tiers
GET /coin-tiers
List all coin tiers in display order.
GET /coin-tiers/{coinTier}
Show a single coin tier.
Event Prizes
GET /events/{event}/prizes
List prize configuration for an event, ordered by place.
// Response 200
{
"data": [
{ "id": 1, "place": 1, "coins": 500, "label": "Gold" },
{ "id": 2, "place": 2, "coins": 300, "label": "Silver" },
{ "id": 3, "place": 3, "coins": 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.*.coins | integer | required — min:1 |
prizes.*.label | string | optional — e.g., "Gold", "Silver" |
// Request body
{
"prizes": [
{ "place": 1, "coins": 500, "label": "Gold" },
{ "place": 2, "coins": 300, "label": "Silver" },
{ "place": 3, "coins": 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",
"coins_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",
"coins_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.coins_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 coins 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,
"coins_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 coins required for redemption." }
{ "message": "Insufficient coins 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_registration",
"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 membership | /users/{user}/coaches/{coach}/memberships, then store verify for Apple/Google |
GET /coaches/{coach} and GET /subscription/plans?subject_type=coach&subject_id={coach} |
is_member, active_membership.status, current membership_options |
| Coach coin bundle | /coaches/{coach}/coins/purchase for Monnify, or store verify for Apple/Google |
GET /coaches/{coach}/coins and GET /coaches/{coach}/coins/transactions |
Updated balance and a coach_purchase transaction |
| Course/event/coach access | Free/manual enrollment or registration, Monnify fulfillment, or store subscription verification | Resource response fields and entitlement checks | have_registered, have_enrolled, is_member, 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_coin_purchases | Apple/Google coin bundle purchase tracking and idempotency. | provider, provider_reference, coin_transaction_id, status |
monnify_transactions | Monnify checkout, webhook, verification, and fulfillment state. | payment_reference, transaction_reference, type, status, fulfilled_at, verified_at |
coin_transactions | User and coach coin ledger. | type, amount, balance_after, idempotency_key, metadata |
coach_memberships, coach_followers | Coach membership access and free follow relationships. | access_source, subscription_id, coach_id, user_id |
course_user, event_user | Course enrollment and event registration 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 coin bundles | transactionId | Repeated verification returns the existing coach_purchase transaction. |
| Google coin 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 membership, enrollment, and registration 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 memberships 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 coin/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.
Coin 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 | Coins for subscription payment |
event_award | +user / -coach | Coach awards coins to member |
coach_purchase | +coach | Coach purchases a coin bundle |
event_escrow | -coach | Coins escrowed for event prizes |
event_escrow_release | +user | Escrowed coins distributed to winner |
event_escrow_refund | +coach | Unclaimed escrow returned to coach |
redemption | -user | Coins spent on a redemption offer |
admin_adjustment | ± | Manual adjustment by admin |