Developer API

LomaBank Developer API

Programmatic API for business integrations. All routes sit under /api/dev and are authenticated with an API key plus a whitelisted server IP.

Base URL{HOST}/api/dev
Authx-api-key header and a whitelisted client IP
Content-Typeapplication/json
1

Getting access

Do this once from the business dashboard (JWT, ADMIN/STAFF, permission business-all) before any /dev call will succeed.

1.1 Create an API key

POST/api/auth/api-keys

The full secret is returned once. The frontend should prompt the user to copy it immediately. Later GET calls only return keyPrefix metadata.

jsonRequest
{
  "name": "Production server"
}
jsonResponse
{
  "statusCode": 200,
  "message": "API key generated successfully",
  "data": {
    "id": "c1d2e3f4-5678-90ab-cdef-1234567890ab",
    "name": "Production server",
    "keyPrefix": "loma_key_3f2a1c…",
    "isActive": true,
    "apiKey": "loma_key_3f2a1c9b8e7d6c5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1",
    "replaced": false
  }
}

Store data.apiKey in your server secrets. Never ship it in a browser or mobile app.

1.2 Whitelist your server IP

Developer API requests are rejected unless the caller IP is on this list. Max 5 IPs.

POST/api/auth/api-keys/ip-whitelist
jsonRequest
{
  "ipAddress": "203.0.113.10",
  "label": "Production server"
}

The IP is taken from cf-connecting-ip (Cloudflare), then x-forwarded-for, then the socket address. Whitelist the public IP your server egresses from.

GET/api/auth/api-keys/ip-whitelist

Lists entries.

DELETE/api/auth/api-keys/ip-whitelist?id=<id>

Removes one.

If the key is valid but the IP is not listed:

json
{
  "statusCode": 403,
  "message": "Request IP is not whitelisted for this Developer API key. Add this IP from the dashboard."
}

Missing or invalid key:

json
{
  "statusCode": 401,
  "message": "API key is missing"
}

or "Invalid API key" / "API key has expired".

1.3 Set a webhook URL

POST/api/auth/webhook-url
jsonRequest
{
  "url": "https://your-app.com/webhooks/loma"
}

Loma POSTs JSON events to this URL when wallets are credited or bank-transfer status changes. See section 4.

1.4 Call the Developer API

http
POST /api/dev/transfers/loma-to-loma
x-api-key: loma_key_3f2a1c9b8e7d6c5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1
Content-Type: application/json

No JWT and no device fingerprint. Only x-api-key from a server whose IP is whitelisted.

2

Response envelope

json
{
  "statusCode": 200,
  "message": "success",
  "data": {}
}

Offset lists also include meta (page, take, itemCount, pageCount, hasPreviousPage, hasNextPage).

StatusMeaning
400Validation
401API key
403IP not whitelisted
404Not found
409Duplicate customerReference
422KYC / insufficient balance
3

Endpoints

Every route below requires x-api-key and a whitelisted IP.

3.1 Create a dynamic customer account

Creates a CUSTOMER_SUBACCOUNT with walletType: DYNAMIC. Incoming credits to this account settle to the business main wallet (minus fee) and fire dynamic_wallet.* webhooks.

POST/api/dev/customer/dynamic
FieldRequired
firstNameyes
lastNameyes
emailyes — unique per business + wallet type
bvnno
ninno
dateOfBirthno (YYYY-MM-DD)
addressno
phoneNumberno
cityno
stateno
jsonRequest
{
  "firstName": "Chidi",
  "lastName": "Eze",
  "email": "chidi@example.com",
  "bvn": "22198765432",
  "nin": "98765432109",
  "dateOfBirth": "1992-06-20",
  "address": "14 Allen Avenue, Ikeja",
  "phoneNumber": "08012345678",
  "city": "Ikeja",
  "state": "Lagos"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Customer created and dynamic wallet assigned successfully",
  "data": {
    "accountName": "CHIDI EZE",
    "accountNumber": "1108765432",
    "walletType": "DYNAMIC",
    "email": "chidi@example.com"
  }
}

Give accountNumber to the customer as their pay-in account.

3.2 Create a static customer account

Takes the same body as 3.1. The wallet stays on the customer account (STATIC), and credits fire static_wallet.credit.

