AtomOwn API

AtomAfrica · Economic Ownership Infrastructure

Build your own ownership app on AtomOwn

The AtomOwn API lets developers and fintechs run a fully branded, white-label economic-ownership network: properties, wallets, unit ownership, a secondary market and payouts: on shared, ledger-backed infrastructure. One REST API, JSON everywhere, opaque bearer tokens.

Base /api/v1 Auth Bearer token Format JSON Money NGN (₦)

Every request is scoped to your network (a “developer” tenant). Your customers register against your network, hold units of your listed properties, and move value inside a double-entry ledger that AtomAfrica reconciles. You keep your brand, customers and data; AtomAfrica keeps the money correct.

  • Multi-tenant & isolated: data is row-scoped to your developer network; customers never cross networks.
  • Ledger-backed: balances are derived from an append-only double-entry ledger, not a mutable number.
  • White-label by config: theme, branding and enabled features come from /app/config at runtime, so your app re-skins with no rebuild.

Quickstart

A minimal integration is four calls. Replace API with your AtomOwn base URL and acme-homes with your network's slug.

  1. Pull your branding & enabled features from GET /app/config.
  2. Register or log a customer in: you receive a bearer token.
  3. Show listings from GET /public/properties and let the customer fund their wallet.
  4. Buy packets with POST /purchases (PIN-gated).
end-to-end
# 1: branding + feature flags for your network
curl "$API/api/v1/app/config?developer=acme-homes"

# 2: log a customer in (tenant-bound by `developer`)
curl -X POST "$API/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"developer":"acme-homes","email":"ada@example.com","password":"secret123"}'
# → { "ok": true, "data": { "token": "ao_live_9f2c…", "user": { … } } }

# 3: browse listings
curl "$API/api/v1/public/properties?developer=acme-homes"

# 4: buy ₦50,000 of packets (needs the customer's 6-digit PIN)
curl -X POST "$API/api/v1/purchases" \
  -H "Authorization: Bearer ao_live_9f2c…" \
  -H "Content-Type: application/json" \
  -d '{"property_id":12,"amount":50000,"pin":"246813"}'

Base URL & versioning

All endpoints live under a single versioned prefix. The version only changes for breaking changes; additive fields are not breaking.

Base path/api/v1
Content typeapplication/json for request bodies and all responses (CSV export is the only exception).
TransportHTTPS only in production. Reject non-TLS traffic.

Verify connectivity any time with GET /api/v1/ping.

Authentication

Two things identify a request: which network it belongs to (the tenant) and which user is making it.

1 · Tenant binding: the developer key

Your network is identified by its slug (or subdomain). Public endpoints take it as a query parameter; auth endpoints take it in the body. After login it is baked into the token, so authenticated calls don't repeat it.

WhereHow you pass the network
Public discovery (config, listings)?developer=acme-homes
Register / login"developer": "acme-homes" in the JSON body
Authenticated callsNothing: the token already carries the tenant & user

2 · Bearer tokens

A successful register or login returns an opaque token (not a JWT: treat it as a secret string). Send it on every authenticated call:

header
Authorization: Bearer ao_live_9f2c8b1e…

Tokens are session-scoped and expire (default 24h). A revoked, expired or malformed token returns 401 invalid_token. Call POST /auth/logout to revoke immediately.

3 · The transaction PIN

Every value-moving action (buy, withdraw, transfer, liquidate) requires the customer's 6-digit PIN in the request body, independent of the login password. Set it with POST /pin/set. A wrong PIN returns 401 with a pin_* code and is rate-limited.

Never store the PIN. Collect it per-action in your UI and forward it once. AtomOwn stores only a hash and enforces attempt throttling.

Requests & responses

Envelope

Every response is wrapped in a stable envelope. Check ok first.

Success 2xx
json
{
  "ok": true,
  "data": { "...": "endpoint payload" },
  "meta": { "...": "present only on some list endpoints" }
}
Error 4xx / 5xx
json
{
  "ok": false,
  "error": {
    "code": "insufficient_funds",
    "message": "Not enough wallet balance.",
    "details": { "...": "optional" }
  }
}

Money

All amounts in JSON are Naira (₦) decimals: e.g. 25000.50. Internally AtomOwn stores integer kobo and derives balances from the ledger, so amounts are always exact. Send amounts as plain numbers (naira); never send kobo.

Pagination

List endpoints that can grow accept ?page (1-based) and ?per (page size, default 25, max 100) and return a pagination object:

json
"pagination": { "page": 2, "per": 25, "total": 340, "pages": 14 }

Headers you'll send

HeaderWhen
Authorization: Bearer …Every authenticated call
Content-Type: application/jsonEvery request with a body
Accept: application/jsonRecommended on all calls
Retries are not automatically de-duplicated. For value-moving POSTs, wait for the response before retrying, and reconcile with GET /transactions if a call times out.

Errors

HTTP status conveys the class; the error.code string is the stable value to branch on. Common codes:

StatuscodeMeaning
400developer_requiredNetwork slug missing on a public/auth call
401invalid_credentialsWrong email/password
401invalid_tokenToken missing, expired or revoked
401pin_invalidWrong transaction PIN
402insufficient_fundsWallet balance too low for the action
403customer_only / forbiddenWrong account type or missing permission
403email_not_verifiedAccount registered but email not verified: call /auth/verify-email
404developer_not_foundNo active network for that slug
409email_takenEmail already registered on this network
422validationMissing/invalid fields (see message)
429rate_limitedToo many requests: see Rate limits

Rate limits

Sensitive endpoints are throttled per IP. When exceeded you get 429 with a Retry-After header (seconds). Back off and retry after that window.

EndpointLimit
POST /auth/login10 / 60s
POST /auth/register5 / 60s
POST /pin/verify8 / 60s
POST /auth/verify-email8 / 60s
POST /auth/resend-verification3 / 60s

Discovery

GET/api/v1/pingPublic

Health check. Confirms the API surface and returns the running version.

Response 200
json
{ "ok": true, "data": { "service": "atomown-api", "version": "v1",
  "ci": "4.7.4", "env": "production", "time": "2026-09-14T09:12:03+01:00" } }
GET/api/v1/app/configPublic

The app bootstrap. Returns your network's theme, branding, editable content pages and the set of enabled features: so a white-label app renders the right skin and only the modules you turned on, with no rebuild.

QueryTypeDescription
developerstringrequiredYour network slug or subdomain
Response 200
json
{ "ok": true, "data": {
  "developer": { "id": 11, "name": "Acme Homes", "slug": "acme-homes" },
  "theme":     { "key": "theme-2", "name": "Editorial" },
  "branding":  { "primary": "#0F7A68", "logo": "https://…/logo.png", "app_name": "Acme Own" },
  "content":   { "about": "…", "contact": "…", "privacy": "…", "terms": "…" },
  "features":  ["wallet", "secondary_market", "installments", "referral", "atomcredit", "manual_funding", "email_verification_required"],
  "loans":     { "enabled": true, "bank_name": "First Trust Bank" },
  "fees": {
    "withdrawal":       { "model": "flat", "pct": 0, "flat": 10000, "min": null, "max": null },
    "transfer":         { "developer": { "model": "percentage", "pct": 1.0, "flat": 0 },
                          "network":   { "model": "percentage", "pct": 1.5, "flat": 0 } },
    "secondary_market": { "developer": { "model": "percentage", "pct": 2.0, "flat": 0 },
                          "network":   { "model": "percentage", "pct": 1.5, "flat": 0 } },
    "liquidation":      { "developer": { "model": "percentage", "pct": 5.0, "flat": 0 },
                          "network":   { "model": "percentage", "pct": 1.5, "flat": 0 } }
  }
} }
// fees.*.developer is null when the developer has not configured that fee.
// fees.*.network  is null when no atomafrica_transaction rule exists in charge_rules.
// Flat amounts are in kobo; percentages are a plain number (1.5 = 1.5%).
GET/api/v1/public/propertiesPublic

The public catalogue: active listings for your network, no login required. Use /public/properties/{id} for one listing.

QueryTypeDescription
developerstringrequiredYour network slug or subdomain
Response 200
json
{ "ok": true, "data": { "properties": [{
  "id": 12, "name": "Palm Estate",
  "category": "residential", "sub_category": "residential",
  "property_category_id": 3,
  "location": "12 Admiralty Way, Lekki Phase 1", "state": "Lagos",
  "latitude": 6.4281, "longitude": 3.4219,
  "total_value": 10000000, "unit_type": "flat",
  "packets_per_unit": 100, "packet_price": 10000, "base_packet_price": 10000,
  "amount_per_unit": 1000000, "base_amount_per_unit": 1000000,
  "total_units": 10, "available_units": 6.2,
  "total_packets": 1000, "available_packets": 620,
  "has_returns": true, "listing_status": "active", "cover": null, "images": []
}] } }

A property is made of units of its unit_type (a Flat/Plot/…). Each unit is packets_per_unit packets; a packet (packet_price) is the smallest, minimum-purchasable share. So packets_per_unit = amount_per_unit ÷ packet_price and total_packets = packets_per_unit × total_units. packet_price is the live price; base_packet_price is the original. category/sub_category is the property type (land · residential · commercial · industrial); property_category_id points to the developer's own custom category (e.g. "Phase 1"). state is the Nigerian state. description may contain rich HTML: sanitize before rendering. latitude/longitude are optional map coordinates.

GET/api/v1/propertiesBearer · property_discovery

Active listings for the signed-in customer's developer network. Supports optional filters to power the discover page's search and tabs. All filters are combined with AND logic. Returns the same property shape as the public catalogue (see above) including state, sub_category, and property_category_id.

QueryTypeDescription
property_category_idintegeroptionalFilter to one developer-defined category tab (id from GET /property-categories)
statestringoptionalExact match on the 36 Nigerian states / FCT, e.g. Lagos
sub_categorystringoptionalland · residential · commercial · industrial
namestringoptionalPartial name search (case-insensitive contains)
descriptionstringoptionalPartial description search
locationstringoptionalPartial location/address search
request
curl "$API/api/v1/properties?state=Lagos&sub_category=residential" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": { "properties": [{ /* property shape — see Public listings */ }] } }
GET/api/v1/property-categoriesBearer · property_discovery

Returns the developer's custom property categories in display order. Use this to build tabs on the discover page: show tabs only when categories.length > 1; when there is only the default category, suppress the tab bar. Each tab passes property_category_id to GET /properties.

request
curl "$API/api/v1/property-categories" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": { "categories": [
  { "id": 3, "name": "Properties", "is_default": true,  "position": 1 },
  { "id": 1, "name": "Phase 1",    "is_default": false, "position": 2 },
  { "id": 2, "name": "Phase 2",    "is_default": false, "position": 3 }
] } }

The is_default category is the one automatically created when a developer posts their first property. Its name is editable but it cannot be deleted. A developer can have at most 3 categories.

Identity & security

POST/api/v1/auth/registerPublic · tenant-bound

Create a customer on your network. Issues a bearer token, a referral code and a welcome notification. Rate-limited 5/60s. When the network has email_verification_required enabled, no token is issued at registration: instead a 6-digit OTP is emailed and the customer must complete POST /auth/verify-email before they can log in.

BodyTypeDescription
developerstringrequiredYour network slug
namestringrequiredFull name
emailstringrequiredUnique per network
passwordstringrequiredMin 6 characters
pinstringrequired6 digits: the transaction PIN
phonestringoptionalContact number
referral_codestringoptionalAttributes the signup to a referrer
request
curl -X POST "$API/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{"developer":"acme-homes","name":"Ada Okoro","email":"ada@example.com",
       "password":"secret123","pin":"246813"}'
Response 200 — email verification disabled (default)
json
{ "ok": true, "data": {
  "token": "ao_live_9f2c8b1e…",
  "user": { "id": 402, "type": "customer", "name": "Ada Okoro",
            "email": "ada@example.com", "developer_id": 11,
            "roles": ["customer"], "permissions": [] }
} }
Response 200 — email verification required
json
{ "ok": true, "data": {
  "email_verification_required": true,
  "email": "ada@example.com"
} }
// No token is issued. A 6-digit OTP (60-min TTL) is emailed to the address.
// Redirect the user to your verification screen and call POST /auth/verify-email.
POST/api/v1/auth/loginPublic · tenant-bound

Exchange credentials for a bearer token. Include developer to scope a customer login to your network. Rate-limited 10/60s. When email_verification_required is enabled, customers with an unverified email receive 403 email_not_verified: redirect them to the verification screen to complete POST /auth/verify-email.

BodyTypeDescription
emailstringrequiredAccount email
passwordstringrequiredAccount password
developerstringoptionalNetwork slug (required for customer logins)
Response 200
json
{ "ok": true, "data": { "token": "ao_live_9f2c8b1e…", "user": { "id": 402, "type": "customer", … } } }
Response 403 — email not verified
json
{ "ok": false, "error": { "code": "email_not_verified",
  "message": "Please verify your email address before logging in.",
  "details": { "email": "ada@example.com" } } }
