MonieRemit API Guide

For frontend and mobile developers. This document covers every active endpoint, the exact flows to implement, and what to expect in every response.


Base URL

https://monieremit-api.up.railway.app

All endpoints are prefixed with /api/v1.


Response Format

Every response shares the same envelope, regardless of success or failure.

Success

{
  "apiObject": "Auth",
  "code": 200,
  "status": "success",
  "message": "Success.",
  "result": { ... }
}

Failure

{
  "apiObject": "Auth",
  "code": 400,
  "status": "failure",
  "message": "Human-readable error description",
  "error": { ... },
  "result": {}
}

Validation failureerror contains Zod's flattened field errors:

{
  "apiObject": "Auth",
  "code": 400,
  "status": "failure",
  "message": "Validation error",
  "error": {
    "fieldErrors": { "phone": ["Invalid phone number"] },
    "formErrors": []
  },
  "result": {}
}
Field Always present Notes
apiObject yes Identifies the resource type — Auth, Customer, Disbursement, Admin, etc.
code yes Mirrors the HTTP status code
status yes "success" or "failure"
message yes Human-readable description. Defaults to "Success." on success.
result yes Payload on success, {} on failure
error failure only Additional error detail (e.g. field-level validation errors)

BigInt note: Any field named amountKobo or amount comes back as a string (e.g. "250000"), not a number. Parse it as needed on the client. All amounts are in kobo (1 NGN = 100 kobo).


Authentication

Protected endpoints require a Bearer token in the Authorization header:

Authorization: Bearer <accessToken>

Access tokens expire after 15 minutes. When they expire, use the refresh token to get a new pair (see Refresh Token).


Endpoint Constants

Suggested mapping to keep endpoint strings out of your components. Set BASE_API_ENDPOINT from your environment config and import API_ENDPOINTS wherever you make requests.

export const BASE_API_ENDPOINT = "https://monieremit-api.up.railway.app/api/v1"

// Routes that share the same URL (GET + POST, or GET + PUT + DELETE) use a single
// entry named after the resource — not the verb. For /:id patterns use ById.
export const API_ENDPOINTS = {
  Auth: {
    Register:     `${BASE_API_ENDPOINT}/auth/register`,
    SendOtp:      `${BASE_API_ENDPOINT}/auth/send-otp`,
    VerifyOtp:    `${BASE_API_ENDPOINT}/auth/verify-otp`,
    VerifyBvn:    `${BASE_API_ENDPOINT}/auth/verify-bvn`,
    Login:        `${BASE_API_ENDPOINT}/auth/login`,
    RefreshToken: `${BASE_API_ENDPOINT}/auth/refresh-token`,
  },
  Customer: {
    CreateAccount: `${BASE_API_ENDPOINT}/customers/create-account`,
    Me:            `${BASE_API_ENDPOINT}/customers/me`,
    Balance:       `${BASE_API_ENDPOINT}/customers/balance`,
    Transactions:  `${BASE_API_ENDPOINT}/customers/transactions`,
  },
  Disbursement: {
    Plan:    `${BASE_API_ENDPOINT}/disbursements/plan`,   // POST (upsert) + GET
    History: `${BASE_API_ENDPOINT}/disbursements/history`,
  },
  Admin: {
    Customers:        `${BASE_API_ENDPOINT}/admin/customers`,
    CustomerById:     (id: string) => `${BASE_API_ENDPOINT}/admin/customers/${id}`,
    FreezeCustomer:   (id: string) => `${BASE_API_ENDPOINT}/admin/customers/${id}/freeze`,
    UnfreezeCustomer: (id: string) => `${BASE_API_ENDPOINT}/admin/customers/${id}/unfreeze`,
    Disbursements:    `${BASE_API_ENDPOINT}/admin/disbursements`,
    FailedJobs:       `${BASE_API_ENDPOINT}/admin/jobs/failed`,
  },
}

Registration Flow

New users go through a 4-step pipeline. Phone ownership (OTP) is confirmed before any KYC (BVN) lookup is triggered. Step 4 requires the access token issued in step 3.

Step 1 - Register

POST /api/v1/auth/register

Body

{
  "firstName": "Adaeze",
  "lastName": "Okafor",
  "phone": "08012345678",
  "password": "securepassword",
  "dateOfBirth": "1995-06-20",
  "gender": "female",
  "address": "12 Marina Road, Lagos",
  "email": "adaeze@example.com",
  "otherNames": "Grace"
}
Field Type Required Notes
firstName string yes
lastName string yes
phone string yes Nigerian phone number
password string yes Min 8 characters
dateOfBirth string yes YYYY-MM-DD
gender string yes "male" or "female"
address string yes Min 5 characters
email string no
otherNames string no

