Account endpoints reference · Base URL https://2fast.ng/api/
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.
Creates a new user account.
{
"fname": "Usman",
"lname": "Halal",
"email": "usman@example.com",
"phone": "08012345678",
"password": "mypassword1",
"transpin": "5532",
"state": "Lagos",
"account": "subscriber"
}
email normalized to lowercase; valid format required.phone normalized to 11-digit Nigerian format (accepts 080…, 234…, or 10-digit).password at least 6 characters.transpin exactly 4 digits; weak PINs (0000, 1234, …) rejected.{ "status": "success", "msg": "Registration Successfull" }
| msg | Reason |
|---|---|
Phone Number Already Exist | Phone already registered |
Email Already Exist | Email already registered |
Phone Number Must Be 11 Digits | Invalid phone format |
Transaction PIN Must Be 4 Digits | PIN length wrong |
Please Set A More Secured Transaction PIN | Weak PIN |
Invalid Email Format | Email malformed |
Password Must Be At Least 6 Characters | Password too short |
Authenticates by phone + password.
{ "phone": "08012345678", "accesspass": "mypassword1" }
{
"status": "success",
"msg": "Login Successful",
"name": "Usman Halal",
"phone": "08012345678"
}
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.
| msg | Reason |
|---|---|
Incorrect credentials. Attempt 1 of 5 | Wrong 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:00 | Locked for 5 minutes |
Account Blocked, Please Contact Customer Support | account_status not active |
Sends a 4-digit recovery code to the user's email via Resend (from support@2fast.ng).
{ "email": "usman@example.com" }
{ "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).
| msg | Reason |
|---|---|
Invalid Email Format | Email malformed |
Email Not Found | No user with that email |
Failed to send recovery email | Resend delivery failed |
Verifies a recovery code without changing the password.
{ "email": "usman@example.com", "code": "5678" }
{ "status": "success", "msg": "Code Verified Successfully" }
| msg | Reason |
|---|---|
Invalid Email Format | Email malformed |
Invalid Code | Code wrong / used / not found |
Code Expired | OTP older than 10 minutes |
Email Not Found | No user with that email |
Sets a new password using a valid recovery code (pending or already-verified).
{
"email": "usman@example.com",
"code": "5678",
"password": "mynewpassword1"
}
{ "status": "success", "msg": "Password Updated Successfully" }
The OTP row is marked used after the password update.
| msg | Reason |
|---|---|
Invalid Email Format | Email malformed |
Invalid Code | Code wrong / used / not found |
Code Expired | OTP older than 10 minutes |
Password Must Be At Least 6 Characters | Password too short |
Email Not Found | No user with that email |
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" }.
Generates a 4-digit email-verification code, sends it via Resend, and invalidates any previous pending code.
{}
{ "status": "success", "msg": "Verification code sent to u***@gmail.com" }
| msg | Reason |
|---|---|
Your email is already verified | No need to resend |
Failed to send email. Please try again | Mail server error |
User not found | Invalid API key |
Verifies the 4-digit code and marks the user's email as verified.
{ "code": "4821" }
{
"status": "success",
"msg": "Email verified successfully",
"email": "user@example.com"
}
| msg | Reason |
|---|---|
Verification code is required | Missing code field |
Invalid verification code | Code is not numeric |
Incorrect verification code. Please check your email and try again | Code doesn't match / expired |
Your email is already verified | Already verified |
Returns the authenticated user's profile.
{}
{
"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..."
}
Paginated list of the user's transactions, newest first.
{ "limit": 20, "offset": 0 }
{
"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"
}
]
}
Details for a single transaction by reference.
{ "ref": "DATA20260303001" }
{
"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.
Double-duty endpoint. Empty JSON body returns the current picture. Multipart body uploads a new one and deletes the previous file.
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.
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"
}
| msg | Reason |
|---|---|
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 |
Removes the current profile picture. Safe to call when none exists.
{}
{ "status": "success", "msg": "Profile picture removed successfully" }
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.
Returns all funding (virtual) accounts linked to the user.
{}
{
"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.
| msg | Reason |
|---|---|
Unable to fetch accounts | Database read error |
User not found | Invalid or missing API key |
Generates a Safehaven virtual account. Idempotent — returns the existing one if already generated.
{}
{
"status": "success",
"msg": "Safehaven virtual account generated successfully",
"account": "1234567890"
}
{
"status": "success",
"msg": "Account already generated",
"account": "1234567890"
}
| msg | Reason |
|---|---|
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 found | Invalid or missing API key |
Shows how much of today's spending limit remains. Advises whether to upgrade or wait for the reset.
{}
{
"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"
}
{
"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).
| msg | Reason |
|---|---|
User not found | Invalid or missing API key |
Returns the user's current tier plus all available tier levels.
{}
{
"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 }
]
}
| msg | Reason |
|---|---|
User not found | Invalid or missing API key |
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.
{ "tier": 2 } // optional; omit to return all tiers
{
"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.
Requests an upgrade. Tier 2 auto-approves if requirements are met; Tier 3 is queued for admin review.
{ "tier": 2 }
{
"status": "success",
"msg": "Congratulations! Your account has been upgraded to Tier 2.",
"auto_approved": true
}
{
"status": "success",
"msg": "Your Tier 3 upgrade request has been submitted for admin review.",
"auto_approved": false
}
| msg | Reason |
|---|---|
Invalid target tier. Use 2 or 3. | Body tier is not 2 or 3 |
Your account is already at Tier X or higher | Downgrade or same tier |
You already have a pending upgrade request for Tier X | Duplicate pending request |
Requirements not met. Missing: Phone Verified, BVN Verified | One or more required KYC items missing |
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.
{
"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.
| msg | Reason |
|---|---|
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 |
Verifies BVN against the user's registered name and date of birth via Monnify.
{ "bvn": "12345678901", "dob": "1990-05-14" }
{
"status": "success",
"msg": "BVN Verified Successfully",
"name_match": 100,
"dob_match": 100
}
| msg | Reason |
|---|---|
BVN Must Be 11 Digits | Wrong length / non-numeric |
Invalid date format. Use yyyy-mm-dd or dd-Mon-yyyy | Unrecognised DOB |
BVN already verified on this account | Already done previously |
BVN verification is unavailable. Please try again later. | Missing MONNIFY_TOKEN |
BVN verification service unreachable | Monnify network error |
BVN verification failed | Monnify rejected the request |
BVN details do not match your registered name | Name match < 50% |
Verifies NIN against the user's registered first/last name.
{ "nin": "12345678901" }
{
"status": "success",
"msg": "NIN Verified Successfully",
"name_match": 92
}
| msg | Reason |
|---|---|
NIN Must Be Numeric | Non-numeric characters |
NIN Must Be 11 Digits | Wrong length |
NIN already verified on this account | Already done previously |
NIN verification is unavailable. Please try again later. | Missing MONNIFY_TOKEN |
NIN verification service unreachable | Monnify network error |
NIN verification failed | Monnify rejected the request |
NIN details do not match your registered name | Name similarity < 50% |
Returns referral code, count, wallet balance and total earned.
{}
{
"status": "success",
"referral_code": "08012345678",
"referral_count": 4,
"referral_wallet": "1200.00",
"total_earned": "1500.00"
}
| msg | Reason |
|---|---|
User not found | Invalid or missing API key |
Moves funds from the referral wallet into the main wallet. Logs a REFTW… transaction.
{ "amount": 500, "transpin": "5532" }
{
"status": "success",
"msg": "Transfer Successful",
"main_wallet": "5500.00",
"referral_wallet": "700.00"
}
| msg | Reason |
|---|---|
Invalid Amount | Missing or non-positive amount |
Invalid Transaction PIN | PIN doesn't match |
Insufficient Referral Balance | Referral wallet < amount |
Transfer failed | Database update error |
Moves funds from the cashback wallet into the main wallet. Logs a CBTW… transaction.
{ "amount": 500, "transpin": "5532" }
{
"status": "success",
"msg": "Transfer Successful",
"main_wallet": "5500.00",
"cashback_wallet": "50.00"
}
| msg | Reason |
|---|---|
Invalid Amount | Missing or non-positive amount |
Invalid Transaction PIN | PIN doesn't match |
Insufficient Cashback Balance | Cashback wallet < amount |
Transfer failed | Database update error |
Transfers from the main wallet to another 2Fast user identified by phone number. Both sides get a transaction record.
{ "phone": "08099887766", "amount": 1000, "transpin": "5532" }
{
"status": "success",
"msg": "Transfer Successful",
"new_balance": 4000
}
| msg | Reason |
|---|---|
Invalid phone number | Phone couldn't be normalised |
Invalid Amount | Missing or non-positive amount |
Invalid Transaction PIN | PIN doesn't match |
You cannot transfer to yourself | Recipient phone equals sender |
Insufficient Balance | Main balance < amount |
Recipient not found | No 2Fast user with that phone |
Transfer failed | Database update error (rolled back) |
Lists all saved beneficiaries for the user, newest first.
{}
{
"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.
Saves a beneficiary. Duplicate phone updates the existing record (upsert on user_id + phone). Network is auto-detected from the phone prefix.
{ "phone": "08099887766", "name": "Ahmed" }
{ "status": "success", "msg": "Beneficiary Added" }
| msg | Reason |
|---|---|
Invalid phone number | Phone couldn't be normalised |
Name is required | Missing/empty name |
Failed to add beneficiary | Database write error |
Removes a saved beneficiary by its numeric id. Users can only delete their own beneficiaries.
{ "id": 12 }
{ "status": "success", "msg": "Beneficiary Removed" }
| msg | Reason |
|---|---|
Invalid beneficiary id | Missing or non-numeric id |
Beneficiary not found | ID doesn't exist or belongs to another user |
Failed to remove beneficiary | Database delete error |
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.
{}
{ "status": "success", "msg": "A verification code has been sent to u******@example.com" }
| msg | Reason |
|---|---|
User not found | Missing/invalid API key (HTTP 401) |
No email on file | Account has no email address |
Email service not configured | RESEND_API_KEY not set (HTTP 500) |
Failed to send email. Please try again | Resend rejected the send, or the code couldn't be stored |
Confirms the emailed code and sets a new 4-digit Transaction PIN. The code is consumed once used.
{ "code": "5678", "new_pin": "5532", "confirm_pin": "5532" }
{ "status": "success", "msg": "Transaction PIN Updated Successfully" }
| msg | Reason |
|---|---|
Verification code is required | code missing/empty |
Invalid verification code | Not 4 digits, or no pending code matches |
Verification code has expired. Please request a new one | Code older than 10 minutes |
New PIN and confirmation are required | new_pin or confirm_pin missing |
Transaction PIN Must Be 4 Digits | new_pin not exactly 4 digits |
PINs do not match | new_pin ≠ confirm_pin |
Please Set A More Secured Transaction PIN | Weak PIN (0000, 1234, 1111, ...) |
New PIN must be different from your current PIN | Same as existing PIN |
Failed to update Transaction PIN | Database write error (HTTP 500) |
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.
{
"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.
{
"status": "success",
"msg": "Your issue has been submitted. We will respond shortly.",
"ticket_id": 12
}
| msg | Reason |
|---|---|
Message is required | No message/query/issue in body |
Message must be at least 10 characters | Too short |
Message must be at most 2000 characters | Too long |
Failed to submit ticket | Database write error (HTTP 500) |
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.
{}
{
"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"
}
]
}
Returns a single ticket with all replies in order. Calling this endpoint automatically marks the ticket as read (user_read = true).
{ "ticket_id": 12 }
{
"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"
}
]
}
}
| msg | Reason |
|---|---|
Invalid ticket_id | Missing or non-numeric id |
Ticket not found | ID doesn't exist or belongs to another user (HTTP 404) |
Adds a user reply to an existing ticket. The ticket status is reopened to open and the admin is notified by email.
{ "ticket_id": 12, "message": "It has been resolved, thank you!" }
{ "status": "success", "msg": "Reply Sent Successfully" }
| msg | Reason |
|---|---|
Invalid ticket_id | Missing or non-numeric id |
Message is required | Empty message |
Message must be at most 2000 characters | Too long |
Ticket not found | Not the user's ticket (HTTP 404) |
This ticket is closed | Cannot reply to closed ticket |
Failed to send reply | Database write error (HTTP 500) |
Returns the number of unread tickets. Use it to render a notification badge on the support icon.
{}
{ "status": "success", "unread": 2 }
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.
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.
{}
{
"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"
}
}
Active notifications for this user, filtered to their account type plus all-user messages.
{}
{
"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" }
}
| Field | Description |
|---|---|
| notifications | Full list for the notification bell / inbox screen. |
| home_banner | Latest all-users notification for the home announcement bar. null if none set. |
| for | 1 = Subscribers, 2 = Agents, 3 = All Users |
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.
{}
{
"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"
}
]
}
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.
{}
{
"status": "success",
"site_color": "#00c896",
"login_design": "design1",
"home_design": "design2",
"app_bg_color": "#f5f5f5"
}
| Field | Description |
|---|---|
| site_color | Primary brand/accent colour — buttons, highlights, active states. |
| app_bg_color | Main app background colour — apply to the root screen. |
| login_design | Login screen design variant set by admin. |
| home_design | Home screen design variant set by admin. |
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.
{}
{
"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" }
]
}
]
}
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 }.
{} // or { "network_id": 1 }
{
"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.
Cable providers with their bouquets. Hide a provider whose providerStatus is "Off", and only show plans with status == "On".
{}
{
"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" }
]
}
]
}