POST/api/v1/auth/verify-emailPublic · tenant-bound

Verify a customer's email address using the 6-digit OTP that was emailed after registration. On success, the email is marked verified and a bearer token is issued so the customer is immediately logged in. Rate-limited 8/60s. Returns already_verified if the address is already confirmed.

BodyTypeDescription
developerstringrequiredYour network slug
emailstringrequiredThe address being verified
codestringrequiredThe 6-digit OTP from the verification email
Response 200
json
{ "ok": true, "data": {
  "token": "ao_live_9f2c8b1e…",
  "user": { "id": 402, "type": "customer", "name": "Ada Okoro",
            "email": "ada@example.com", "email_verified": true }
} }
Error codeStatusMeaning
otp_invalid401Code is wrong or expired
otp_max_attempts4015 wrong attempts: request a new code with /auth/resend-verification
already_verified200Email is already verified (returns a token so the user can proceed)
POST/api/v1/auth/resend-verificationPublic · tenant-bound

Re-send the email verification OTP. A 60-second cooldown applies between sends (returns cooldown if called too soon). The previous unused code is invalidated and a new 6-digit code with a 60-minute TTL is issued. Rate-limited 3/60s.

BodyTypeDescription
developerstringrequiredYour network slug
emailstringrequiredThe address to re-send to
Response 200
json
{ "ok": true, "data": { "sent": true } }
// Always returns { sent: true } regardless of whether the email exists,
// to prevent address enumeration. On cooldown (60s) sent is still true.
GET/api/v1/meBearer

The authenticated user, their roles and permission keys. Use it to rehydrate a session and to gate admin UI.

Response 200
json
{ "ok": true, "data": { "user": { "id": 402, "type": "customer", "name": "Ada Okoro",
  "email": "ada@example.com", "developer_id": 11, "roles": ["customer"], "permissions": [] } } }
POST/api/v1/auth/logoutBearer

Revoke the current token immediately.

Response 200
json
{ "ok": true, "data": { "loggedOut": true } }
POST/api/v1/pin/setBearer

Set or change the 6-digit transaction PIN. POST /pin/verify checks a PIN without moving money (rate-limited 8/60s): useful to pre-validate before a purchase screen.

BodyTypeDescription
pinstringrequiredExactly 6 digits
POST/api/v1/kycBearer

Submit identity (BVN + ID) for review; read status with GET /api/v1/kyc/status. Your developer admin approves/declines via the Developer Admin API.

BodyTypeDescription
bvnstringrequiredBank Verification Number
id_typestringrequirede.g. nin, passport, drivers_license
id_numberstringrequiredDocument number
POST/api/v1/bank-accountBearer

Register the customer's payout bank account. Add-once and immutable (a safeguard for withdrawals); read it with GET /api/v1/bank-account.

BodyTypeDescription
bank_codestringrequiredBank / institution code
account_numberstringrequiredNUBAN account number
account_namestringrequiredAccount holder name

Wallet & purchases

GET/api/v1/walletBearer · customer

The customer's spendable wallet balance, derived from the ledger.

Response 200
json
{ "ok": true, "data": { "wallet": { "balance": 152000.00, "currency": "NGN" } } }
POST/api/v1/wallet/topupBearer · customer

Fund the wallet (settle your PSP first, then record the funding here). The customer is credited in full, then the AtomAfrica deposit charge is deducted (customer nets amount − charge); the charge goes to AtomAfrica. Nothing touches the developer wallet. Returns the credited amount, charge and new balance.

BodyTypeDescription
amountnumberrequiredNaira to fund (e.g. 25000)
Response 200
json
{ "ok": true, "data": { "reference": "DEP-3461A1B3", "amount": 25000.00,
  "deposit_fee": 50.00, "credited": 24950.00, "balance": 177000.00 } }
POST/api/v1/purchasesBearer · customer · PIN

Buy packets of a listing. Pass a naira amount (recommended: packets are computed at the live packet_price and may be fractional) or an explicit packets quantity. The minimum purchase is one packet; less returns minimum_one_packet (422). Debits the wallet, allocates packets and writes the ledger atomically. Also fails with insufficient_funds (402) or a pin_* code (401).

BodyTypeDescription
property_idnumberrequiredListing to buy into
amountnumberamount or packetsNaira to spend (e.g. 15000 → 1.5 packets at ₦10,000/packet)
packetsnumberamount or packetsExplicit packet quantity (may be fractional)
pinstringrequired6-digit transaction PIN
request
curl -X POST "$API/api/v1/purchases" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"property_id":12,"amount":15000,"pin":"246813"}'
Response 200
json
{ "ok": true, "data": { "reference": "PUR-0637A42D", "packets": 1.5,
  "packet_price": 10000.00, "ownership": "1.5 packets", "cost": 15000.00,
  "atomafrica_fee": 225.00, "property": "Palm Estate" } }
GET/api/v1/portfolioBearer · customer

The customer's holdings across properties. Each holding reports packets owned plus the ownership broken into the unit language: whole_units + remainder_packets and a ready-made ownership label (e.g. "1 Flat and 20 packets"): with current value re-priced at the live packet price.

Response 200
json
{ "ok": true, "data": { "holdings": [{
  "property": "Palm Estate", "unit_type": "flat", "packets_per_unit": 100,
  "packets": 120, "units": 1.2, "whole_units": 1, "remainder_packets": 20,
  "ownership": "1 Flat and 20 packets", "packet_price": 10000.00,
  "acquisition_value": 1200000.00, "current_value": 1200000.00
}] } }
GET/api/v1/transactionsBearer

The customer's own wallet history (deposits, purchases, transfers, payouts). Fetch one receipt with GET /api/v1/transactions/{reference}. Use this to reconcile after a timed-out POST.

POST/api/v1/installment-plansBearer · customer

Buy progressively: open a plan for a property, list plans with GET /installment-plans, and pay an instalment with POST /installment-plans/{id}/pay (PIN-gated). Units vest as the plan is paid down. The unit price is locked for the life of the plan.

BodyTypeDescription
property_idnumberrequiredProperty to buy into
total_amountnumberrequiredTotal naira to invest across all payments
tenorintegerrequiredNumber of payments (minimum 2)
frequencystringrequireddaily · weekly · monthly
pinstringrequired6-digit transaction PIN
Response 200
json
{ "ok": true, "data": { "plan": {
  "id": 17, "property": "Palm Estate", "status": "active",
  "total_amount": 120000.00, "tenor": 6, "frequency": "monthly",
  "installment_amount": 20000.00, "locked_unit_price": 10000.00,
  "paid_count": 0, "next_due_at": "2026-11-08" } } }
GET/api/v1/developer-bank-accounts/customer-viewBearer · customer

Returns the developer's active bank accounts for manual wallet funding. Only available when the manual_funding feature is enabled on the network. Show these to the customer so they can make a direct bank transfer and then record it with POST /wallet/topup.

Response 200
json
{ "ok": true, "data": { "manual_funding_enabled": true,
  "accounts": [
    { "bank_name": "First Bank", "account_number": "3012345678",
      "account_name": "Acme Homes Ltd", "is_active": true }
  ] } }
GET/api/v1/public/n/{slug}/verifyPublic

Public record-verification lookup. Given a property reference, returns the property's details, encumbrance position (whether it carries an active loan), and packet-level status — without exposing any owner personal information. Use this to let a customer, lawyer or bank confirm a record is real at own.atomafrica.tech/n/{slug}/verify.

QueryTypeDescription
refstringrequiredProperty reference or slug to look up
Response 200
json
{ "ok": true, "data": { "property": {
  "name": "Palm Estate", "location": "Lekki, Lagos", "status": "active",
  "encumbered": false, "encumbrance_note": null,
  "packets": [{ "ref": "PKT-00042", "status": "owned", "encumbered": false }]
} } }

Moving ownership

POST/api/v1/withdrawalsBearer · customer · PIN

Request a payout to the customer's registered bank account. The amount + charge are debited and held. It's a two-step release: the developer admin confirms (amount + charge move to AtomAfrica), then the super admin releases the payout to the beneficiary (status → paid). A decline at either step refunds the customer in full. List the customer's requests with GET /withdrawals.

BodyTypeDescription
amountnumberrequiredNaira to withdraw
pinstringrequired6-digit transaction PIN
Response 200
json
{ "ok": true, "data": { "reference": "WD-7DD1C6B4", "status": "pending", "amount": 40000.00 } }
POST/api/v1/transfers/verify-recipientBearer · customer

Check whether a recipient exists on this network before creating a transfer. Returns their name, KYC status, and whether they are the caller. Transfer is only allowed when kyc_verified = true and is_self = false.

BodyTypeDescription
emailstringrequiredEmail to look up
Response fieldTypeNotes
recipient.namestringDisplay name
recipient.emailstring
recipient.kyc_statusstringverified | pending | not_submitted
recipient.kyc_verifiedbooleantrue only when KYC is fully verified: transfer is enabled
recipient.is_selfbooleantrue when the recipient is the caller: transfer blocked
POST/api/v1/transfersBearer · customer · PIN

Send packets to another customer on your network. Two types: Gift (recipient pays nothing, sender pays fees) and Trade (you set a price per packet; recipient pays that agreed amount to you on acceptance; sender still pays dev fee + network fee). The recipient must accept or reject (POST /transfers/{id}/accept · /reject); list with GET /transfers.

BodyTypeDescription
recipient_emailstringrequiredRecipient's email: must be verified first via /verify-recipient
property_idnumberrequiredWhich property holding to transfer from
packetsnumberrequiredPackets to send (may be fractional)
transfer_typestringoptionalgift (default) or trade
price_per_packetnumbertrade onlyAgreed price in naira per packet; required when transfer_type = trade
pinstringrequiredSender's 6-digit transaction PIN
GET/api/v1/marketBearer · customer

The peer-to-peer secondary market for your network. Browse open listings, then:

MethodPathPurpose
POST/marketList your packets for resale (packets, price_per_packet, pin)
POST/market/{id}/buyBuy a listing (PIN-gated)
POST/market/{id}/cancelCancel your listing
GET/market/{id}/messagesNegotiation thread
POST/market/{id}/messagesPost a negotiation message
POST/api/v1/liquidationsBearer · customer · PIN

Sell packets back to the developer at the buy-back rate. Preview the payout first with POST /liquidations/preview; the request then enters your developer-admin approval queue. List with GET /liquidations.

BodyTypeDescription
property_idnumberrequiredHolding to sell back
packetsnumberrequiredPackets to liquidate (may be fractional)
pinstringrequired6-digit transaction PIN (create only)
POST/api/v1/bid-offersBearer · customer · feature:bid_offers

Submit a bid offer expressing buying interest to all other holders of the same property. Your name is shared with recipients; your contact details are never included. Each holder gets an email; the developer also gets a copy to broker any resulting deal via Transfer or Secondary Market. Requires bid_offers to be enabled on the network.

BodyTypeDescription
property_idnumberrequiredProperty you own packets in
messagestringoptionalShort message (max 500 chars) shown to recipients alongside your name
request
curl -X POST "$API/api/v1/bid-offers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"property_id":12,"message":"Looking to acquire more packets in Palm Estate."}'
Response 201
json
{ "ok": true, "data": { "bid_offer": {
  "id": 3, "property_id": 12, "property_name": "Palm Estate",
  "status": "active", "recipient_count": 4
} } }
Errors
CodeError keyMeaning
403forbiddenYou hold no active packets in this property
409duplicateYou already have an active bid offer on this property

List your own bid offers with GET /bid-offers — returns id, property_name, message, status, recipient_count, created_at. Withdraw an active bid with DELETE /bid-offers/{id}.

Engagement

GET/api/v1/favoritesBearer · customer

The customer's watchlist. Add or remove a property with POST /favorites/{propertyId} and DELETE /favorites/{propertyId}.

GET/api/v1/referralBearer · customer

The customer's referral code, stats and the active program terms. Your developer admin configures the program via the Developer Admin API.

GET/api/v1/notificationsBearer

In-app inbox. Mark read with POST /notifications/read (all) or POST /notifications/{id}/read. Register a device for push with POST /push-tokens (token, platform).

POST/api/v1/contactBearer

Support threads. Open a ticket, list with GET /contact, read one with GET /contact/{id}, and reply with POST /contact/{id}/reply. The channel (customer→developer, or developer→AtomAfrica) is decided by the caller's role.

GET/api/v1/terms/{key}Bearer

Fetch the current terms & conditions for a specific feature before the customer performs an action. The developer's custom copy is returned if they have set one; otherwise the platform default applies. The {{developer_name}} variable is already resolved server-side. Versions increment each time the developer saves a new copy — use version to detect when the customer needs to re-accept.