POST/api/dev/customer/static
FieldRequired
firstNameyes
lastNameyes
emailyes — unique per business + wallet type
bvnno
ninno
dateOfBirthno (YYYY-MM-DD)
addressno
phoneNumberno
cityno
stateno
jsonRequest
{
  "firstName": "Chidi",
  "lastName": "Eze",
  "email": "chidi@example.com",
  "bvn": "22198765432",
  "nin": "98765432109",
  "dateOfBirth": "1992-06-20",
  "address": "14 Allen Avenue, Ikeja",
  "phoneNumber": "08012345678",
  "city": "Ikeja",
  "state": "Lagos"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Customer created and static wallet assigned successfully",
  "data": {
    "accountName": "CHIDI EZE",
    "accountNumber": "1108765433",
    "walletType": "STATIC",
    "email": "chidi@example.com"
  }
}

3.3 List customer accounts

GET/api/dev/customer/lists

Query: searchTerm, walletType (STATIC | DYNAMIC), page (default 1), take (default 10), order (ASC | DESC)

GET/api/dev/customer/lists?page=1&take=10&walletType=DYNAMIC&searchTerm=chidi
jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": [
    {
      "id": "sa-1111-2222-3333-444455556666",
      "accountName": "CHIDI EZE",
      "accountNumber": "1108765432",
      "accountType": "CUSTOMER_SUBACCOUNT",
      "walletType": "DYNAMIC",
      "status": "ACTIVE",
      "firstName": "Chidi",
      "lastName": "Eze",
      "email": "chidi@example.com",
      "createdAt": "2026-08-20T09:00:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "take": 10,
    "itemCount": 1,
    "pageCount": 1,
    "hasPreviousPage": false,
    "hasNextPage": false
  }
}

3.4 Get one customer account

GET/api/dev/customer/:id

:id is the customer record UUID, not the account number.

jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "id": "sa-1111-2222-3333-444455556666",
    "accountName": "CHIDI EZE",
    "accountNumber": "1108765432",
    "accountType": "CUSTOMER_SUBACCOUNT",
    "walletType": "DYNAMIC",
    "status": "ACTIVE",
    "email": "chidi@example.com"
  }
}

404 if the id is not a customer account owned by this business.

3.5 Check wallet balance

Works for main, sub, and customer accounts you own.

POST/api/dev/accounts/balance
jsonRequest
{
  "accountNumber": "1108765432"
}
jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "bankName": "LomaBank",
    "accountName": "CHIDI EZE",
    "accountNumber": "1108765432",
    "availableBalance": "250000.00",
    "bookedBalance": "250000.00"
  }
}

3.6 Freeze a wallet

PATCH/api/dev/wallets/deactivate
jsonRequest
{
  "accountNumber": "1108765432",
  "reason": "Customer requested freeze"
}

reason is optional.

jsonResponse
{
  "statusCode": 200,
  "message": "Wallet deactivated successfully",
  "data": {
    "accountNumber": "1108765432",
    "accountName": "CHIDI EZE",
    "status": "INACTIVE"
  }
}

3.7 Unfreeze a wallet

PATCH/api/dev/wallets/activate
jsonRequest
{
  "accountNumber": "1108765432"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Wallet activated successfully",
  "data": {
    "accountNumber": "1108765432",
    "accountName": "CHIDI EZE",
    "status": "ACTIVE"
  }
}

3.8 Upgrade wallet tier

PATCH/api/dev/wallets/tier

tier: TIER_1 · TIER_2 · TIER_3

jsonRequest
{
  "accountNumber": "1108765432",
  "tier": "TIER_3"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Wallet tier upgraded successfully",
  "data": {
    "accountNumber": "1108765432",
    "accountName": "CHIDI EZE",
    "previousTier": "TIER_1",
    "tier": "TIER_3"
  }
}

3.9 List banks

Use this before a bank transfer to get sortCode.

GET/api/dev/banks?search=access&page=1&take=50

take max 400.

jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": [
    {
      "id": "bank-uuid",
      "name": "Access Bank",
      "code": "000014"
    }
  ],
  "meta": {
    "page": 1,
    "take": 50,
    "itemCount": 1,
    "pageCount": 1,
    "hasPreviousPage": false,
    "hasNextPage": false
  }
}

code is the NIP sort code you send as sortCode on bank transfers.

3.10 Other-bank name enquiry

Resolve the beneficiary name before POST /transfers/loma-to-bank.

GET/api/dev/banks/name-enquiry?sortCode=000014&accountNumber=0221234567
jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "accountName": "CHIDI EZE",
    "accountNumber": "0221234567"
  }
}

3.11 Loma wallet name enquiry

Resolve a Loma account name before POST /transfers/loma-to-loma.

GET/api/dev/wallets/name-enquiry?walletNo=1108765432
jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": {
    "accountName": "CHIDI EZE",
    "accountNumber": "1108765432",
    "status": "ACTIVE"
  }
}

3.12 Loma to Loma transfer