Success response 201

{
  "status": "success",
  "result": {
    "customerId": "3f2a1b4c-..."
  }
}

Save customerId - you need it in step 3.


Step 2 - Send OTP

POST /api/v1/auth/send-otp

Body

{
  "phone": "08012345678"
}

An OTP is sent to the phone via Termii SMS. OTP expires after 10 minutes.

Success response 200

{
  "status": "success",
  "result": {
    "pinId": "c8dcd048-..."
  }
}

Save pinId - you need it in step 3.


Step 3 - Verify OTP

POST /api/v1/auth/verify-otp

Body

{
  "customerId": "3f2a1b4c-...",
  "pinId": "c8dcd048-...",
  "pin": "481923"
}

Success response 200

{
  "status": "success",
  "result": {
    "accessToken": "eyJhbGci...",
    "refreshToken": "eyJhbGci..."
  }
}

Store both tokens. Use accessToken in the Authorization header for step 4 and all subsequent requests.


Step 4 - Verify BVN

POST /api/v1/auth/verify-bvn

Requires Authorization: Bearer <accessToken> from step 3.

No customerId in the body - the server reads it from your token.

Body

{
  "bvn": "22345678901"
}

The BVN is looked up against the Qore/NIBSS registry. If the BVN is already linked to another account, this step returns an error.

Success response 200

{
  "status": "success",
  "result": {
    "firstName": "ADAEZE",
    "lastName": "OKAFOR",
    "phone": "08012345678"
  }
}

You can show the returned name on-screen to let the user confirm their identity before proceeding to account creation.


Login

POST /api/v1/auth/login

Body

{
  "phone": "08012345678",
  "password": "securepassword"
}

Success response 200

{
  "status": "success",
  "result": {
    "accessToken": "eyJhbGci...",
    "refreshToken": "eyJhbGci..."
  }
}

Refresh Token

POST /api/v1/auth/refresh-token

Call this when you get a 401 on any protected endpoint. The old refresh token is invalidated and a new pair is issued.

Body

{
  "refreshToken": "eyJhbGci..."
}

Success response 200

{
  "status": "success",
  "result": {
    "accessToken": "eyJhbGci...",
    "refreshToken": "eyJhbGci..."
  }
}

Update both tokens in storage. The old refresh token is single-use.


Customer Endpoints

All endpoints below require Authorization: Bearer <accessToken>.


Create Bank Account

POST /api/v1/customers/create-account

Call this once after the user completes registration. It provisions a real MFB-backed account via Qore. The account starts with PND (Post No Debit) active - the user cannot withdraw until their first deposit is confirmed.

No body required.

Success response 201

{
  "status": "success",
  "result": {
    "accountNumber": "0123456789"
  }
}

Error 400 if account already exists for this customer.


Get My Profile

GET /api/v1/customers/me

Success response 200

{
  "status": "success",
  "result": {
    "id": "3f2a1b4c-...",
    "firstName": "Adaeze",
    "lastName": "Okafor",
    "phone": "08012345678",
    "email": "adaeze@example.com",
    "qoreAccountNumber": "0123456789",
    "customerStatus": "active",
    "kycStatus": "bvn_verified",
    "pndActive": false,
    "createdAt": "2025-05-29T10:00:00.000Z",
    "balance": 500000
  }
}

balance is in kobo (NGN 5,000.00 in this example). It is a live poll from Qore and is undefined if the account has not been created yet.


Get Balance

GET /api/v1/customers/balance

Live poll from Qore. Do not call this on every screen render - cache it client-side and refresh on user action.

Success response 200

{
  "status": "success",
  "result": {
    "balanceKobo": 500000
  }
}

Get Transactions

GET /api/v1/customers/transactions

Live poll from Qore. Returns raw Qore transaction objects. Field names follow Qore's schema.

Success response 200

{
  "status": "success",
  "result": [ ... ]
}

Disbursement Endpoints

All endpoints below require Authorization: Bearer <accessToken>.

A disbursement plan defines a recurring automatic transfer from the customer's MonieRemit account to their personal bank account.


Create or Update Plan

POST /api/v1/disbursements/plan

