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