2Fast API v1

Account endpoints reference · Base URL https://2fast.ng/api/

Authentication

All /account/* endpoints require a date token in the Authorization header, using today's UTC date in YYYYMMDD format:

Authorization: Token 20260824

Missing or mismatched token returns HTTP 401. All requests are POST with Content-Type: application/json.

POST/account/register

Creates a new user account.

Request body

{
  "fname": "Usman",
  "lname": "Halal",
  "email": "usman@example.com",
  "phone": "08012345678",
  "password": "mypassword1",
  "transpin": "5532",
  "state": "Lagos",
  "account": "subscriber"
}

Validation

Success

{ "status": "success", "msg": "Registration Successfull" }

Errors

msgReason
Phone Number Already ExistPhone already registered
Email Already ExistEmail already registered
Phone Number Must Be 11 DigitsInvalid phone format
Transaction PIN Must Be 4 DigitsPIN length wrong
Please Set A More Secured Transaction PINWeak PIN
Invalid Email FormatEmail malformed
Password Must Be At Least 6 CharactersPassword too short

POST/account/login

Authenticates by phone + password.

Request body

{ "phone": "08012345678", "accesspass": "mypassword1" }

Success

{
  "status": "success",
  "msg": "Login Successful",
  "name": "Usman Halal",
  "phone": "08012345678"
}

Rate limiting

Failed attempts are tracked per phone and per IP. After 5 fails in 5 minutes, the account/IP is locked for 5 minutes. Every attempt is logged with browser, OS, IP, and user-agent.

Errors

msgReason
Incorrect credentials. Attempt 1 of 5Wrong phone/password
Incorrect credentials. Attempt 4 of 5 ⚠️1–2 attempts left
Too many failed attempts. Please try again after 5 minutes at 14:30:00Locked for 5 minutes
Account Blocked, Please Contact Customer Supportaccount_status not active

POST/account/recover

Sends a 4-digit recovery code to the user's email via Resend (from support@2fast.ng).

Request body

{ "email": "usman@example.com" }

Success

{ "status": "success", "msg": "A 4-digit recovery code has been sent to your email" }

Stored in otps (otp_type=recovery, pending, expires in 10 minutes).

Errors

msgReason
Invalid Email FormatEmail malformed
Email Not FoundNo user with that email
Failed to send recovery emailResend delivery failed

POST/account/verify-code

Verifies a recovery code without changing the password.

Request body

{ "email": "usman@example.com", "code": "5678" }

Success

{ "status": "success", "msg": "Code Verified Successfully" }

Errors

msgReason
Invalid Email FormatEmail malformed
Invalid CodeCode wrong / used / not found
Code ExpiredOTP older than 10 minutes
Email Not FoundNo user with that email

POST/account/update-password

Sets a new password using a valid recovery code (pending or already-verified).

Request body

{
  "email": "usman@example.com",
  "code": "5678",
  "password": "mynewpassword1"
}

Success

{ "status": "success", "msg": "Password Updated Successfully" }

The OTP row is marked used after the password update.

Errors

msgReason
Invalid Email FormatEmail malformed
Invalid CodeCode wrong / used / not found
Code ExpiredOTP older than 10 minutes
Password Must Be At Least 6 CharactersPassword too short
Email Not FoundNo user with that email

User Endpoints — Authentication

All /user/* endpoints authenticate with the user's API key instead of the date token. Send it in the Authorization header:

Authorization: Bearer BAHCCvJ4y36...

The API key is returned by /account/login and shown on /user as api_key. Missing or unknown keys return { "status": "fail", "msg": "User not found" }.

POST/user/resend-verification

Generates a 4-digit email-verification code, sends it via Resend, and invalidates any previous pending code.

Request body

{}

Success

{ "status": "success", "msg": "Verification code sent to u***@gmail.com" }

Errors

msgReason
Your email is already verifiedNo need to resend
Failed to send email. Please try againMail server error
User not foundInvalid API key

POST/user/verify-email

Verifies the 4-digit code and marks the user's email as verified.

Request body

{ "code": "4821" }

Success

{
  "status": "success",
  "msg": "Email verified successfully",
  "email": "user@example.com"
}

Errors

msgReason
Verification code is requiredMissing code field
Invalid verification codeCode is not numeric
Incorrect verification code. Please check your email and try againCode doesn't match / expired
Your email is already verifiedAlready verified

POST/user

Returns the authenticated user's profile.

Request body

{}

Success

{
  "status": "success",
  "name": "Usman Halal",
  "fname": "Usman", "lname": "Halal",
  "email": "user@example.com",
  "phone": "08012345678",
  "state": "Lagos",
  "balance": "5000.00",
  "referral_wallet": "200.00",
  "cashback_wallet": "50.00",
  "referral_code": "08012345678",
  "account_tier": "tier1",
  "user_type": "subscriber",
  "kyc_verified": false,
  "api_key": "BAHCCvJ4y36..."
}

POST/user/transactions

Paginated list of the user's transactions, newest first.

Request body

{ "limit": 20, "offset": 0 }

Success

{
  "status": "success",
  "transactions": [
    {
      "tId": 101, "transref": "DATA20260303001",
      "servicename": "Data Bundle",
      "servicedesc": "1GB MTN data for 08012345678",
      "amount": 300.00, "status": 0,
      "oldbal": 5000.00, "newbal": 4700.00,
      "date": "2026-03-03 20:00:00"
    }
  ]
}

POST/user/transaction-details

Details for a single transaction by reference.

Request body

{ "ref": "DATA20260303001" }

Success

{
  "status": "success",
  "transaction": {
    "tId": 101, "transref": "DATA20260303001",
    "servicename": "Data Bundle",
    "servicedesc": "1GB MTN data for 08012345678",
    "amount": 300.00, "status": 0, "status_label": "Successful",
    "oldbal": 5000.00, "newbal": 4700.00,
    "date": "2026-03-03 20:00:00"
  }
}

Returns Transaction not found if the ref doesn't exist or belongs to another user — both cases look identical for security.

POST/user/profile-picture

Double-duty endpoint. Empty JSON body returns the current picture. Multipart body uploads a new one and deletes the previous file.

Get current picture

Content-Type: application/json
{}
{
  "status": "success",
  "has_picture": true,
  "image_path": "/user-uuid/profile_1234567890.jpg",
  "image_url": "https://.../signed-url"
}

When there's no picture, has_picture is false and both URL fields are null.

Upload

Content-Type: multipart/form-data
Field name: profile_picture
Allowed:    JPG, JPEG, PNG
Max size:   5MB
{
  "status": "success",
  "msg": "Profile picture updated successfully",
  "image_path": "/user-uuid/profile_1234567890.jpg",
  "image_url": "https://.../signed-url"
}

Errors

msgReason
Invalid file type. Only JPG and PNG allowed.Wrong file extension
File too large. Maximum size is 5MB.File exceeds 5MB
Uploaded file is not a valid image.File is empty or corrupt
Failed to save image. Check folder permissions.Server write error

POST/user/profile-picture/delete

Removes the current profile picture. Safe to call when none exists.

Request body

{}

Success

{ "status": "success", "msg": "Profile picture removed successfully" }

Example

curl -X POST https://2fast.ng/account/login \\
  -H "Content-Type: application/json" \\
  -H "Authorization: Token 20260824" \\
  -d '{"phone":"08012345678","accesspass":"mypassword1"}'

Replace 20260824 with today's UTC date in YYYYMMDD.


POST/user/funding-accounts

Returns all funding (virtual) accounts linked to the user.

Request body

{}

Success

{
  "status": "success",
  "accounts": [
    {
      "provider": "safehaven",
      "account_number": "1234567890",
      "account_name": "Usman Halal",
      "bank_name": "Safehaven Bank"
    }
  ]
}

Empty accounts array means the user hasn't generated a virtual account yet — call /user/generate-account.

Errors

msgReason
Unable to fetch accountsDatabase read error
User not foundInvalid or missing API key

POST/user/generate-account

Generates a Safehaven virtual account. Idempotent — returns the existing one if already generated.

Request body

{}

Success (newly created)

{
  "status": "success",
  "msg": "Safehaven virtual account generated successfully",
  "account": "1234567890"
}

Success (already existed)

{
  "status": "success",
  "msg": "Account already generated",
  "account": "1234567890"
}

Errors

msgReason
Unable to generate account at this time. Safehaven may be unavailable. Please try again later.Missing SAFEHAVEN_TOKEN, Safehaven API is down, upstream error, or DB insert failed
User not foundInvalid or missing API key

POST/user/daily-limit

Shows how much of today's spending limit remains. Advises whether to upgrade or wait for the reset.

Request body

{}

Success (limit not hit)

{
  "status": "success",
  "limit_hit": false,
  "action": "ok",
  "message": "You have ₦38,000.00 remaining of your daily limit.",
  "daily_limit": 50000,
  "spent_today": 12000,
  "remaining": 38000,
  "percent_used": 24,
  "current_tier": 1,
  "can_upgrade": true,
  "reset_in_seconds": 32400,
  "reset_in": "9h 0m",
  "resets_at": "2026-08-26T00:00:00.000Z"
}

Success (limit hit)

{
  "status": "success",
  "limit_hit": true,
  "action": "upgrade",
  "message": "You've reached your daily limit. Upgrade your account to increase it.",
  "daily_limit": 50000,
  "spent_today": 50000,
  "remaining": 0,
  "percent_used": 100,
  "current_tier": 1,
  "can_upgrade": true,
  "reset_in_seconds": 32400,
  "reset_in": "9h 0m"
}

action is ok (below limit), upgrade (limit hit, upgrade possible), or wait (limit hit, already max tier).

Errors

msgReason
User not foundInvalid or missing API key

POST/user/tier

Returns the user's current tier plus all available tier levels.

Request body

{}

Success

{
  "status": "success",
  "current_tier": 1,
  "daily_limit": 50000,
  "max_balance": 300000,
  "tier_levels": [
    { "tier": 1, "name": "Basic",    "daily_limit": 50000,   "requires_kyc": false },
    { "tier": 2, "name": "Standard", "daily_limit": 200000,  "requires_kyc": true  },
    { "tier": 3, "name": "Premium",  "daily_limit": 1000000, "requires_kyc": true  }
  ]
}

Errors

msgReason
User not foundInvalid or missing API key

POST/user/tier-requirements

Lists requirements for all tiers (or one specific tier). Authentication is optional — when the API key is sent, each requirement is marked as met/unmet.

Request body

{ "tier": 2 }   // optional; omit to return all tiers

Success (authenticated)

{
  "status": "success",
  "user_current_tier": 1,
  "tiers": [
    {
      "tier_level": 2,
      "tier_name": "Standard",
      "color": "#3B82F6",
      "daily_limit_formatted": "₦200,000.00",
      "max_balance_formatted": "₦500,000.00",
      "is_unlimited_balance": false,
      "requirements_count": 3,
      "requirements": [
        { "key": "email_verified", "label": "Email Verified", "description": "Verify your email address", "required": true, "met": true },
        { "key": "phone_verified", "label": "Phone Verified", "description": "Verify your phone number", "required": true, "met": false },
        { "key": "bvn",            "label": "BVN Verified",   "description": "Verify your BVN",           "required": true, "met": false }
      ],
      "all_requirements_met": false,
      "is_current_tier": false,
      "can_upgrade_to": true
    }
  ]
}

When called without an API key, the met, all_requirements_met, is_current_tier, can_upgrade_to, and user_current_tier fields are omitted.

POST/user/request-upgrade

Requests an upgrade. Tier 2 auto-approves if requirements are met; Tier 3 is queued for admin review.

Request body

{ "tier": 2 }

Success (Tier 2, auto-approved)

{
  "status": "success",
  "msg": "Congratulations! Your account has been upgraded to Tier 2.",
  "auto_approved": true
}

Success (Tier 3, pending review)

{
  "status": "success",
  "msg": "Your Tier 3 upgrade request has been submitted for admin review.",
  "auto_approved": false
}

Errors

msgReason
Invalid target tier. Use 2 or 3.Body tier is not 2 or 3
Your account is already at Tier X or higherDowngrade or same tier
You already have a pending upgrade request for Tier XDuplicate pending request
Requirements not met. Missing: Phone Verified, BVN VerifiedOne or more required KYC items missing

POST/user/submit-proof-of-address

Upload proof of address as multipart/form-data, field name proof_of_address. Required before Tier 3 upgrade. JPG/PNG/PDF, max 5MB. Any previously uploaded file is replaced.

Success

{
  "status": "success",
  "msg": "Proof of address uploaded successfully. It will be reviewed when you request a Tier 3 upgrade.",
  "file": "address_proof_1712345678901.pdf",
  "file_url": "https://.../signed-url",
  "proof_of_address_status": 0
}

proof_of_address_status: 0 pending, 1 approved, 2 rejected.

Errors

msgReason
Proof of address is only required for Tier 3 upgrade. Please upgrade to Tier 2 first.User is still Tier 1
Your account is already at Tier 3. No further proof of address is needed.Already max tier
No file uploaded. Please attach your proof of address document.Missing multipart or field
Invalid file type. Only JPG, PNG, and PDF are accepted.Wrong extension
File too large. Maximum size is 5MB.Exceeds 5MB
Uploaded file appears to be corrupt or invalid.Empty file
Failed to save file. Please check server folder permissions.Storage upload error

POST/user/verify-bvn

Verifies BVN against the user's registered name and date of birth via Monnify.

Request body

{ "bvn": "12345678901", "dob": "1990-05-14" }

Success

{
  "status": "success",
  "msg": "BVN Verified Successfully",
  "name_match": 100,
  "dob_match": 100
}

Errors

msgReason
BVN Must Be 11 DigitsWrong length / non-numeric
Invalid date format. Use yyyy-mm-dd or dd-Mon-yyyyUnrecognised DOB
BVN already verified on this accountAlready done previously
BVN verification is unavailable. Please try again later.Missing MONNIFY_TOKEN
BVN verification service unreachableMonnify network error
BVN verification failedMonnify rejected the request
BVN details do not match your registered nameName match < 50%

POST/user/verify-nin

Verifies NIN against the user's registered first/last name.

Request body

{ "nin": "12345678901" }

Success

{
  "status": "success",
  "msg": "NIN Verified Successfully",
  "name_match": 92
}

Errors

msgReason
NIN Must Be NumericNon-numeric characters
NIN Must Be 11 DigitsWrong length
NIN already verified on this accountAlready done previously
NIN verification is unavailable. Please try again later.Missing MONNIFY_TOKEN
NIN verification service unreachableMonnify network error
NIN verification failedMonnify rejected the request
NIN details do not match your registered nameName similarity < 50%

POST/user/referral-stats

Returns referral code, count, wallet balance and total earned.

Request body

{}

Success

{
  "status": "success",
  "referral_code": "08012345678",
  "referral_count": 4,
  "referral_wallet": "1200.00",
  "total_earned": "1500.00"
}

Errors

msgReason
User not foundInvalid or missing API key

POST/user/referral-to-wallet

Moves funds from the referral wallet into the main wallet. Logs a REFTW… transaction.

Request body

{ "amount": 500, "transpin": "5532" }

Success

{
  "status": "success",
  "msg": "Transfer Successful",
  "main_wallet": "5500.00",
  "referral_wallet": "700.00"
}

Errors

msgReason
Invalid AmountMissing or non-positive amount
Invalid Transaction PINPIN doesn't match
Insufficient Referral BalanceReferral wallet < amount
Transfer failedDatabase update error

POST/user/cashback-to-wallet

Moves funds from the cashback wallet into the main wallet. Logs a CBTW… transaction.

Request body

{ "amount": 500, "transpin": "5532" }

Success

{
  "status": "success",
  "msg": "Transfer Successful",
  "main_wallet": "5500.00",
  "cashback_wallet": "50.00"
}

Errors

msgReason
Invalid AmountMissing or non-positive amount
Invalid Transaction PINPIN doesn't match
Insufficient Cashback BalanceCashback wallet < amount
Transfer failedDatabase update error

POST/user/wallet-transfer

Transfers from the main wallet to another 2Fast user identified by phone number. Both sides get a transaction record.

Request body

{ "phone": "08099887766", "amount": 1000, "transpin": "5532" }

Success

{
  "status": "success",
  "msg": "Transfer Successful",
  "new_balance": 4000
}

Errors

msgReason
Invalid phone numberPhone couldn't be normalised
Invalid AmountMissing or non-positive amount
Invalid Transaction PINPIN doesn't match
You cannot transfer to yourselfRecipient phone equals sender
Insufficient BalanceMain balance < amount
Recipient not foundNo 2Fast user with that phone
Transfer failedDatabase update error (rolled back)

POST/user/beneficiaries

Lists all saved beneficiaries for the user, newest first.

Request body

{}

Success

{
  "status": "success",
  "beneficiaries": [
    { "id": 12, "phone": "08099887766", "name": "Ahmed", "network": "MTN" }
  ]
}

network is null when the phone prefix doesn't match a known Nigerian network.

POST/user/beneficiary-add

Saves a beneficiary. Duplicate phone updates the existing record (upsert on user_id + phone). Network is auto-detected from the phone prefix.

Request body

{ "phone": "08099887766", "name": "Ahmed" }

Success

{ "status": "success", "msg": "Beneficiary Added" }

Errors

msgReason
Invalid phone numberPhone couldn't be normalised
Name is requiredMissing/empty name
Failed to add beneficiaryDatabase write error

POST/user/beneficiary-delete

Removes a saved beneficiary by its numeric id. Users can only delete their own beneficiaries.

Request body

{ "id": 12 }

Success

{ "status": "success", "msg": "Beneficiary Removed" }

Errors

msgReason
Invalid beneficiary idMissing or non-numeric id
Beneficiary not foundID doesn't exist or belongs to another user
Failed to remove beneficiaryDatabase delete error

POST/user/request-pin-code

Sends a 4-digit code to the user's registered email to authorise a Transaction PIN change. Any previous pending PIN-change code is expired first. Code lifetime: 10 minutes.

Request body

{}

Success

{ "status": "success", "msg": "A verification code has been sent to u******@example.com" }

Errors

msgReason
User not foundMissing/invalid API key (HTTP 401)
No email on fileAccount has no email address
Email service not configuredRESEND_API_KEY not set (HTTP 500)
Failed to send email. Please try againResend rejected the send, or the code couldn't be stored

POST/user/update-pin

Confirms the emailed code and sets a new 4-digit Transaction PIN. The code is consumed once used.

Request body

{ "code": "5678", "new_pin": "5532", "confirm_pin": "5532" }

Success

{ "status": "success", "msg": "Transaction PIN Updated Successfully" }

Errors

msgReason
Verification code is requiredcode missing/empty
Invalid verification codeNot 4 digits, or no pending code matches
Verification code has expired. Please request a new oneCode older than 10 minutes
New PIN and confirmation are requirednew_pin or confirm_pin missing
Transaction PIN Must Be 4 Digitsnew_pin not exactly 4 digits
PINs do not matchnew_pinconfirm_pin
Please Set A More Secured Transaction PINWeak PIN (0000, 1234, 1111, ...)
New PIN must be different from your current PINSame as existing PIN
Failed to update Transaction PINDatabase write error (HTTP 500)

POST/support

Opens a new support ticket. An intelligent auto-reply is generated instantly from the message content (wallet funding delays, data/airtime delivery, cable, electricity tokens, PIN/login, refunds), and the admin is notified by email.

Request body

{
  "ref": "DATA20260303001",
  "message": "I bought data but customer did not receive it"
}

ref is optional and defaults to GENERAL. The message field is required (also accepts query or issue) and must be 10–2000 characters.

Success

{
  "status": "success",
  "msg": "Your issue has been submitted. We will respond shortly.",
  "ticket_id": 12
}

Errors

msgReason
Message is requiredNo message/query/issue in body
Message must be at least 10 charactersToo short
Message must be at most 2000 charactersToo long
Failed to submit ticketDatabase write error (HTTP 500)

POST/support/ticket

Lists the user's tickets (newest first, up to 100) with the most recent reply on each. Use user_read: false to show an unread indicator on the ticket card.

Request body

{}

Success

{
  "status": "success",
  "tickets": [
    {
      "id": 12,
      "ref": "DATA20260303001",
      "message": "I bought data but customer did not receive it",
      "status": "open",
      "user_read": false,
      "latest_reply": "So sorry for the inconveniences...",
      "replied_by": "Admin",
      "date": "2026-03-03 21:00:00"
    }
  ]
}

POST/support/view

Returns a single ticket with all replies in order. Calling this endpoint automatically marks the ticket as read (user_read = true).

Request body

{ "ticket_id": 12 }

Success

{
  "status": "success",
  "ticket": {
    "id": 12,
    "ref": "DATA20260303001",
    "message": "I bought data but customer did not receive it",
    "status": "open",
    "date": "2026-03-03 21:00:00",
    "replies": [
      {
        "id": 1,
        "reply": "So sorry for the inconveniences...",
        "replied_by": "Admin",
        "image": null,
        "date": "2026-03-03 21:00:01"
      }
    ]
  }
}

Errors

msgReason
Invalid ticket_idMissing or non-numeric id
Ticket not foundID doesn't exist or belongs to another user (HTTP 404)

POST/support/reply

Adds a user reply to an existing ticket. The ticket status is reopened to open and the admin is notified by email.

Request body

{ "ticket_id": 12, "message": "It has been resolved, thank you!" }

Success

{ "status": "success", "msg": "Reply Sent Successfully" }

Errors

msgReason
Invalid ticket_idMissing or non-numeric id
Message is requiredEmpty message
Message must be at most 2000 charactersToo long
Ticket not foundNot the user's ticket (HTTP 404)
This ticket is closedCannot reply to closed ticket
Failed to send replyDatabase write error (HTTP 500)

POST/support/unread

Returns the number of unread tickets. Use it to render a notification badge on the support icon.

Request body

{}

Success

{ "status": "success", "unread": 2 }

App Content Endpoints

All endpoints below take an empty body and are authenticated with the user's API key (Authorization: Token <api_key>). An invalid key returns { "status": "fail", "msg": "User not found" } with HTTP 401.

POST/api/contact

Returns the support/contact channels the admin filled in. Empty fields are excluded — loop through whatever comes back and render each key with its matching icon.

Request body

{}

Success

{
  "status": "success",
  "contact": {
    "phone": "08012345678",
    "email": "support@2fast.ng",
    "whatsapp": "08012345678",
    "whatsapp_group": "https://chat.whatsapp.com/xxxxx",
    "instagram": "https://instagram.com/halaltech",
    "facebook": "https://facebook.com/halaltech",
    "twitter": "https://twitter.com/halaltech",
    "telegram": "https://t.me/halaltech"
  }
}

POST/api/notifications

Active notifications for this user, filtered to their account type plus all-user messages.

Request body

{}

Success

{
  "status": "success",
  "count": 2,
  "notifications": [
    { "id": 5, "subject": "System Maintenance", "message": "Down for maintenance Sunday 10pm", "for": 3, "audience": "All Users" },
    { "id": 4, "subject": "New SME Plans", "message": "Cheaper SME data plans added", "for": 1, "audience": "Subscribers" }
  ],
  "home_banner": { "id": 5, "subject": "System Maintenance", "message": "Down for maintenance Sunday 10pm" }
}

Fields

FieldDescription
notificationsFull list for the notification bell / inbox screen.
home_bannerLatest all-users notification for the home announcement bar. null if none set.
for1 = Subscribers, 2 = Agents, 3 = All Users

POST/api/ads

Only active banners are returned. Use image_url directly in your Image component. If link is not empty, make the banner tappable and navigate to that URL.

Request body

{}

Success

{
  "status": "success",
  "count": 1,
  "banners": [
    {
      "id": 1, "title": "Promo Sale",
      "link": "https://yourdomain.com/promo",
      "status": "yes",
      "image_path": "/ads/banner_1234567890_1234.jpg",
      "image_url": "https://2fast.ng/ads/banner_1234567890_1234.jpg",
      "date_added": "2026-03-03 20:00:00"
    }
  ]
}

POST/api/app-settings

Call on app launch and cache the result. Re-fetch on each app resume so admin colour/theme changes take effect without a full app update.

Request body

{}

Success

{
  "status": "success",
  "site_color": "#00c896",
  "login_design": "design1",
  "home_design": "design2",
  "app_bg_color": "#f5f5f5"
}

Fields

FieldDescription
site_colorPrimary brand/accent colour — buttons, highlights, active states.
app_bg_colorMain app background colour — apply to the root screen.
login_designLogin screen design variant set by admin.
home_designHome screen design variant set by admin.

POST/api/networks

Check networkStatus first — if "Off", hide the entire network. Then loop data_types and only show tabs where status == "On". Network IDs: MTN 1, AIRTEL 2, GLO 3, T2 MOBILE 4.

Request body

{}

Success

{
  "status": "success",
  "networks": [
    {
      "id": 1, "network": "MTN",
      "networkStatus": "On",
      "vtuStatus": "On",
      "sharesellStatus": "On",
      "airtimepinStatus": "Off",
      "datapinStatus": "Off",
      "AirtimeToCash": "Off",
      "data_types": [
        { "name": "Daily", "slug": "Daily", "column": "dailyStatus", "status": "On" },
        { "name": "Monthly", "slug": "Monthly", "column": "monthlyStatus", "status": "On" },
        { "name": "Awoof", "slug": "Awoof", "column": "awoofStatus", "status": "Off" }
      ]
    }
  ]
}

POST/api/data-plans

Always use price — it is already the correct price for this user's account type (subscriber / agent / vendor). Optionally filter by network with { "network_id": 1 }.

Request body

{}   // or { "network_id": 1 }

Success

{
  "status": "success",
  "plans": [
    {
      "id": 1,
      "plan_id": "1001",
      "name": "500MB",
      "type": "SME",
      "day": 30,
      "price": 140.00,
      "userprice": 140.00,
      "agentprice": 135.00,
      "vendorprice": 130.00,
      "cashback": 5.00,
      "status": "On",
      "network_id": 1,
      "network": "MTN",
      "networkStatus": "On"
    }
  ]
}

The plans table also stores provider_name, provider_id and provider_price for admin/server use — these are never returned to the app.

POST/api/cabletv-plans

Cable providers with their bouquets. Hide a provider whose providerStatus is "Off", and only show plans with status == "On".

Request body

{}

Success

{
  "status": "success",
  "providers": [
    {
      "id": 1, "provider": "DSTV", "providerStatus": "On",
      "plans": [
        { "id": 1, "plan_id": "dstv-padi", "name": "DStv Padi", "day": 30, "price": 2500.00, "status": "On" }
      ]
    }
  ]
}

POST/api/airtime

Buys airtime using the user's wallet balance. Requires a valid API key (Authorization: Token YOUR_API_KEY or Bearer). The wallet is debited atomically before the provider is called — if the provider fails, the full amount is refunded automatically.

Request body

{
  "network": 1,
  "phone": "08012345678",
  "amount": 200,
  "airtime_type": "VTU",
  "ref": "AIR20260303001"
}
FieldRequiredDescription
networkYesNetwork ID: 1=MTN, 2=AIRTEL, 3=GLO, 4=T2 MOBILE
phoneYesRecipient phone number (11 digits, 234… also accepted)
amountYesAmount in naira, must be greater than 0
airtime_typeNoDefaults to VTU. Sent to the provider uppercased.
refNoYour unique reference. If omitted, one is auto-generated (AIR…). A ref already used in any transaction is rejected as a duplicate.

Every request acquires a lock on its ref before processing, so the same reference can never run twice in parallel.

Success (HTTP 200)

{
  "status": "success",
  "msg": "Airtime purchase successful"
}

msg is the provider's response message. The transaction is recorded with status Successful.

Failure responses

Most failures return HTTP 200 with "status": "fail" — always check the status field, not just the HTTP code.

HTTPmsgReason
401Missing or invalid API key
200Account Blocked, Please Contact Customer SupportAccount is not active
200Invalid Networknetwork missing or not a positive integer
200Invalid Phone Numberphone failed validation
200Invalid Amountamount missing, not a number, or ≤ 0
200Duplicate Referenceref already exists in transactions
409Duplicate Request, Please WaitSame ref is currently being processed
200Insufficient BalanceWallet balance is below amount
200provider message / service unavailableProvider failed — wallet was refunded, transaction marked Failed
500Unexpected error: …Server error; nothing was charged

POST/api/airtime-to-cash

Converts airtime into wallet cash. Works in 3 steps against the same endpoint. Requires a valid API key. (Also reachable at /api/Airtime-To-Cash.)

Common field for all steps: network (1=MTN, 2=AIRTEL, 3=GLO, 4=T2 MOBILE). An invalid network returns Invalid Network on every step.

Step 1 — Start conversion / request OTP

{
  "step": 1,
  "network": 1,
  "phone_number": "08012345678"
}

Success (step 1)

{
  "status": "success",
  "msg": "provider message"
}

If the network doesn't need an OTP, the response also includes "skip_otp": true and "identifier": "…" — in that case jump straight to step 3. Otherwise an OTP is sent to the phone and you continue with step 2.

Step 2 — Verify OTP

{
  "step": 2,
  "network": 1,
  "phone_number": "08012345678",
  "otp": "1234"
}

Success (step 2)

{
  "status": "success",
  "msg": "provider message",
  "identifier": "abc123",
  "airtime_balance": 5000
}

Keep the identifier — it is required for step 3.

Step 3 — Convert & credit wallet

{
  "step": 3,
  "network": 1,
  "identifier": "abc123",
  "amount": 500,
  "pin": "5532",
  "ref": "A2C20260303001"
}
FieldDescription
identifierFrom step 1 (skip_otp) or step 2 response
amountAmount of airtime being converted (naira)
pinThe user's transaction PIN
refOptional unique reference; auto-generated (A2C…) if omitted

On success the wallet is credited atomically and a Successful transaction is recorded. Duplicate ref values are rejected and every request is locked before processing.

Success (step 3)

{
  "status": "success",
  "msg": "provider response"
}

Failure responses

Most failures return HTTP 200 with "status": "fail" — check the status field.

HTTPmsgReason
401Missing or invalid API key
200Account Blocked, Please Contact Customer SupportAccount is not active
200Invalid Stepstep is not 1, 2 or 3
200Invalid Phone NumberSteps 1–2: bad phone_number
200Invalid OTPStep 2: otp must be 4–8 digits
200Invalid Identifier / Invalid Amount / Invalid PINStep 3: missing or invalid field
200Duplicate ReferenceStep 3: ref already used
409Duplicate Request, Please WaitAn identical request is being processed
200provider message / service unavailableProvider failed — no credit applied
500Conversion succeeded but wallet credit failed…Provider succeeded but credit failed — contact support