Calling this again updates the existing plan (upsert). The plan is activated immediately.

Body

{
  "amount": 250000,
  "frequency": "monthly",
  "nextDueAt": "2025-06-01T00:00:00.000Z",
  "withdrawalAccountNumber": "0987654321",
  "withdrawalBankCode": "058"
}
Field Type Required Notes
amount integer yes In kobo
frequency string yes "weekly" or "monthly"
nextDueAt string yes ISO 8601 datetime of first/next execution
withdrawalAccountNumber string yes Destination account
withdrawalBankCode string yes CBN bank code, e.g. "058" for GTBank

Success response 200

{
  "status": "success",
  "result": {
    "id": "abc123-...",
    "customerId": "3f2a1b4c-...",
    "amount": "250000",
    "frequency": "monthly",
    "nextDueAt": "2025-06-01T00:00:00.000Z",
    "withdrawalAccountNumber": "0987654321",
    "withdrawalBankCode": "058",
    "active": true
  }
}

amount is returned as a string.


Get Plan

GET /api/v1/disbursements/plan

Success response 200 - same shape as the create response.

Error 404 if no plan has been created yet.


Get Disbursement History

GET /api/v1/disbursements/history

Success response 200

{
  "status": "success",
  "result": [
    {
      "id": "log-id-...",
      "customerId": "3f2a1b4c-...",
      "planId": "abc123-...",
      "amountKobo": "250000",
      "status": "confirmed",
      "debitReference": "ADAE1K8F3Q",
      "transferReference": "ADAE1K8F5R",
      "failureReason": null,
      "attemptCount": 1,
      "createdAt": "2025-06-01T00:00:05.000Z",
      "updatedAt": "2025-06-01T00:00:08.000Z"
    }
  ]
}
Status Meaning
pending In progress or queued
confirmed Successfully transferred to destination account
failed Failed after 3 attempts, see failureReason

Admin Endpoints

These endpoints have no auth guard in the current build - add an admin auth middleware before shipping to production.


List All Customers

GET /api/v1/admin/customers

Returns all customers. bvnHash and passwordHash are omitted.


Freeze Customer

POST /api/v1/admin/customers/:id/freeze

Freezes the Qore account and sets customerStatus to suspended. The customer cannot log in after this.

Success response 200

{ "status": "success", "result": { "frozen": true } }

Unfreeze Customer

POST /api/v1/admin/customers/:id/unfreeze

Unfreezes the Qore account and sets customerStatus back to active.


List All Disbursements

GET /api/v1/admin/disbursements

All disbursement logs across all customers, newest first.


Failed Jobs

GET /api/v1/admin/jobs/failed

All disbursement logs with status: "failed". Use this as your ops escalation queue.


Enum Reference

These are the exact string values returned and accepted by the API.

customerStatus

kycStatus

DisbursementPlan frequency

DisbursementLog / MonieTransaction status


Common Errors

Scenario Status Message
Missing or expired access token 401 "Authentication required"
Suspended account 403 "Account suspended"
BVN already registered 400 "BVN already associated with another account"
Expired OTP 400 "OTP expired or invalid"
Wrong OTP 400 "Incorrect OTP"
Phone already registered 400 "Phone number already registered"
No bank account yet 400 "No bank account found"
No disbursement plan 404 "No disbursement plan found"
Server/Qore error 500 Descriptive message

Typical User Journey (Mobile)

1. Registration screen
   POST /auth/register             -> save customerId

2. OTP screen
   POST /auth/send-otp             -> save pinId
   POST /auth/verify-otp           -> save accessToken + refreshToken

3. BVN screen  (now authenticated)
   POST /auth/verify-bvn           -> show confirmed name to user

4. Home screen
   POST /customers/create-account  (once, on first login after BVN verified)
   GET  /customers/me

5. Balance screen
   GET  /customers/balance

6. Transactions screen
   GET  /customers/transactions

7. Disbursement setup screen
   POST /disbursements/plan
   GET  /disbursements/plan      (to show current plan)

8. Disbursement history screen
   GET  /disbursements/history

Interactive Docs

The Swagger UI lists every endpoint with full request/response schemas and a live "Try it out" interface — useful for manual testing without a separate HTTP client.

If you're importing the API into Postman or generating a typed client (e.g. with openapi-typescript), grab the OpenAPI JSON spec directly.

This guide is also served as a rendered HTML page at /api/docs/guide, so you can share a link instead of the raw markdown file.