Path paramValid values
keytransfer · bid_offer · secondary_market · property_listing · installment_plan · buy_packets
request
curl "$API/api/v1/terms/transfer" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": { "terms": {
  "feature_key": "transfer",
  "title": "Transfer Terms & Conditions",
  "content": "By proceeding with this transfer, you acknowledge …",
  "version": 1
} } }

Once the customer accepts, record it server-side with POST /terms/{key}/accept (no body required). List all acceptances for the current user with GET /terms/accepted — returns [{ feature_key, version, accepted_at }]. The mobile SDK uses these together with a localStorage version cache so the modal is only shown when the T&C version changes.

GET/api/v1/properties/{id}/documentsBearer · customer

List the documents attached to a property listing. Only returned to authenticated customers. The developer uploads documents (C of O, survey plans, brochures, etc.) via the admin portal — use masked/redacted copies before attaching.

Response 200
json
{ "ok": true, "data": { "documents": [{
  "id": 5, "label": "Certificate of Occupancy",
  "url": "https://…/uploads/properties/12/docs/coo.pdf",
  "original_name": "coo-masked.pdf", "mime_type": "application/pdf"
}] } }

AtomCredit

AtomCredit lets customers borrow against their property portfolio as an equity release (borrow against existing packet holdings) or purchase finance (borrow to co-fund a new property purchase). The feature is gated: check atomcredit.enabled in /app/config. When disabled all AtomCredit endpoints return 403 feature_disabled.

Eligibility check

GET/api/v1/credit-requests/eligibilityBearer · customer

Check whether the customer can apply and retrieve the current terms for the requested product type. Always call this before showing the application screen so your UI can display the bank name, applicable limits and rate.

QueryTypeDescription
product_typestringoptionalequity_release (default) or purchase_finance
property_idintegeroptionalRequired for purchase_finance: the property the customer intends to buy into
amountnumberoptionalIf supplied, checks whether this specific principal is within the applicable limits
Response 200: eligible
json
{ "ok": true, "data": {
  "eligible": true,
  "product_type": "equity_release",
  "bank_name": "First Trust Bank",
  "interest_rate": 18.0,
  "tenor_range": { "min_months": 3, "max_months": 24 },
  "max_ltv_pct": 30.0,
  "portfolio_value": 2500000.00,
  "max_principal": 750000.00,
  "min_principal": 50000.00,
  "terms_summary": "18% p.a. reducing balance · 3–24 months"
} }
Response 200: not eligible
json
{ "ok": true, "data": { "eligible": false, "reason": "no_portfolio" } }
// reason: no_bank_linked | no_portfolio | active_facility_exists | kyc_required

Submit credit request

POST/api/v1/credit-requestsBearer · customer

Submit a credit request. The request enters the developer admin queue (submitted) and provisional ownership locks are placed on the relevant packets. A sign_deadline applies (default 7 days): the customer must digitally sign the offer deed before this expires.

BodyTypeDescription
product_typestringrequiredequity_release or purchase_finance
property_idintegerrequiredTarget property (equity release: one you hold; purchase finance: a listed property)
tenor_monthsintegerrequiredRepayment tenor in months (within bank's configured range)
requested_principalnumberoptional*Loan amount in naira — required for equity release
contribution_amountnumberoptional*Customer's upfront equity contribution — required for purchase finance
consent_methodstringrequiredHow informed consent was recorded: app_checkbox
request
curl -X POST "$API/api/v1/credit-requests" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"product_type":"equity_release","property_id":12,"tenor_months":12,"requested_principal":500000,"consent_method":"app_checkbox"}'
Response 201
json
{ "ok": true, "data": { "credit_request": {
  "id": 7, "status": "submitted", "product_type": "equity_release",
  "requested_principal": 500000.00, "tenor_months": 12,
  "expires_at": "2026-10-09T10:00:00", "created_at": "2026-10-02T10:00:00"
} } }
Error codeStatusMeaning
active_facility_exists409Customer already has an active AtomCredit facility
active_request_exists409A pending credit request already exists
no_portfolio422No qualifying holdings for equity release
amount_exceeds_ltv422Principal exceeds portfolio × max LTV%
tenor_out_of_range422Tenor is outside the bank's allowed range
property_not_found404Property does not exist on this network

Credit request history

GET/api/v1/credit-requestsBearer · customer

The customer's own credit request history across all statuses. Read a single request with GET /credit-requests/{id} — the single-request response also includes document_version (latest deed) and document_versions (all versions, including the release deed generated after discharge). Fetch the deed HTML with GET /credit-requests/{id}/deed?doc_version_id={dvId}.

Response 200
json
{ "ok": true, "data": {
  "credit_requests": [{
    "id": 7, "product_type": "equity_release", "status": "with_bank",
    "requested_principal": 500000.00, "tenor_months": 12,
    "expires_at": "2026-10-09T10:00:00", "created_at": "2026-10-02T10:00:00"
  }],
  "pagination": { "page": 1, "per": 15, "total": 1, "pages": 1 }
} }
// status lifecycle: submitted → consents_pending → with_bank → approved
//   → pending_signing → (facility created) | declined | cancelled | expired
POST/api/v1/credit-requests/{id}/cancelBearer · customer

Cancel a pending credit request. Only allowed while the request is in submitted, consents_pending, or with_bank status. Cancellation releases all provisional ownership locks immediately.

Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "cancelled" } } }
POST/api/v1/credit-requests/{id}/signBearer · customer

Electronic deed signing: the customer reads and signs the offer deed in-app. The request must be in consents_pending status (developer has confirmed and deed is ready). PIN-gated. Once all parties (customer, developer, bank) have signed, the facility is created and the bank notified to disburse. Fetch the deed HTML first with GET /credit-requests/{id}/deed.

BodyTypeDescription
pinstringrequiredCustomer's 6-digit transaction PIN
signatory_namestringrequiredFull legal name of the signatory as it appears on the deed
rolestringrequiredRole/title of the signatory (e.g. "Customer")
capacitystringoptionalCapacity in which signing (e.g. "Beneficial Owner")
witness_namestringoptionalFull name of the witness to the signature
witness_addressstringoptionalAddress of the witness
Response 200
json
{ "ok": true, "data": {
  "signature": { "id": 22, "party": "customer", "signatory_name": "Ada Okoro",
    "method": "electronic", "signed_at": "2026-10-05T14:22:00" },
  "execution": null
} }
// execution: non-null (facility object) once all 3 parties have signed

Facilities

GET/api/v1/facilitiesBearer · customer

List all AtomCredit facilities for the authenticated customer across all statuses. Read a single facility with GET /facilities/{id}.

Response 200
json
{ "ok": true, "data": {
  "facilities": [{
    "id": 3, "product_type": "equity_release", "status": "active",
    "principal": 500000.00, "outstanding_principal": 450000.00,
    "interest_rate": 18.0, "tenor_months": 12, "monthly_installment": 45833.33,
    "repayment_day_of_month": 5, "loan_ends_at": "2027-09-15",
    "disbursed_at": "2026-09-15T09:00:00",
    "locked_packets": 4, "locked_percentage": 80.0
  }],
  "pagination": { "page": 1, "per": 15, "total": 1, "pages": 1 }
} }
// status: pending_disbursement | pending_allocation | active | in_arrears | enforcement | closed_repaid
GET/api/v1/facilities/{id}/repaymentsBearer · customer

Full repayment schedule for a facility: each instalment's due date, expected amount, and cleared status.

Response 200
json
{ "ok": true, "data": {
  "facility_id": 3, "principal": 500000.00, "total_interest": 90000.00,
  "repayments": [
    { "id": 11, "instalment_number": 1, "due_date": "2026-10-15",
      "expected_amount": 49166.67, "status": "cleared",
      "cleared_at": "2026-10-14T11:00:00" },
    { "id": 12, "instalment_number": 2, "due_date": "2026-11-15",
      "expected_amount": 49166.67, "status": "pending", "cleared_at": null }
  ]
} }
GET/api/v1/facilities/{id}/notificationsBearer · customer

Lifecycle notifications for a facility: disbursement confirmation, repayment reminders, arrears warnings, enforcement notices, etc.

Response 200
json
{ "ok": true, "data": {
  "notifications": [
    { "id": 55, "type": "facility_disbursed",
      "title": "Facility disbursed",
      "body": "Your AtomCredit facility of ₦500,000 has been disbursed.",
      "sent_at": "2026-09-15T09:05:00", "read_at": "2026-09-15T09:12:00" }
  ]
} }

Enforcement cases

GET/api/v1/enforcement-casesBearer · customer

List enforcement cases opened against the customer's facilities. A case is created when the bank serves a default notice. Read a single case with GET /enforcement-cases/{id}.

Response 200
json
{ "ok": true, "data": {
  "enforcement_cases": [{
    "id": 1, "facility_id": 3, "status": "cure_running",
    "opened_at": "2026-10-01T08:00:00",
    "cure_period_ends_at": "2026-10-22T08:00:00",
    "outstanding_at_notice": 98333.34
  }]
} }
// status: notice_served | cure_running | enforcement_notice | listed | offer_accepted | settled | allocated | closed

Developer Admin API

Same base, same envelope, same bearer auth: but these endpoints act on your whole network and require a developer-admin account. Only developer admins can call them; staff and agents are blocked. A call from any other account type returns 403. Your permission keys are visible on the user object from /me.