Immediate debit (no dashboard approval OTP). customerReference is your idempotency key — reuse is 409.

POST/api/dev/transfers/loma-to-loma
FieldRequiredNotes
senderAccountyesYour main / sub / customer account number
receiverAccountyesDestination Loma account
amountyesNumber, greater than 0
customerReferenceyesUnique per business
narrationno
jsonRequest
{
  "senderAccount": "1102345678",
  "receiverAccount": "1108765432",
  "amount": 25000,
  "customerReference": "PAYOUT-20260827-0001",
  "narration": "Customer settlement"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Transaction successfully completed",
  "data": {
    "transactionReference": "LMA202608271234",
    "lomaTransRef": "TP-REF-8899",
    "customerReference": "PAYOUT-20260827-0001",
    "amount": 25000,
    "senderAccount": "1102345678",
    "receiverAccount": "1108765432",
    "receiverAccountName": "CHIDI EZE",
    "status": "SUCCESS"
  }
}

Business KYC must be approved. Sender wallet must be ACTIVE.

3.13 Loma to other bank transfer

Immediate (or pending) NIP debit. Status updates also arrive as wallet.bank_transfer webhooks — always validate those (section 5).

POST/api/dev/transfers/loma-to-bank
FieldRequiredNotes
senderAccountNumberyesYour source account
beneficiaryAccountNumberyes
beneficiaryAccountNameyesFrom name enquiry
sortCodeyesFrom /dev/banks
amountyesNumber, minimum 10
customerReferenceyesUnique per business
narrationno
jsonRequest
{
  "senderAccountNumber": "1102345678",
  "beneficiaryAccountNumber": "0221234567",
  "beneficiaryAccountName": "Chidi Eze",
  "sortCode": "000014",
  "amount": 50000,
  "customerReference": "VENDOR-20260827-0002",
  "narration": "Vendor payout"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Transaction successfully completed",
  "data": {
    "transactionReference": "LMA202608271299",
    "lomaTransRef": "NIP-REF-4400",
    "customerReference": "VENDOR-20260827-0002",
    "amount": 50000,
    "senderAccount": "1102345678",
    "beneficiaryAccountNumber": "0221234567",
    "beneficiaryAccountName": "Chidi Eze",
    "beneficiaryBank": "Access Bank",
    "sortCode": "000014",
    "status": "SUCCESS"
  }
}

status can be SUCCESS or PENDING. If PENDING, wait for wallet.bank_transfer webhooks and validate each one.

3.14 Transaction history

GET/api/dev/transactions/history

Query: accountNumber, transStatus (PENDING · AWAITING_APPROVAL · CANCELLED · SUCCESS · FAILED), transType (CREDIT · DEBIT), searchTerm, page, take, order

GET/api/dev/transactions/history?page=1&take=20&accountNumber=1102345678&transStatus=SUCCESS
jsonResponse
{
  "statusCode": 200,
  "message": "success",
  "data": [
    {
      "id": "txn-uuid",
      "sourceAccountNo": "0221234567",
      "targetAccountNo": "1108765432",
      "sourceAccountName": "CHIDI EZE",
      "targetAccountName": "CHIDI EZE",
      "transferType": "WALLET_TO_BANK",
      "amount": "50000",
      "total": "50000",
      "transStatus": "SUCCESS",
      "transType": "CREDIT",
      "narration": "account funded",
      "TransRef": "LMA202608261234",
      "SessionId": "999988887777",
      "createdAt": "2026-08-26T11:00:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "take": 20,
    "itemCount": 1,
    "pageCount": 1,
    "hasPreviousPage": false,
    "hasNextPage": false
  }
}

3.15 Validate a webhook transaction (required)

Look up a transaction you own by TransRef or SessionId. Use the data.reference (or data.sessionId) from every webhook payload.

POST/api/dev/transactions/validate
jsonRequest
{
  "reference": "999988887777"
}
jsonResponse
{
  "statusCode": 200,
  "message": "Transaction validated successfully",
  "data": {
    "id": "txn-uuid",
    "amount": "50000",
    "total": "50000",
    "Transfee": "100",
    "transStatus": "SUCCESS",
    "transType": "CREDIT",
    "transferType": "WALLET_TO_BANK",
    "TransRef": "LMA202608261234",
    "SessionId": "999988887777",
    "sourceAccountNo": "0221234567",
    "sourceAccountName": "CHIDI EZE",
    "targetAccountNo": "1108765432",
    "targetAccountName": "CHIDI EZE",
    "narration": "account funded",
    "createdAt": "2026-08-26T11:00:00.000Z"
  }
}

404Transaction not found for the provided reference. Treat the webhook as untrusted until this succeeds. See section 5 for the required flow.

4

Webhooks

When a webhook URL is saved, Loma POSTs JSON to it. Your endpoint must respond with HTTP 2xx within 15 seconds. Failed deliveries retry up to 5 times with exponential backoff.

4.1 Request Loma sends you

http
POST https://your-app.com/webhooks/loma
Content-Type: application/json
User-Agent: LomaBank-Business-Webhook/1.0
X-Loma-Event: dynamic_wallet.credited
X-Loma-Reference: 999988887777

X-Loma-Event matches payload.event. X-Loma-Reference is the value you pass to /dev/transactions/validate.

4.2 Events to listen for

eventWhen it firesTypical data.reference
dynamic_wallet.creditedLoma-to-Loma credit into a dynamic customer wallet (then settled to your main wallet minus fee)Incoming transfer reference
dynamic_wallet.account_fundedExternal bank (NIP) credit into a dynamic customer walletNIP sessionID
static_wallet.creditCredit into a static customer / main / sub account (NIP or Loma-to-Loma)sessionID (NIP) or transfer reference
wallet.bank_transferStatus update on an outbound wallet → other-bank debit you initiatedYour transfer TransRef / customer reference

Treat any other event as unknown. Do not credit a customer until validate succeeds.

4.3 dynamic_wallet.credited

Loma wallet paid a dynamic customer account. Gross lands on the dynamic wallet, fee is taken, net settles to your main account.

jsonPayload
{
  "event": "dynamic_wallet.credited",
  "data": {
    "amount": 10000,
    "fee": 50,
    "netAmount": 9950,
    "reference": "W2W-REF-889900",
    "settlementReference": "SETTLE-W2W-889900",
    "narration": "Dynamic wallet credit — CHIDI EZE",
    "walletType": "DYNAMIC",
    "accountNumber": "1108765432",
    "accountName": "CHIDI EZE",
    "customerFirstName": "Chidi",
    "customerLastName": "Eze",
    "sourceAccountName": "ADA OKEKE",
    "sourceAccountNumber": "1101111222",
    "sourceBankSortCode": "090620",
    "sourceBankName": "LomaBank",
    "status": "SUCCESS",
    "timestamp": "2026-08-27T10:15:00.000Z"
  }
}

Validate with { "reference": "W2W-REF-889900" }.

4.4 dynamic_wallet.account_funded

Someone paid the dynamic account from another Nigerian bank.

jsonPayload
{
  "event": "dynamic_wallet.account_funded",
  "data": {
    "amount": 50000,
    "fee": 100,
    "netAmount": 49900,
    "reference": "999988887777",
    "settlementReference": "SETTLE-NIP-999988887777",
    "narration": "TRF FROM CHIDI EZE",
    "walletType": "DYNAMIC",
    "accountNumber": "1108765432",
    "accountName": "CHIDI EZE",
    "customerFirstName": "Chidi",
    "customerLastName": "Eze",
    "sourceAccountName": "CHIDI EZE",
    "sourceAccountNumber": "0221234567",
    "sourceBankSortCode": "000014",
    "sourceBankName": "Access Bank",
    "status": "SUCCESS",
    "timestamp": "2026-08-27T10:20:00.000Z"
  }
}

Validate with { "reference": "999988887777" } (the NIP session id).

4.5 static_wallet.credit

Credit stayed on a static / main / sub account (no dynamic settlement sweep).

jsonFrom another bank
{
  "event": "static_wallet.credit",
  "data": {
    "amount": "25000",
    "reference": "888877776666",
    "narration": "account funded",
    "walletType": "STATIC",
    "accountNumber": "1102345678",
    "accountName": "ACME PAYMENTS LIMITED",
    "sourceAccountName": "CHIDI EZE",
    "sourceAccountNumber": "0221234567",
    "sourceBankSortCode": "000014",
    "sourceBankName": "Access Bank",
    "status": "SUCCESS",
    "timestamp": "2026-08-27T10:25:00.000Z"
  }
}
jsonFrom another Loma wallet
{
  "event": "static_wallet.credit",
  "data": {
    "amount": "15000",
    "reference": "W2W-IN-554433",
    "narration": "wallet credit",
    "walletType": "STATIC",
    "accountNumber": "1102345678",
    "accountName": "ACME PAYMENTS LIMITED",
    "sourceAccountName": "Ngozi Ade",
    "sourceAccountNumber": "1109988776",
    "sourceBankName": "LomaBank",
    "status": "SUCCESS",
    "timestamp": "2026-08-27T10:30:00.000Z"
  }
}

Validate with { "reference": "<data.reference>" }.

4.6 wallet.bank_transfer

Status of a bank payout you started via /dev/transfers/loma-to-bank (or the dashboard). You can receive more than one event as the NIP status moves (PENDINGSUCCESS / FAILED).

jsonPayload
{
  "event": "wallet.bank_transfer",
  "data": {
    "amount": "50000",
    "fee": "25",
    "total": "50025",
    "reference": "VENDOR-20260827-0002",
    "sessionId": "NIP-SESSION-4400",
    "narration": "Vendor payout",
    "walletType": "STATIC",
    "sourceAccountNumber": "1102345678",
    "sourceAccountName": "ACME PAYMENTS LIMITED",
    "targetAccountNumber": "0221234567",
    "targetAccountName": "Chidi Eze",
    "targetBankName": "Access Bank",
    "status": "SUCCESS",
    "transferType": "WALLET_TO_BANK",
    "timestamp": "2026-08-27T10:40:00.000Z"
  }
}

Validate with { "reference": "VENDOR-20260827-0002" } or { "reference": "NIP-SESSION-4400" }. Only mark the payout complete when validate returns transStatus: "SUCCESS".

5

Always validate webhook transactions

A webhook POST is a notification, not proof. Replay, delay, or a forged body must not credit a customer or close a payout.

Required flow for every event in section 4

  1. 1Return 200 quickly so Loma does not retry unnecessarily. Process asynchronously if needed.
  2. 2Read event and data.reference (for bank payouts also try data.sessionId).
  3. 3Call POST /api/dev/transactions/validate with that reference and your x-api-key.
  4. 4If validate is 404 or the amounts / accounts do not match the webhook body, ignore the event.
  5. 5If validate is 200, use the validate data (amount, transStatus, accounts) as source of truth — not the raw webhook fields.
  6. 6Store TransRef / SessionId so the same event is not applied twice.
textFlow
Webhook received
    → POST /api/dev/transactions/validate  { "reference": payload.data.reference }
    → 200 + transStatus SUCCESS  → apply credit / close payout
    → 404 or mismatch            → do nothing, log for review

Example after a dynamic_wallet.account_funded webhook:

http
POST /api/dev/transactions/validate
x-api-key: loma_key_...
Content-Type: application/json

{ "reference": "999988887777" }

Only then mark invoice INV-1001 paid for data.amount on data.targetAccountNo.

Do not trust data.status from the webhook alone for bank payouts. Wait for validate to show SUCCESS (or FAILED).

6

Typical integration sequences

Collect from customers (dynamic accounts)

  1. 1POST /dev/customer/dynamic → show accountNumber to the payer.
  2. 2Listen for dynamic_wallet.credited or dynamic_wallet.account_funded.
  3. 3Validate data.reference.
  4. 4Fulfil the order using validate amount / transStatus.

Collect on a static / main account

  1. 1Share your static or main accountNumber.
  2. 2Listen for static_wallet.credit.
  3. 3Validate, then fulfil.

Pay out to a Loma wallet

  1. 1GET /dev/wallets/name-enquiry?walletNo=...
  2. 2POST /dev/transfers/loma-to-loma with a unique customerReference.
  3. 3Treat data.status === "SUCCESS" as complete. Optional: validate data.transactionReference.

Pay out to another bank

  1. 1GET /dev/bankssortCode.
  2. 2GET /dev/banks/name-enquiry.
  3. 3POST /dev/transfers/loma-to-bank.
  4. 4If PENDING, wait for wallet.bank_transfer and validate each update.
7

Endpoint index

MethodPathUse
POST/api/dev/customer/dynamicCreate dynamic pay-in account
POST/api/dev/customer/staticCreate static customer account
GET/api/dev/customer/listsList customer accounts
GET/api/dev/customer/:idGet one customer account
POST/api/dev/accounts/balanceBalance
PATCH/api/dev/wallets/deactivateFreeze
PATCH/api/dev/wallets/activateUnfreeze
PATCH/api/dev/wallets/tierUpgrade tier
GET/api/dev/banksBank list
GET/api/dev/banks/name-enquiryNIP name enquiry
GET/api/dev/wallets/name-enquiryLoma name enquiry
POST/api/dev/transfers/loma-to-lomaWallet transfer
POST/api/dev/transfers/loma-to-bankBank transfer
GET/api/dev/transactions/historyHistory
POST/api/dev/transactions/validateConfirm a webhook transaction

Setup (dashboard, JWT): POST/GET/DELETE /api/auth/api-keys, POST/GET/DELETE /api/auth/api-keys/ip-whitelist, POST/GET /api/auth/webhook-url.