Admin endpoints live under /api/v1/admin/*. Sign in with your developer-admin account from the admin portal, then drive these programmatically with its bearer token.
MethodPathPermissionPurpose
GET/admin/propertiesproperties.manageList / create / update listings (unit_type, number_of_units, amount_per_unit, packet_price, state, property_category_id, latitude, longitude), estates, allocations, images. Response includes state, sub_category (alias for category), property_category_id.
POST/admin/properties/{id}/returnsreturns.postDistribute a return to owners: per_packet rate × packets held
GET/admin/customerscustomers.viewList customers · detail (balance + wallet history) · CSV import
POST/admin/customers/{id}/fundcustomers.assignDeveloper admin funds a customer (PIN): debits developer, credits customer net, charge → AtomAfrica
POST/admin/notifications/sendcustomers.viewBroadcast to one / all / a KYC segment
GET/admin/kyckyc.reviewReview, approve or decline KYC
POST/admin/withdrawals/{id}/approvewithdrawals.approveDeveloper confirms a pending payout; super admin releases an approved one (sees beneficiary bank). /decline reverses. Also liquidations.
GET/admin/walletwallet.developer.moveNetwork main wallet · top-up · withdraw
GET/admin/staffstaff.manageList / create / activate staff & agents; assign roles (PATCH /role); view payment history
GET/admin/roles · /admin/permissionsstaff.manageSystem & developer custom roles (full CRUD); list developer-group permissions
POST/admin/commissions · /admin/agent-commissionstaff.manageManual commission / salary payment to staff; get/set agent attribution commission rate
GET/admin/staff-withdrawals · /admin/my/*withdrawals.approveStaff & agents request bank payouts; developer approves / declines from queue
GET/admin/featuresfeatures.toggleToggle app modules for your network
POST/admin/referral-programreferral.manageConfigure the referral program
POST/admin/email-templatesemail.templatesEdit branded email templates
GET/admin/transactions · /admin/auditreports.viewNetwork-wide ledger & audit log (CSV export)
GET/admin/chargescharges.developerView/edit your developer-layer fees
GET/admin/bid-offersproperties.manageList customer bid offers; close (withdraw) one with PATCH /admin/bid-offers/{id}
GET/admin/termsbranding.manageList all six feature T&C slots (with custom flag); save with POST /admin/terms/{key}, reset to default with DELETE /admin/terms/{key}. Keys: transfer · bid_offer · secondary_market · property_listing · installment_plan · buy_packets
GET/admin/properties/{id}/documentsproperties.manageList property documents; upload with POST (multipart: label + document); delete with DELETE /admin/properties/{id}/documents/{docId}
GET/admin/property-categoriesproperties.manageList developer's categories (max 3); create (POST), rename (PATCH /:id), delete (DELETE /:id — non-default only, properties reassigned to default)
GET/admin/allocationsproperties.managePlot allocation management: create (POST /admin/properties/{id}/allocate), list all/by-property/pending-docs, dissolve, approve document request, confirm doc issued. Supports co-ownership (allocation shares).

List endpoints accept ?page/?per and, where noted, ?q search and ?format=csv export.

Credit request queue

GET/api/v1/admin/credit-requestsDeveloper admin · customers.view

List all AtomCredit requests on your network. Filter by status to see the ones awaiting your review (submitted). Paginated. Read a single request with GET /admin/credit-requests/{id}.

QueryTypeDescription
statusstringoptionalsubmitted · consents_pending · with_bank · approved · pending_signing · declined · cancelled · expired
product_typestringoptionalequity_release or purchase_finance
page / perintegeroptionalPagination (default page 1, per 20, max 100)
request
curl "$API/api/v1/admin/credit-requests?status=submitted" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "credit_requests": [{
    "id": 7, "product_type": "equity_release", "status": "submitted",
    "customer": { "id": 9, "name": "Ada Okoro", "kyc_status": "approved" },
    "requested_principal": 500000.00, "tenor_months": 12,
    "expires_at": "2026-10-09T10:00:00", "created_at": "2026-10-02T10:00:00"
  }],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }
POST/api/v1/admin/credit-requests/{id}/confirmDeveloper admin · customers.view

Maker step. First developer admin confirms the request passes internal checks. Status moves submitted → consents_pending. A different developer admin must then call approve-confirm (checker step) before the request reaches the bank. Provisional locks placed at submission remain in effect.

BodyTypeDescription
notestringoptionalInternal note attached to the audit trail
request
curl -X POST "$API/api/v1/admin/credit-requests/7/confirm" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"note":"KYC verified, holdings confirmed."}'
Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "consents_pending" } } }
Errors
CodeError keyMeaning
409not_found_or_wrong_statusRequest is not in submitted state
POST/api/v1/admin/credit-requests/{id}/approve-confirmDeveloper admin · customers.view

Checker step. A different developer admin (not the one who called confirm) approves the confirmation and forwards the request to the bank. Status moves consents_pending → with_bank. Returns maker_checker_violation if the same admin calls both steps.

Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "with_bank" } } }
Errors
CodeError keyMeaning
409not_confirmed_yetMaker step has not been completed
403maker_checker_violationSame admin cannot perform both maker and checker steps
POST/api/v1/admin/credit-requests/{id}/declineDeveloper admin · customers.view

Decline the request at the developer level. Status moves to declined and all provisional ownership locks are released. The customer is notified.

BodyTypeDescription
reasonstringoptionalDecline reason shown to the customer
Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "declined" } } }
POST/api/v1/admin/credit-requests/{id}/signDeveloper admin · customers.view

Counter-sign the offer deed on behalf of the developer. Request must be in pending_signing status. PIN-gated. Once both the customer and the developer have signed, the facility is created automatically.

BodyTypeDescription
pinstringrequiredDeveloper admin's 6-digit transaction PIN
Response 200
json
{ "ok": true, "data": {
  "credit_request": { "id": 7, "status": "pending_signing" },
  "facility_created": true,
  "facility_id": 3
} }

Facilities

GET/api/v1/admin/facilitiesDeveloper admin · customers.view

List all active or historical AtomCredit facilities on your network. Filter by status or customer_id. Read a single facility with GET /admin/facilities/{id}.

QueryTypeDescription
statusstringoptionalpending_disbursement · pending_allocation · active · in_arrears · enforcement · closed_repaid
customer_idintegeroptionalFilter by customer
Response 200
json
{ "ok": true, "data": {
  "facilities": [{
    "id": 3, "product_type": "equity_release", "status": "active",
    "customer": { "id": 9, "name": "Ada Okoro" },
    "principal": 500000.00, "outstanding_principal": 450000.00,
    "interest_rate": 18.0, "tenor_months": 12,
    "disbursed_at": "2026-09-15T09:00:00", "loan_ends_at": "2027-09-15"
  }],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }

AtomCredit reports

GET/api/v1/admin/reports/chargesDeveloper admin · reports.view

All properties on your network that are currently under an AtomCredit charge (have one or more active facilities), with the total locked value and number of active facilities per property.

Response 200
json
{ "ok": true, "data": {
  "charges": [{
    "property_id": 12, "property_name": "Lekki Gardens Phase 1",
    "active_facilities": 3, "total_principal": 1500000.00,
    "locked_packets": 12
  }]
} }
GET/api/v1/admin/reports/confirmationsDeveloper admin · reports.view

All purchase finance facilities where the developer has confirmed the customer's equity contribution, with confirmation dates and amounts.

Response 200
json
{ "ok": true, "data": {
  "confirmations": [{
    "facility_id": 3, "customer_name": "Ada Okoro", "property_name": "Lekki Gardens Phase 1",
    "contribution_amount": 200000.00, "confirmed_at": "2026-09-14T10:00:00"
  }]
} }
GET/api/v1/admin/reports/allocationsDeveloper admin · reports.view

All unit allocations made under purchase finance facilities, including the unit reference, customer, and allocation date.

Response 200
json
{ "ok": true, "data": {
  "allocations": [{
    "facility_id": 3, "unit_id": 88, "unit_reference": "LG-B2-P14",
    "customer_name": "Ada Okoro", "property_name": "Lekki Gardens Phase 1",
    "allocated_at": "2026-09-15T08:30:00"
  }]
} }

Customers

GET/api/v1/admin/customersDeveloper admin · customers.view

Paginated customer list for your network. Filter by q (name/email/phone) or kyc_status.

request
curl "$API/api/v1/admin/customers?q=ada&kyc_status=approved" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "customers": [
    { "id": 9, "user_id": 14, "name": "Ada Okoro", "email": "ada@example.com",
      "phone": "08012345678", "kyc_status": "approved",
      "acquisition_channel": "app", "created_at": "2026-08-01T10:00:00" }
  ],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }
POST/api/v1/admin/customersDeveloper admin · customers.view

Create a customer directly (no self-registration). A temporary password is generated and emailed. Returns the customer record and the temporary password (shown once).

BodyTypeDescription
namestringrequiredFull name
emailstringrequiredLogin email
phonestringrequiredMobile number
request
curl -X POST "$API/api/v1/admin/customers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Ada Okoro","email":"ada@example.com","phone":"08012345678"}'
Response 201
json
{ "ok": true, "data": {
  "customer": { "id": 9, "name": "Ada Okoro", "email": "ada@example.com", "phone": "08012345678" },
  "temp_password": "7f3a9c12!84"
} }

Developer wallet

GET/api/v1/admin/walletDeveloper admin · wallet.developer.move

Current balance and ledger entries for your network's main wallet. The same wallet is debited when you fund customers or pay commissions.

request
curl "$API/api/v1/admin/wallet" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "balance": 480000.00,
  "entries": [
    { "direction": "credit", "amount": 500000.00, "balance_after": 480000.00,
      "description": "Developer wallet top-up (DEV-DEP-7A3F)", "at": "2026-09-01T09:00:00" }
  ]
} }
POST/api/v1/admin/wallet/topupDeveloper admin · wallet.developer.move

Fund your network wallet (PIN-gated). A deposit fee is deducted and routed to AtomAfrica. Returns the reference, new balance and fee charged.

BodyTypeDescription
amountnumberrequiredAmount in naira (must be > 0)
pinstringrequiredYour 6-digit transaction PIN
request
curl -X POST "$API/api/v1/admin/wallet/topup" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"amount":500000,"pin":"123456"}'
Response 200
json
{ "ok": true, "data": {
  "reference": "DEV-DEP-7A3F2C1B",
  "balance": 480000.00,
  "deposit_fee": 2500.00
} }
POST/api/v1/admin/wallet/withdrawDeveloper admin · wallet.developer.move

Withdraw from your network wallet (PIN-gated). Requires a payout bank account to be registered. The request enters the approval queue; a super admin releases it.

BodyTypeDescription
amountnumberrequiredAmount in naira
pinstringrequiredYour 6-digit transaction PIN
request
curl -X POST "$API/api/v1/admin/wallet/withdraw" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"amount":100000,"pin":"123456"}'
Response 200
json
{ "ok": true, "data": { "reference": "DEV-WD-B4C1A902", "status": "pending" } }
Errors
CodeError keyMeaning
402insufficient_fundsBalance is less than the requested amount
409bank_requiredNo payout bank account registered

Team (staff & agents)

Staff are internal accounts granted permissions via roles. Agents are sales representatives who earn commission on attributed customer purchases. Both are created by the developer admin and scoped to the network.

GET/api/v1/admin/staffDeveloper admin · staff.manage

Paginated list of all staff and agents in the network. Returns role_id alongside the role name for use in assignment UI.

Response 200
json
{ "ok": true, "data": {
  "team": [{
    "id": 5, "name": "Emeka Obi", "email": "emeka@corp.com", "phone": "+2348012345678",
    "type": "staff", "status": "active",
    "role": "Accounting", "role_id": 3,
    "created_at": "2026-10-01T08:00:00"
  }],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }
POST/api/v1/admin/staffDeveloper admin · staff.manage

Create a staff member or agent. A one-time temporary password and PIN are returned in the response — share them with the new member directly. For staff, supply a role_id; agents always receive the system agent role.

BodyTypeDescription
namestringrequiredFull name
emailstringrequiredMust be globally unique across the entire platform — no two non-customer accounts (developer admin, staff, agent, bank staff, bank admin) may share an email address
typestringrequiredstaff or agent
phonestringoptionalPhone number
role_idintegeroptional*Required when type = staff. Must be a system role or a custom role owned by this developer. Ignored for agents.
request
curl -X POST "$API/api/v1/admin/staff" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Emeka Obi","email":"emeka@corp.com","type":"staff","role_id":3}'
Response 201
json
{ "ok": true, "data": {
  "user": { "id": 5, "name": "Emeka Obi", "email": "emeka@corp.com", "type": "staff" },
  "temp_password": "a3f8b2!47",
  "temp_pin": "293847"
} }
Errors
CodeError keyMeaning
409email_takenThat email is already in use by another admin, staff, or agent account on the platform
PATCH/api/v1/admin/staff/{id}Developer admin · staff.manage

Update a team member's status.

BodyTypeDescription
statusstringrequiredactive · inactive · suspended
PATCH/api/v1/admin/staff/{id}/roleDeveloper admin · staff.manage

Reassign a staff member to a different role. Replaces the previous role assignment. Only applies to staff type, not agents.

BodyTypeDescription
role_idintegerrequiredID of the target role (system or developer-owned)
request
curl -X PATCH "$API/api/v1/admin/staff/5/role" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"role_id":4}'
Response 200
json
{ "ok": true, "data": { "assigned": true, "role": "Finance", "role_id": 4 } }

Paying staff, agents & partners

POST/api/v1/admin/commissionsDeveloper admin · staff.manage

Manually credit a staff member, agent, or API partner's wallet directly from the developer wallet. No AtomAfrica charge on internal payouts. Use kind: "salary" for regular payments and kind: "commission" for performance bonuses.

For staff and agent, pass the user's id (from GET /admin/staff). For partner, pass the partner row id (from GET /admin/partners), not the partner's portal user id.
BodyTypeDescription
recipient_idintegerrequiredUser ID (staff/agent) or partner row ID (partner)
recipient_typestringrequiredstaff · agent · partner
amountnumberrequiredAmount in naira (must be > 0)
kindstringrequiredcommission or salary
request
curl -X POST "$API/api/v1/admin/commissions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"recipient_id":5,"recipient_type":"staff","amount":50000,"kind":"salary"}'
Response 201
json
{ "ok": true, "data": { "reference": "SAL-8A3F1C…", "amount": 50000.00,
  "to": "staff", "recipient": "Emeka Obi", "kind": "salary" } }
Errors
CodeError keyMeaning
402insufficient_developer_fundsDeveloper wallet balance too low
422commission_errorRecipient not found in this network, or invalid type/kind
GET/api/v1/admin/staff/{id}/paymentsDeveloper admin · staff.manage

Wallet balance and last 50 commission/salary entries for a staff member.

Response 200
json
{ "ok": true, "data": {
  "name": "Emeka Obi", "balance": 150000.00,
  "payments": [
    { "reference": "COMM-8A3F…", "kind": "salary", "amount": 50000.00, "created_at": "2026-10-01" }
  ]
} }

Roles & permissions

System roles (staff, agent, developer_admin) are read-only. Developer admins can create custom roles with any subset of developer-group permissions. Every staff member must be assigned a role; agents always use the system agent role.

GET/api/v1/admin/rolesDeveloper admin · staff.manage

All roles visible to this developer: system roles (shared across networks) plus custom roles created by this developer. Each role includes its id, permission key array, and system flag.

Response 200
json
{ "ok": true, "data": {
  "roles": [
    { "id": 1, "key": "developer_admin", "name": "Developer Admin", "is_system": true,
      "permissions": ["properties.manage","customers.view","staff.manage","…"] },
    { "id": 3, "key": "custom_13_accounting", "name": "Accounting", "is_system": false,
      "permissions": ["earnings.view","reports.view"] }
  ]
} }
GET/api/v1/admin/permissionsDeveloper admin · staff.manage

Full catalogue of developer-group permissions — the universe of keys you can include in a custom role.

Response 200
json
{ "ok": true, "data": {
  "permissions": [
    { "id": 4, "key": "earnings.view", "description": "View own earnings and commission history" },
    { "id": 7, "key": "properties.manage", "description": "Create and update property listings" }
  ]
} }
POST/api/v1/admin/rolesDeveloper admin · staff.manage

Create a custom role for this developer network. The key is auto-generated as custom_{devId}_{slug}.

BodyTypeDescription
namestringrequiredHuman-readable role name, e.g. "Accounting"
permission_idsinteger[]optionalPermission IDs from GET /admin/permissions
request
curl -X POST "$API/api/v1/admin/roles" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Accounting","permission_ids":[4,7]}'
Response 201
json
{ "ok": true, "data": { "id": 3, "key": "custom_13_accounting", "name": "Accounting" } }
Errors
CodeError keyMeaning
409duplicateA role with that name already exists for this developer
PATCH/api/v1/admin/roles/{id}Developer admin · staff.manage

Update a custom role's name and/or permissions. Passing permission_ids replaces the full permission set. System roles cannot be updated.

BodyTypeDescription
namestringoptionalNew display name
permission_idsinteger[]optionalReplaces the current permission set entirely
DELETE/api/v1/admin/roles/{id}Developer admin · staff.manage

Delete a custom developer role. Fails if any staff member is currently assigned to it — reassign them first.

Errors
CodeError keyMeaning
409has_membersOne or more staff members are assigned to this role
POST/api/v1/admin/roles/{id}/assignDeveloper admin · staff.manage

Assign a staff member to this role, replacing their previous assignment. Alternative to PATCH /admin/staff/{id}/role — use whichever direction suits your UI.

BodyTypeDescription
staff_idintegerrequiredUser ID of the staff member

Agent commission

When a customer registers with an agent's referral code, they are permanently attributed to that agent. On every subsequent purchase the agent earns a percentage of the sale, credited automatically to their wallet. Rates can be set globally or per-property.

GET/api/v1/admin/agent-commissionDeveloper admin · staff.manage

Global commission rate and any property-specific overrides.

Response 200
json
{ "ok": true, "data": {
  "global_rate": 2.5,
  "rules": [
    { "property_id": 12, "property_name": "Sunset Estate", "rate_percent": 4.0 }
  ]
} }
POST/api/v1/admin/agent-commissionDeveloper admin · staff.manage

Set or remove agent commission rates. Two scopes:

  • Global rate — omit property_id. Applies to all purchases unless a property override exists.
  • Property override — include property_id. Takes precedence over the global rate for purchases of that property only. Pass "rate_percent": null to remove the override and fall back to the global rate.
BodyTypeDescription
rate_percentnumber | nullrequiredCommission % of the purchase amount (e.g. 2.5). Pass null together with a property_id to remove that property's override.
property_idintegeroptionalOmit to set the global rate; supply to set or remove a per-property override.
request
# Global rate
curl -X POST "$API/api/v1/admin/agent-commission" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"rate_percent":2.5}'

# Property override
curl -X POST "$API/api/v1/admin/agent-commission" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"rate_percent":4.0,"property_id":12}'

# Remove property override (reverts to global rate)
curl -X POST "$API/api/v1/admin/agent-commission" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"rate_percent":null,"property_id":12}'
Response 200 — set
json
{ "ok": true, "data": { "saved": true } }
Response 200 — override removed
json
{ "ok": true, "data": { "deleted": true } }
GET/api/v1/admin/agent-commission/statsAgent · earnings.view

An agent's own attribution stats — attributed customer count, total commission earned, and recent history. Called by the agent's own session.

Response 200
json
{ "ok": true, "data": {
  "attributed_customers": 14, "total_earned": 87500.00,
  "recent": [
    { "amount": 5000.00, "property": "Sunset Estate", "created_at": "2026-10-05" }
  ]
} }

Staff, agent & partner payouts

Staff and agents save a bank account once and request withdrawals from their wallet; partners supply bank details per-request (no stored account). The developer reviews all pending requests in one queue and approves or declines each; the physical bank transfer is made manually outside the platform.

Staff / agent — own payouts

GET/api/v1/admin/my/bank-accountStaff or agent · earnings.view

Return the staff/agent's saved bank account details, or null if none saved yet.

Response 200
json
{ "ok": true, "data": {
  "bank_account": { "bank_name": "Access Bank", "account_number": "0123456789", "account_name": "EMEKA OBI" }
} }
POST/api/v1/admin/my/bank-accountStaff or agent · earnings.view

Save or update personal bank account details. One account per user; updates replace the existing record.

BodyTypeDescription
bank_namestringrequiredBank name
account_numberstringrequired10-digit NUBAN account number
account_namestringrequiredName as it appears on the account
POST/api/v1/admin/my/withdrawStaff or agent · earnings.view

Request a withdrawal from the staff/agent's wallet. Min ₦100. Only one pending request at a time is allowed. Funds are not moved until the developer approves.

BodyTypeDescription
amountnumberrequiredAmount in naira (min ₦100)
Response 201
json
{ "ok": true, "data": { "reference": "SWR-F3A1C8…", "status": "pending", "amount": 50000.00 } }
Errors
CodeError keyMeaning
409pending_existsA withdrawal is already pending — wait for it to be processed
402insufficient_fundsWallet balance below the requested amount
409bank_requiredNo bank account saved yet
GET/api/v1/admin/my/withdrawal-requestsStaff or agent · earnings.view

List the staff/agent's own withdrawal requests and their current status.

Response 200
json
{ "ok": true, "data": {
  "requests": [
    { "id": 2, "reference": "SWR-F3A1…", "amount": 50000.00,
      "status": "pending", "created_at": "2026-10-09T14:00:00" }
  ]
} }

Developer — approval queue

GET/api/v1/admin/staff-withdrawalsDeveloper admin · withdrawals.approve

List all staff and agent withdrawal requests for this network. Filter with ?status=pending.

Response 200
json
{ "ok": true, "data": {
  "requests": [{
    "id": 2, "user_name": "Emeka Obi", "user_type": "staff",
    "amount": 50000.00, "bank_name": "Access Bank",
    "account_number": "0123456789", "account_name": "EMEKA OBI",
    "status": "pending", "created_at": "2026-10-09T14:00:00"
  }]
} }
POST/api/v1/admin/staff-withdrawals/{id}/approveDeveloper admin · withdrawals.approve

Approve a pending withdrawal request. Immediately debits the staff/agent's wallet and marks the request approved. Complete the bank transfer manually.

Response 200
json
{ "ok": true, "data": { "approved": true, "reference": "SWR-F3A1…" } }
POST/api/v1/admin/staff-withdrawals/{id}/declineDeveloper admin · withdrawals.approve

Decline a pending withdrawal. No wallet movement; optionally provide a reason.

BodyTypeDescription
notestringoptionalReason shown to the staff/agent

API Partners

API Partners are third-party platforms (fintechs, agents, bill-payment apps) that sell your properties to their own customers via an API key. Each partner earns a configurable commission on sales. The developer creates and manages partners; partners access a dedicated portal (same login page) and a REST integration API.

Two funding models (set at creation, switchable any time): Trust — partner asserts they collected funds externally; the customer wallet is credited immediately and a reconciliation balance accrues on the partner row until the developer marks it settled. Debit — partner pre-funds their AtomOwn wallet; every fund call debits the partner wallet in real time; the call is rejected if insufficient.
Super admin cannot create partners — only developer admins can. Super admin has read-only oversight.
GET/api/v1/admin/partnersDeveloper admin · partners.manage

List all partners for this developer. Super admin sees all partners across networks with a developer_name field.

Response 200
json
{ "ok": true, "data": { "partners": [{
  "id": 3, "name": "PayFast NG", "contact_email": "dev@payfast.ng",
  "status": "active", "funding_model": "trust",
  "commission_rate": 2.5, "commission_trigger": "purchase",
  "reconciliation_balance": 125000.00,
  "customer_count": 18, "total_commission": 37500.00,
  "created_at": "2026-10-01T09:00:00"
}] } }
POST/api/v1/admin/partnersDeveloper admin · partners.manage

Create a new API partner. This generates an API key for third-party integration AND a portal login (email + temporary password) in one call. Save all three credentials on first display — they cannot be retrieved again.

BodyTypeDescription
namestringrequiredPartner display name
contact_emailstringrequiredPortal login email — must be globally unique across all portal users (staff, agents, developers, partners, banks)
commission_ratenumberrequiredCommission percentage (e.g. 2.5 for 2.5%)
commission_triggerstringrequiredpurchase · each_installment · conversion
funding_modelstringrequiredtrust or debit
Response 201
json
{ "ok": true, "data": {
  "partner": { "id": 3, "name": "PayFast NG", "status": "active",
               "funding_model": "trust", "commission_rate": 2.5 },
  "api_key": "pk_live_4f8c2b1d…",
  "portal_email": "dev@payfast.ng",
  "portal_password": "a3f9c1e0b2d5f8a4"
} }
Errors
CodeError keyMeaning
409email_takenEmail already in use by another portal user
403forbiddenSuper admin cannot create partners
PUT/api/v1/admin/partners/{id}Developer admin · partners.manage

Update partner settings. All fields are optional — send only what changes. Switching funding_model takes effect immediately on the next fund call; existing balances are unaffected.

BodyTypeDescription
namestringoptionalNew display name
contact_emailstringoptionalNew portal login email (must remain globally unique)
commission_ratenumberoptionalNew commission percentage
commission_triggerstringoptionalpurchase · each_installment · conversion
funding_modelstringoptionaltrust or debit
statusstringoptionalactive or inactive
POST/api/v1/admin/partners/{id}/regenerate-keyDeveloper admin · partners.manage

Invalidate the current API key and generate a new one. Requires the developer's admin PIN. The old key stops working immediately.

BodyTypeDescription
pinstringrequired6-digit admin transaction PIN
Response 200
json
{ "ok": true, "data": { "api_key": "pk_live_9c3e1a2f…" } }
POST/api/v1/admin/partners/{id}/reconcileDeveloper admin · partners.manage

Trust model only. Mark the outstanding reconciliation balance as settled (partner has remitted the funds externally). Resets reconciliation_balance to zero and logs the event with the amount cleared. Call this after the partner pays you by bank transfer or other means.

BodyTypeDescription
notestringoptionalReconciliation note (e.g. "Ref: GTB 20261015")
Response 200
json
{ "ok": true, "data": {
  "reconciled_naira": 125000.00,
  "message": "Reconciliation complete. Balance reset to ₦0."
} }
Errors
CodeError keyMeaning
422not_trust_modelPartner is on the debit model — no reconciliation needed
422nothing_to_reconcileBalance is already zero

Bid offers

GET/api/v1/admin/bid-offersDeveloper admin · properties.manage

List bid offers submitted by customers on your network's properties. Filter by status to see active bids. The customer's name is included; contact details are not surfaced here either — the developer brokers the deal directly.

QueryTypeDescription
statusstringoptionalactive (default) or withdrawn — omit for all
page / perintegeroptionalPagination (default page 1, per 25)
request
curl "$API/api/v1/admin/bid-offers?status=active" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "bid_offers": [{
    "id": 3, "property_id": 12, "property_name": "Palm Estate",
    "customer_id": 42, "customer_name": "Ada Okoro",
    "message": "Looking to acquire more packets.",
    "status": "active", "created_at": "2026-10-13 09:14:00"
  }],
  "count": 1,
  "pagination": { "page": 1, "per": 25, "total": 1, "pages": 1 }
} }

Close (withdraw) a bid offer on behalf of the customer with PATCH /admin/bid-offers/{id} (empty body). Returns { "closed": true }.

Feature Terms & Conditions

GET/api/v1/admin/termsDeveloper admin · branding.manage

List all six feature T&C slots with their current content, version and whether a custom copy is active. If is_custom is false, the platform default is in use. The variables array lists substitution tokens available in content (e.g. {{developer_name}}).

request
curl "$API/api/v1/admin/terms" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "terms": [{
    "feature_key": "transfer", "title": "Transfer Terms & Conditions",
    "content": "By proceeding with this transfer …",
    "version": 1, "is_custom": false, "updated_at": null
  }, { "feature_key": "bid_offer", … }, { "feature_key": "secondary_market", … },
     { "feature_key": "property_listing", … }, { "feature_key": "installment_plan", … },
     { "feature_key": "buy_packets", … }],
  "variables": [{ "name": "{{developer_name}}", "description": "Your company name as set in branding settings" }]
} }

Save a custom T&C for a feature with POST /admin/terms/{key} — body: { title, content }. The version is incremented automatically; customers who previously accepted will see the updated modal on their next action. Reset to the platform default with DELETE /admin/terms/{key}.

Path paramValid values
keytransfer · bid_offer · secondary_market · property_listing · installment_plan · buy_packets

Property documents

GET/api/v1/admin/properties/{id}/documentsDeveloper admin · properties.manage

List, upload and delete documents attached to a property listing. Documents are visible to authenticated customers via GET /properties/{id}/documents. Only attach masked or redacted copies — do not expose raw title documents.

MethodPathPurpose
GET/admin/properties/{id}/documentsList documents (id, label, url, original_name, file_size, created_at)
POST/admin/properties/{id}/documentsUpload a document — multipart/form-data: label (text) + document (file). Accepted: PDF, JPEG, PNG, WEBP, DOC, DOCX · max 10 MB
DELETE/admin/properties/{id}/documents/{docId}Delete a document (also removes the file from disk)
request
curl -X POST "$API/api/v1/admin/properties/12/documents" \
  -H "Authorization: Bearer $TOKEN" \
  -F "label=Certificate of Occupancy" \
  -F "document=@/path/to/coo-masked.pdf"
Response 201
json
{ "ok": true, "data": { "document": {
  "id": 5, "label": "Certificate of Occupancy",
  "url": "https://…/uploads/properties/12/docs/coo-masked.pdf",
  "original_name": "coo-masked.pdf", "file_size": 204800,
  "mime_type": "application/pdf", "created_at": "2026-10-13T10:00:00"
} } }

Property categories

GET/api/v1/admin/property-categoriesDeveloper admin · properties.manage

Manage developer-scoped categories that group listings into tabs on the customer discover page. A developer can have at most 3 categories. The first is the default — created automatically when the developer posts their first property (named "Properties" unless renamed). The default category cannot be deleted; the other two can (their properties are reassigned to the default). All category names are editable with PATCH.

MethodPathPurpose
GET/admin/property-categoriesList all categories in position order
POST/admin/property-categoriesCreate a category — body: { name }. Fails with max_categories if already at 3
PATCH/admin/property-categories/{id}Rename — body: { name }. Works on the default category too
DELETE/admin/property-categories/{id}Delete a non-default category; its properties move to the default
request
curl "$API/api/v1/admin/property-categories" \
  -H "Authorization: Bearer $TOKEN"

curl -X POST "$API/api/v1/admin/property-categories" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Phase 2"}'

curl -X PATCH "$API/api/v1/admin/property-categories/3" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Residential"}'
Response 200 — GET list
json
{ "ok": true, "data": { "categories": [
  { "id": 3, "name": "Properties", "is_default": true,  "position": 1 },
  { "id": 1, "name": "Phase 1",    "is_default": false, "position": 2 }
] } }
Response 201 — POST create
json
{ "ok": true, "data": { "category": { "id": 2, "name": "Phase 2", "is_default": false, "position": 2 } } }

When a property is created without a property_category_id, it is assigned to the default category automatically. Pass property_category_id in POST /admin/properties or PATCH /admin/properties/{id} to assign or reassign a listing.

Allocations

POST/api/v1/admin/properties/{id}/allocateDeveloper admin · properties.manage

Assign a physical plot number to a customer's packet holding. Allocations model the physical unit behind the fractional ownership: a plot number, a unit fraction (how much of one full unit the allocation represents), and optionally co-ownership (multiple customers share one plot). The minimum fraction is 0.5 — sub-half-unit allocations are not supported. Co-owners get individual allocation_shares rows.

MethodPathPurpose
POST/admin/properties/{id}/allocateCreate allocation — assigns a plot to a customer
GET/admin/properties/{id}/allocationsList allocations for a property
GET/admin/allocationsList all allocations (filterable by ?property_id= and ?status=)
GET/admin/allocations/pending-docsList allocations with pending document requests
GET/admin/allocations/{id}Single allocation detail with all shares
PATCH/admin/allocations/{id}/dissolveDissolve an allocation (only while status=active and before any doc request)
PATCH/admin/allocations/{id}/approve-docApprove a doc request — set pickup location, status → doc_pending_pickup
PATCH/admin/allocations/{id}/confirm-docConfirm physical document was handed to customer — status → doc_issued (permanent lock)
Body (POST allocate)TypeDescription
customer_email or customer_idstring / intrequiredCustomer to assign the plot to
plot_numberstringrequiredPhysical plot identifier, unique per property (e.g. A12)
unit_fractionnumberoptionalFraction of a full unit (default 1.0, minimum 0.5, rounded to 0.5 increments)
request
curl -X POST "$API/api/v1/admin/properties/12/allocate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"customer_email":"ada@example.com","plot_number":"A12","unit_fraction":1.0}'
Response 201
json
{ "ok": true, "data": { "allocation": {
  "id": 1, "property_id": 12, "plot_number": "A12",
  "unit_fraction": 1.0, "total_packets": 100, "status": "active",
  "shares": [{ "customer_id": 42, "customer_name": "Ada Okoro", "packets": 100 }]
} } }

Status machine: active → customer requests document → doc_requested → developer approves with pickup location → doc_pending_pickup → developer confirms handover → doc_issued (permanent). dissolved is a terminal state set by the developer from active only. Doc-issued packets are locked: they cannot be transferred, sold on secondary market, or used as loan collateral.

Co-ownership: if a plot's total packets are split across more than one customer (each has their own share), no single customer owns 100% and therefore no one can request the physical document until they consolidate. can_request_doc on the customer allocation response signals eligibility.

Document request (customer-facing): POST /allocations/{id}/doc-request · DELETE /allocations/{id}/doc-request (cancel). See Property documents for the customer-side listing.

Partner API

The Partner API lets third-party platforms (fintechs, agents, bill-payment apps) sell your properties to their own customer base, credit customer wallets, and trigger purchases — all under your developer brand. Partners are created by you (the developer) and earn configurable commissions.

There are two integration modes for partners:

ModeAuth headerUse case
Third-party APIX-Partner-Key: pk_live_…Server-to-server integration: enroll customers, fund wallets, trigger purchases programmatically from the partner's own backend
Partner portalAuthorization: Bearer …Web dashboard login (same URL as staff/developer portal): view customers, commissions, wallet, and request withdrawals
All /api/v1/partner/* endpoints use X-Partner-Key. All /api/v1/admin/partner-portal/* endpoints use a Bearer token obtained from the standard POST /auth/login (omit the developer field for portal users).

Enroll customer

POST/api/v1/partner/customersPartner API key

Register a new customer on the developer's network via the partner. Returns a unique ref that identifies this customer in all subsequent partner calls. The partner's commission is attributed to sales this customer makes.

BodyTypeDescription
namestringrequiredFull name
emailstringrequiredCustomer email
phonestringoptionalPhone number
passwordstringoptionalInitial password. If omitted a random one is generated.
request
curl -X POST "$API/api/v1/partner/customers" \
  -H "X-Partner-Key: pk_live_4f8c2b1d…" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada Okoro","email":"ada@example.com","phone":"08012345678"}'
Response 201
json
{ "ok": true, "data": {
  "customer": { "ref": "USR-A1B2C3", "name": "Ada Okoro", "email": "ada@example.com" }
} }

Fund customer wallet

POST/api/v1/partner/customers/{ref}/wallet/fundPartner API key

Credit a customer's wallet with funds collected by the partner. Behaviour depends on the funding model set for this partner:

  • Trust model: customer wallet is credited immediately. The amount is added to the partner's reconciliation_balance — the running total the partner owes the developer. The developer reconciles this manually when the partner remits.
  • Debit model: the same amount is first debited from the partner's own wallet and then credited to the customer. The call is rejected if the partner wallet has insufficient balance.
BodyTypeDescription
amountnumberrequiredAmount in naira
referencestringoptionalYour idempotency reference — repeated calls with the same reference are no-ops
notestringoptionalMemo shown in the customer's transaction history
request
curl -X POST "$API/api/v1/partner/customers/USR-A1B2C3/wallet/fund" \
  -H "X-Partner-Key: pk_live_4f8c2b1d…" \
  -H "Content-Type: application/json" \
  -d '{"amount":50000,"reference":"PF-TXN-00123","note":"Payment for Palm Estate"}'
Response 201
json
{ "ok": true, "data": {
  "funded": 50000.00, "customer_balance": 80000.00,
  "funding_model": "trust",
  "reconciliation_balance": 125000.00
} }
Errors
CodeError keyMeaning
402insufficient_fundsDebit model: partner wallet balance too low
409duplicate_referenceThis reference was already processed

Purchase & installments

Partners can trigger purchases and installment plans on behalf of enrolled customers. The customer's wallet must have sufficient balance first (use fund wallet). Commission is attributed to the partner according to the commission_trigger setting.

MethodPathPurpose
POST/partner/customers/{ref}/purchaseBuy packets — body: { property_id, amount, pin } (customer's PIN)
POST/partner/customers/{ref}/installmentsSubscribe to an installment plan — body: { property_id, installment_amount, frequency, tenor_count, pin }
GET/partner/customers/{ref}/installmentsList the customer's active installment plans
GET/partner/customers/{ref}/installments/{planId}Show a single plan with payment schedule
POST/partner/customers/{ref}/installments/{planId}/payPay one installment — body: { amount, pin }

Customer portfolio & transactions

MethodPathPurpose
GET/partner/customers/{ref}/portfolioAll holdings for the customer (units, packets, value)
GET/partner/customers/{ref}/portfolio/{holdingId}Single holding detail
GET/partner/customers/{ref}/transactionsCustomer ledger — ?page&per
GET/partner/customers/{ref}Customer profile (name, email, status, wallet balance)
GET/partner/customersAll customers enrolled by this partner — paginated
GET/partner/propertiesActive properties on the developer's network
GET/partner/properties/{id}Single property detail

Partner wallet & commissions

GET/api/v1/partner/walletPartner API key

Partner's own wallet: balance, total commission earned, and pending withdrawal amount.

Response 200
json
{ "ok": true, "data": {
  "wallet": {
    "balance": 37500.00,
    "total_commission_earned": 42000.00,
    "pending_withdrawal": 0.00,
    "funding_model": "trust",
    "reconciliation_balance": 125000.00
  }
} }
GET/api/v1/partner/wallet/transactionsPartner API key

Full ledger for the partner's wallet. Filterable.

QueryTypeDescription
typestringall (default) · commission · topup · withdrawal · fund_out
directionstringall (default) · credit · debit
from / tostringDate range — YYYY-MM-DD
page / perintegerPagination (default per 25)
Response 200
json
{ "ok": true, "data": {
  "transactions": [{
    "date": "2026-10-14T10:22:00", "description": "Commission — COM-9F2C…",
    "txn_type": "commission", "direction": "credit",
    "amount_naira": 2500.00, "balance_after": 37500.00
  }],
  "pagination": { "page": 1, "per": 25, "total": 12, "pages": 1 }
} }
POST/api/v1/partner/withdrawPartner API key

Request a withdrawal from the partner's wallet. Bank details are supplied per-request (no stored bank account for partners). The developer approves from the same withdrawal queue as staff/agent withdrawals.

BodyTypeDescription
amountnumberrequiredAmount in naira
bank_codestringrequired3-digit CBN bank code (e.g. 058)
account_numberstringrequired10-digit NUBAN
account_namestringrequiredAccount name
Response 201
json
{ "ok": true, "data": {
  "withdrawal": { "reference": "PWR-3A1C8F…", "amount": 30000.00, "status": "pending" }
} }
GET/api/v1/partner/wallet/withdrawalsPartner API key

List all past withdrawal requests for this partner with status and bank details.

GET/api/v1/partner/commissionsPartner API key

Commission earn history — each entry shows the customer, base transaction amount, rate applied, and commission credited to the partner wallet.

Partner portal (web dashboard)

Partners log into the admin portal at the same URL as staff and developers. The portal recognises the partner account type and shows a restricted view: dashboard, customers, commissions, and wallet. All portal endpoints use a standard Bearer token from POST /auth/login (no developer field needed).

MethodPathPurpose
GET/admin/partner-portal/meFull partner profile: developer name, funding model, wallet balance, commission totals, customer count
GET/admin/partner-portal/walletWallet summary: balance, total earned, pending withdrawal, reconciliation balance
GET/admin/partner-portal/wallet/transactionsLedger — same filters as the API key version (type, direction, from, to)
GET/admin/partner-portal/wallet/withdrawalsWithdrawal request history
POST/admin/partner-portal/wallet/fundSelf-service top-up: body { amount_naira, reference, note }. Credited immediately (trust). Idempotent on reference.
POST/admin/partner-portal/withdrawRequest withdrawal — body: { amount_naira, bank_code, account_number, account_name }
GET/admin/partner-portal/commissionsCommission earn history — paginated
GET/admin/partner-portal/customersCustomers enrolled by this partner — paginated

Bank Admin API

Banks that want to manage AtomCredit loans from their own systems: rather than the AtomOwn admin portal: use these endpoints. Auth is the same bearer-token model: sign in with the bank admin credentials issued at onboarding, then use the token on every call. All routes are under /api/v1/admin/bank/*.

The bank_id is resolved from the token: never passed as a parameter. Bank admin tokens carry only bank.* permissions; they cannot reach any developer-admin or customer routes.
POST/api/v1/auth/loginPublic

Exchange bank admin credentials for a bearer token. Do not pass a developer field: bank admins are not tenant-scoped.

request
curl -X POST "$API/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"emeka@firsttrustbank.ng","password":"BkA3F8c1!"}'
Response 200
json
{ "ok": true, "data": { "token": "ao_live_7e4a…", "user": { "id": 7, "type": "bank_admin",
  "name": "Emeka Obi", "email": "emeka@firsttrustbank.ng", "bank_id": 1 } } }

Parameters

GET/api/v1/admin/bank/parametersBank admin

List all parameter versions for your bank ordered by effective_from DESC. The version currently in effect is the most recent one where today falls within effective_from … effective_until. New credit requests always use the currently effective version. Existing facilities carry a frozen snapshot of the version that was active at approval — they are unaffected by later changes.

Response 200
json
{ "ok": true, "data": {
  "versions": [{
    "id": 2, "bank_id": 1, "version": 2,
    "effective_from": "2026-09-01", "effective_until": null,
    "interest_rate": "18.0000",
    "tenor_options_months": "6,12,24,36",
    "advance_cap": "30.0000",
    "default_fee_pct": "5.0000",
    "net_realisation_factor": "0.8500",
    "cure_period_days": 30,
    "resale_period_days": 90,
    "settlement_period_days": 14,
    "listing_price_floor_factor": "0.7500",
    "release_step_pct": "1.0000",
    "request_lock_expiry_hours": 168,
    "allocation_window_hours": 72,
    "realisation_cost_fixed": 0,
    "created_by": 5, "created_at": "2026-09-01T08:00:00"
  }]
} }
// tenor_options_months: comma-separated list of allowed tenor choices shown to customers
// advance_cap: max LTV % (e.g. "30.0000" = 30% of unit value)
// default_fee_pct: penalty % added to outstanding balance on enforcement notice
// net_realisation_factor: expected recovery fraction on forced sale (used in risk calc)
// release_step_pct: minimum % increment for progressive lock release after each repayment
POST/api/v1/admin/bank/parametersBank admin

Create a new parameter version. It takes effect from effective_from and supersedes previous versions for any new credit request submitted on or after that date. All fields are required.

BodyTypeDescription
effective_fromdaterequiredDate (YYYY-MM-DD) from which this version applies
effective_untildateoptionalExpiry date — leave null for open-ended
interest_ratedecimalrequiredAnnual interest rate % (e.g. "18.0000")
tenor_options_monthsstringrequiredComma-separated allowed tenor values shown to customers (e.g. "6,12,24,36")
advance_capdecimalrequiredMaximum LTV % — principal cannot exceed this fraction of unit value (e.g. "30.0000")
default_fee_pctdecimalrequiredPenalty % added to outstanding balance when enforcement notice is served
net_realisation_factordecimalrequiredAssumed recovery fraction on forced sale (e.g. "0.8500" = 85%)
cure_period_daysintegerrequiredDays the borrower has to cure arrears after default notice before escalation
resale_period_daysintegerrequiredMaximum days allowed to sell the collateral once enforcement notice is served
settlement_period_daysintegerrequiredDays to complete settlement after sale proceeds are received
listing_price_floor_factordecimalrequiredMinimum listing price as a fraction of last known unit value (e.g. "0.7500")
release_step_pctdecimalrequiredMinimum lock-release increment per repayment cycle (e.g. "1.0000" = 1%)
request_lock_expiry_hoursintegerrequiredHours before an un-acted-upon credit request expires (e.g. 168 = 7 days)
allocation_window_hoursintegerrequiredHours after disbursement that the developer has to allocate the unit (purchase finance)
realisation_cost_fixedintegerrequiredFixed cost (kobo) charged against sale proceeds for enforcement expenses. Use 0 if none.
request
curl -X POST "$API/api/v1/admin/bank/parameters" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "effective_from":"2026-10-01","interest_rate":"18.0000",
    "tenor_options_months":"6,12,24,36","advance_cap":"30.0000",
    "default_fee_pct":"5.0000","net_realisation_factor":"0.8500",
    "cure_period_days":30,"resale_period_days":90,"settlement_period_days":14,
    "listing_price_floor_factor":"0.7500","release_step_pct":"1.0000",
    "request_lock_expiry_hours":168,"allocation_window_hours":72,
    "realisation_cost_fixed":0
  }'
Response 201
json
{ "ok": true, "data": { "version": { "id": 3, "version": 3,
  "effective_from": "2026-10-01", "interest_rate": "18.0000",
  "tenor_options_months": "6,12,24,36", "advance_cap": "30.0000",
  "created_by": 5, "created_at": "2026-10-01T08:00:00" } } }
PATCH/api/v1/admin/bank/parameters/{id}Bank admin

Update a parameter version. effective_until can always be changed (use this to retire a version early). All other fields can only be changed while no facility has been approved against this version — once a facility references it the terms are frozen.

BodyTypeDescription
effective_untildateoptionalExpiry date — pass null to remove the expiry
interest_rate · tenor_options_months · advance_cap · …variousoptionalAny other parameter field — only accepted if the version has never been used by a facility (in_use: false)
Response 200
json
{ "ok": true, "data": { "version": { "id": 3, "interest_rate": "18.0000", … }, "in_use": false } }
// in_use: true means non-effective_until fields were ignored (version is locked)

Credit request queue

GET/api/v1/admin/bank/credit-requestsBank admin

Credit requests forwarded to this bank by developer admins. Filter by status to see requests awaiting decision (with_bank). Paginated. Use GET /admin/bank/credit-requests/{id} for full detail.

QueryTypeDescription
statusstringoptionalwith_bank (awaiting decision) · approved (deed pending signatures). Only these two values are accepted; other values are ignored.
product_typestringoptionalequity_release or purchase_finance
page / perintegeroptionalPagination (default page 1, per 20)
request
curl "$API/api/v1/admin/bank/credit-requests?status=with_bank" \
  -H "Authorization: Bearer $TOKEN"
Response 200
json
{ "ok": true, "data": {
  "credit_requests": [{
    "id": 7, "product_type": "equity_release", "status": "with_bank",
    "requested_principal": 500000.00, "tenor_months": 12,
    "customer": { "name": "Ada Okoro", "kyc_status": "approved" },
    "created_at": "2026-10-02T10:00:00"
  }],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }
GET/api/v1/admin/bank/credit-requests/{id}Bank admin

Full detail: customer KYC, live property holdings and portfolio value at current prices, requested terms, and the applicable parameter version.

Response 200
json
{ "ok": true, "data": {
  "credit_request": { "id": 7, "status": "with_bank", "product_type": "equity_release",
    "requested_principal": 500000.00, "tenor_months": 12, "interest_rate": 18.0 },
  "customer": { "name": "Ada Okoro", "email": "ada@example.com",
    "kyc_status": "approved", "dob": "1990-03-15" },
  "holdings": [
    { "property": "Lekki Gardens Phase 1", "unit_type": "plot",
      "packets": 5, "current_value": 1250000.00,
      "free_packets": 5, "locked_packets": 0 }
  ],
  "live_portfolio_value": 1250000.00,
  "max_eligible_principal": 375000.00
} }
POST/api/v1/admin/bank/credit-requests/{id}/approveBank admin

Approve the credit request and generate the offer deed. Status moves with_bank → approved. The deed is stored; both parties must sign before a facility is created. You may override the principal or tenor within your parameter limits.

BodyTypeDescription
approved_principalnumberoptionalOverride the approved principal (must be ≤ requested and within LTV)
approved_tenor_monthsintegeroptionalOverride the approved tenor
notestringoptionalApproval note shown to the customer
request
curl -X POST "$API/api/v1/admin/bank/credit-requests/7/approve" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"approved_principal":450000,"approved_tenor_months":12}'
Response 200
json
{ "ok": true, "data": {
  "credit_request": { "id": 7, "status": "approved",
    "approved_principal": 450000.00, "approved_tenor_months": 12,
    "decided_by_user_id": 5, "decided_at": "2026-10-03T11:15:00" }
} }
POST/api/v1/admin/bank/credit-requests/{id}/declineBank admin

Decline the credit request at the bank level. Status moves to declined; provisional ownership locks are released. The customer is notified with your reason.

BodyTypeDescription
reasonstringoptionalDecline reason shown to the customer and developer
Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "declined",
  "decided_by_user_id": 5, "decided_at": "2026-10-03T11:20:00",
  "decision_reason": "Insufficient equity coverage." } } }
POST/api/v1/admin/bank/credit-requests/{id}/request-infoBank admin

Request additional information from the developer or customer. Status moves with_bank → info_requested. The request returns to your queue once the developer re-confirms.

BodyTypeDescription
notestringrequiredSpecific information required (shown to developer and customer)
Response 200
json
{ "ok": true, "data": { "credit_request": { "id": 7, "status": "info_requested" } } }
POST/api/v1/admin/bank/credit-requests/{id}/signBank admin

Counter-sign the offer deed on behalf of the bank. Status moves to pending_signing and both the customer and developer admin are notified to sign. Once all three parties have signed, the facility is created automatically.

BodyTypeDescription
pinstringrequiredBank admin's 6-digit transaction PIN
Response 200
json
{ "ok": true, "data": {
  "credit_request": { "id": 7, "status": "pending_signing" },
  "message": "Bank signature recorded. Awaiting customer and developer signatures."
} }

Portfolio (facilities)

GET/api/v1/admin/bank/facilitiesBank admin

All AtomCredit facilities the bank has disbursed, across all statuses. Filter by status or product_type. Read a single facility with GET /admin/bank/facilities/{id}.

QueryTypeDescription
statusstringoptionalpending_disbursement · pending_allocation · active · in_arrears · enforcement · closed_repaid
product_typestringoptionalequity_release or purchase_finance
page / perintegeroptionalPagination
Response 200
json
{ "ok": true, "data": {
  "facilities": [{
    "id": 3, "product_type": "equity_release", "status": "active",
    "customer": { "id": 9, "name": "Ada Okoro" },
    "principal": 500000.00, "outstanding_principal": 450000.00,
    "interest_rate": 18.0, "tenor_months": 12, "repayment_day_of_month": 5,
    "monthly_installment": 45833.33, "loan_ends_at": "2027-09-15",
    "disbursed_at": "2026-09-15T09:00:00", "bank_reference": "BNK-2026-003"
  }],
  "pagination": { "page": 1, "per": 20, "total": 1, "pages": 1 }
} }
POST/api/v1/admin/bank/facilities/{id}/disburseBank admin

Record disbursement of the facility. Equity release: debits AtomCredit fee from bank wallet, disburses loan principal to customer wallet, facility → active. Purchase finance: debits loan fee + principal from bank wallet, auto-deducts customer contribution from customer wallet, executes the property purchase (investment, lock, inventory, platform fee, agent commission), facility → active. Everything happens atomically.

BodyTypeDescription
repayment_day_of_monthintegeroptionalDay of the month repayments fall due (1–28). Defaults to the calendar day of disbursement if omitted.
request
curl -X POST "$API/api/v1/admin/bank/facilities/3/disburse" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"repayment_day_of_month":5}'
Response 200
json
{ "ok": true, "data": {
  "facility": { "id": 3, "status": "active", "disbursed_at": "2026-09-15T09:00:00" },
  "atomcredit_fee": 4500.00, "fee_rate_pct": 1.0
} }
Error codeStatusMeaning
wrong_status409Facility is not in pending_disbursement state
insufficient_funds422Bank wallet balance is below the AtomCredit fee

Repayments & facility lifecycle

GET/api/v1/admin/bank/facilities/{id}/repaymentsBank admin

Retrieve the full repayment schedule for a facility, including each instalment's status, expected amount, and cleared date.

Response 200
json
{ "ok": true, "data": {
  "facility_id": 3, "principal": 450000.00,
  "repayments": [
    { "id": 11, "amount": 44250.00, "is_cleared": true,
      "cleared_at": "2026-10-04T14:00:00", "source": "manual",
      "bank_reference": "TRF-001", "idempotency_key": "REP-FAC3-OCT26",
      "required_lock_pct_after": 91.67, "released_pct": 8.33,
      "release_applied_at": "2026-10-04T14:01:00",
      "is_reversed": false, "reversed_at": null, "reversal_reason": null },
    { "id": 12, "amount": 44250.00, "is_cleared": false,
      "cleared_at": null, "source": "manual",
      "bank_reference": null, "idempotency_key": "REP-FAC3-NOV26",
      "required_lock_pct_after": 83.33, "released_pct": null,
      "release_applied_at": null, "is_reversed": false }
  ]
} }
POST/api/v1/admin/bank/facilities/{id}/repaymentsBank admin

Report that the customer has made a payment. Creates a repayment record (uncleared by default). Use the /clear action once you confirm the funds arrived, or pass is_cleared: true to mark it cleared immediately. idempotency_key prevents duplicate entries on retry.

BodyTypeDescription
amountnumberrequiredAmount received in naira
idempotency_keystringrequiredUnique key to prevent duplicate records on retry (e.g. "REP-FAC3-NOV26")
bank_referencestringoptionalYour internal payment / transfer reference
sourcestringoptionalPayment channel (e.g. "manual", "auto_debit")
is_clearedbooleanoptionalMark cleared immediately (default false)
Response 200
json
{ "ok": true, "data": { "repayment": {
  "id": 13, "amount": 44250.00, "is_cleared": false,
  "idempotency_key": "REP-FAC3-NOV26", "bank_reference": null, "source": "manual"
} } }
Error codeStatusMeaning
amount_exceeds_outstanding422Amount exceeds the outstanding principal balance
POST/api/v1/admin/bank/facilities/{fid}/repayments/{rid}/clearBank admin

Confirm (clear) a repayment. Marks it cleared and releases the proportional locked percentage from the customer's holdings. The release object shows what percentage was released and whether it was applied immediately or suspended.

Response 200
json
{ "ok": true, "data": {
  "repayment": { "id": 12, "is_cleared": true, "cleared_at": "2026-11-04T15:00:00" },
  "release": {
    "required_lock_pct_after": 75.0,
    "released_pct": 8.33,
    "applied": true,
    "suspended": false
  }
} }
POST/api/v1/admin/bank/facilities/{fid}/repayments/{rid}/reverseBank admin

Reverse a previously cleared repayment (e.g. a returned cheque). Re-locks the released percentage and returns the instalment to pending.

BodyTypeDescription
reasonstringrequiredReason for reversal (recorded in audit log and shown on the repayment record)
Response 200
json
{ "ok": true, "data": { "repayment": { "id": 12, "status": "pending", "reversed": true } } }
POST/api/v1/admin/bank/facilities/{id}/dischargeBank admin

Discharge a facility (customer has repaid in full). Releases 100% of locked holdings, moves status to closed_repaid, and generates a release deed documenting the discharge. The bank must explicitly call this endpoint; there is no automatic discharge. Always call this after confirming full repayment.

BodyTypeDescription
notestringoptionalDischarge note (audit log)
Response 200
json
{ "ok": true, "data": {
  "facility": { "id": 3, "status": "closed_repaid" },
  "lock_state": "released",
  "release_deed": {
    "doc_version_id": 2, "doc_ref": "deed-release-fac3",
    "content_hash": "abc123…", "document_path": "deeds/release/fac3.html"
  }
} }
POST/api/v1/admin/bank/facilities/{id}/mark-arrearsBank admin

Mark a facility as in arrears after missed or partial payments. Status moves active → in_arrears. Sends the customer an arrears notification.

BodyTypeDescription
notestringoptionalArrears note shown to the customer
Response 200
json
{ "ok": true, "data": { "facility": { "id": 3, "status": "in_arrears" } } }
POST/api/v1/admin/bank/facilities/{id}/clear-arrearsBank admin

Restore an in-arrears facility to active after the customer has caught up on missed payments.

Response 200
json
{ "ok": true, "data": { "facility": { "id": 3, "status": "active" } } }
POST/api/v1/admin/bank/facilities/{id}/serve-default-noticeBank admin

Open an enforcement case for a facility in active or in_arrears status. Creates an enforcement_case record in notice_served state. The customer receives a default notice notification.

BodyTypeDescription
notestringoptionalNotice text visible to the customer
Response 200
json
{ "ok": true, "data": {
  "facility": { "id": 3 },
  "enforcement_case": { "id": 1, "status": "notice_served" }
} }

Enforcement cases

GET/api/v1/admin/bank/enforcement-casesBank admin

All enforcement cases for the bank's facilities. Filter by status and/or facility_id. Read a single case with GET /admin/bank/enforcement-cases/{id}.

QueryTypeDescription
statusstringoptionalnotice_served · cure_running · enforcement_notice · listed · offer_accepted · settled · allocated · closed
facility_idintegeroptionalFilter cases for a specific facility (used on the facility detail page)
Response 200
json
{ "ok": true, "data": {
  "enforcement_cases": [{
    "id": 1, "facility_id": 3, "status": "cure_running",
    "customer": { "name": "Ada Okoro" },
    "outstanding_at_notice": 88500.00, "default_fee": 4425.00,
    "enforcement_portion_pct": 80.0, "opened_at": "2026-10-01T08:00:00",
    "cure_period_ends_at": "2026-10-22T08:00:00"
  }],
  "count": 1
} }
POST/api/v1/admin/bank/enforcement-cases/{id}/start-cureBank admin

Give the customer a cure period to repay the arrears. Status moves notice_served → cure_running. The cure_period_ends_at is calculated as cure_days from today (default 21 days).

BodyTypeDescription
cure_daysintegeroptionalNumber of days for the cure window (default 21)
notestringoptionalMessage to the customer about the cure requirements
Response 200
json
{ "ok": true, "data": {
  "enforcement_case": { "id": 1, "status": "cure_running",
    "cure_period_ends_at": "2026-10-22T08:00:00" }
} }
POST/api/v1/admin/bank/enforcement-cases/{id}/cure-paidBank admin

Record that the customer paid the arrears. Closes the enforcement case (settled), moves the facility back to active, and applies a consolidated lock release for all cleared repayments since the case was opened. Can be called from either notice_served or cure_running status.

Response 200
json
{ "ok": true, "data": {
  "enforcement_case": { "id": 1, "status": "settled" },
  "facility": { "id": 3, "status": "active" }
} }
POST/api/v1/admin/bank/enforcement-cases/{id}/serve-enforcement-noticeBank admin

Escalate from cure period to formal enforcement. Status moves → enforcement_notice. The customer receives a formal notice; the bank may then list the collateral for sale.

BodyTypeDescription
notestringoptionalNotice text (shown to customer and recorded in audit log)
Response 200
json
{ "ok": true, "data": { "enforcement_case": { "id": 1, "status": "enforcement_notice" } } }
POST/api/v1/admin/bank/enforcement-cases/{id}/disputeBank admin

Record a customer-raised dispute against an enforcement case. The dispute is appended to the audit log as a case.disputed event. The enforcement case status is not changed — the bank must then review and decide how to proceed.

BodyTypeDescription
notestringrequiredNature of the customer's dispute (recorded verbatim in the audit log)
Response 200
json
{ "ok": true, "data": { "enforcement_case": { "id": 1, "status": "enforcement_notice" },
  "dispute_recorded": true } }

Wallet

GET/api/v1/admin/bank/walletBank admin

Read-only view of the bank's AtomCredit wallet. The AtomCredit platform fee is debited from here on each disbursement.

Response 200
json
{ "ok": true, "data": {
  "wallet": { "balance": 4800000.00, "total_funded": 5000000.00, "total_withdrawn": 200000.00 },
  "statement": [
    { "id": 99,  "type": "credit", "amount": 5000000.00,
      "description": "Initial wallet credit", "created_at": "2026-09-01 09:00:00" },
    { "id": 101, "type": "debit",  "amount": 4500.00,
      "description": "AtomCredit fee: facility #3", "created_at": "2026-09-15 09:30:00" }
  ]
} }

Reports

GET/api/v1/admin/bank/reports/portfolioBank admin

Portfolio summary: total active facilities by product type, outstanding balance, collected repayments, and AtomCredit fees paid to date.

Response 200
json
{ "ok": true, "data": {
  "active_facilities": 12,
  "total_outstanding": 5400000.00,
  "equity_release": { "count": 8, "outstanding": 3600000.00 },
  "purchase_finance": { "count": 4, "outstanding": 1800000.00 },
  "total_repayments_cleared": 1200000.00,
  "total_atomcredit_fees": 54000.00
} }
GET/api/v1/admin/bank/reports/arrearsBank admin

All facilities currently in in_arrears or enforcement status, with the arrears amount and number of missed payments.

Response 200
json
{ "ok": true, "data": {
  "arrears": [{
    "facility_id": 3, "customer_name": "Ada Okoro", "status": "in_arrears",
    "principal": 450000.00, "arrears_amount": 88500.00,
    "missed_payments": 2, "last_cleared_at": "2026-09-04T14:00:00"
  }]
} }
GET/api/v1/admin/bank/reports/enforcementBank admin

All open enforcement cases with their current status, cure deadlines, and collateral summary.

Response 200
json
{ "ok": true, "data": {
  "enforcement_cases": [{
    "id": 1, "facility_id": 3, "status": "cure_running",
    "customer_name": "Ada Okoro", "outstanding_at_notice": 88500.00,
    "cure_period_ends_at": "2026-10-22T08:00:00",
    "enforcement_portion_pct": 80.0
  }]
} }
GET/api/v1/admin/bank/reports/realisationBank admin

Collateral realisation history: facilities resolved through enforcement, with proceeds recovered and any residual balance.

Response 200
json
{ "ok": true, "data": {
  "realisations": [{
    "enforcement_case_id": 1, "facility_id": 3, "customer_name": "Ada Okoro",
    "outstanding_at_enforcement": 350000.00,
    "proceeds_recovered": 380000.00, "residual": 30000.00,
    "resolved_at": "2026-11-10T10:00:00"
  }]
} }

Team (staff)

Requires bank.staff.manage permission (assigned to the bank_admin role by default). All operations are scoped to the authenticated bank — the bank_id is resolved from the token.

GET/api/v1/admin/bank/staffBank admin · bank.staff.manage

List all staff members for this bank. Paginated (?page/?per). Returns name, email, phone, type, role, status.

Response 200
json
{ "ok": true, "data": {
  "count": 2,
  "pagination": { "page": 1, "per": 25, "total": 2, "pages": 1 },
  "team": [
    { "id": 12, "name": "Zainab Musa", "email": "z.musa@bank.ng", "phone": null,
      "type": "staff", "status": "active", "role": "Loan Officer", "role_id": 8,
      "created_at": "2026-10-10T09:00:00" }
  ]
} }
POST/api/v1/admin/bank/staffBank admin · bank.staff.manage

Create a bank staff member. A temporary password and PIN are generated and returned once — share them with the new member. An optional role_id assigns a role; if omitted the system staff role is used.

Body fieldTypeDescription
namestringrequiredFull name
emailstringrequiredLogin email — must be unique within this bank
phonestringoptionalPhone number
role_idintegeroptionalID from GET /admin/bank/roles; falls back to system staff role
Response 201
json
{ "ok": true, "data": {
  "user": { "id": 12, "name": "Zainab Musa", "email": "z.musa@bank.ng", "type": "staff" },
  "temp_password": "4f8c2b1d!47",
  "temp_pin": "083142"
} }
PATCH/api/v1/admin/bank/staff/{id}Bank admin · bank.staff.manage

Activate, deactivate, or suspend a staff member. Body: { "status": "active" | "inactive" | "suspended" }.

PATCH/api/v1/admin/bank/staff/{id}/roleBank admin · bank.staff.manage

Reassign a staff member's role. Body: { "role_id": 8 }. The role must belong to this bank or be a system role.

Roles & permissions

System roles are built-in. Create custom bank roles to grant specific bank.* permissions to your staff.

GET/api/v1/admin/bank/rolesBank admin · bank.staff.manage

List system roles (relevant to bank staff) plus any custom roles created for this bank. Each role includes its assigned permission keys.

Response 200
json
{ "ok": true, "data": { "roles": [
  { "id": 2, "key": "staff", "name": "Staff", "is_system": true,
    "permissions": [] },
  { "id": 9, "key": "bank_1_loan_officer", "name": "Loan Officer",
    "is_system": false, "permissions": ["bank.loans.view","bank.loans.manage"] }
] } }
GET/api/v1/admin/bank/permissionsBank admin · bank.staff.manage

List all available bank.* permissions that can be assigned to custom roles.

Response 200
json
{ "ok": true, "data": { "permissions": [
  { "id": 18, "key": "bank.loans.view",      "description": "View loan applications" },
  { "id": 19, "key": "bank.loans.manage",    "description": "Review, approve and decline loans" },
  { "id": 20, "key": "bank.wallet.view",     "description": "View bank wallet balance and history" },
  { "id": 21, "key": "bank.wallet.move",     "description": "Fund and withdraw from bank wallet" },
  { "id": 22, "key": "bank.settings.manage", "description": "Manage bank LTV ratio and interest rate" },
  { "id": 23, "key": "bank.staff.manage",    "description": "Manage bank staff and roles" }
] } }
POST/api/v1/admin/bank/rolesBank admin · bank.staff.manage

Create a custom bank role. Body: { "name": "Loan Officer", "permission_ids": [18, 19] }. Only bank.* permissions are accepted.

Response 201
json
{ "ok": true, "data": { "id": 9, "key": "bank_1_loan_officer", "name": "Loan Officer" } }
PATCH/api/v1/admin/bank/roles/{id}Bank admin · bank.staff.manage

Rename a custom role and/or replace its permission set. Body: { "name": "Senior Loan Officer", "permission_ids": [18, 19, 20] }. System roles cannot be edited.

DELETE/api/v1/admin/bank/roles/{id}Bank admin · bank.staff.manage

Delete a custom bank role. Fails with 409 has_members if any staff are still assigned to it — reassign them first.

POST/api/v1/admin/bank/roles/{id}/assignBank admin · bank.staff.manage

Assign a staff member to a role (replaces any existing role). Body: { "staff_id": 12 }.