Introduction
GP CoreBanking is a mock core banking system that GP products develop and test against. Its API covers onboarding Clients, opening Accounts, moving money and issuing Loans. It is never used for real banking.
Key Features
- Client Management - Create and look up Retail and Corporate Clients
- Account Management - Savings, Checking, Current and Loan Accounts, each with a single running balance
- Moving Money - Deposits, Withdrawals and Transfers, each applied atomically in the database
- Loans - Issue and list Loans against Loan Accounts
- API Keys - One key per Machine client, issued and revoked by an Admin
Base URL
All API requests should be prefixed with the following base URL:
https://bkclatwirugxogrfbhno.supabase.co/functions/v1/corebanking
Every response is JSON. Successful responses are {"status": "success", "data": ...};
creates return 201, everything else 200.
Health Check
The base URL itself is a public health check (no credentials needed):
GET /
Example Response:
{
"status": "online",
"message": "Welcome to the GP Core Banking API",
"version": "1.0.0",
"timestamp": "2026-10-22T09:30:00.000Z"
} timestamp is the server's current time.
Implementation Notes
- The API is a Supabase Edge Function over a Postgres database
- Each Account has a single running balance; Deposits, Withdrawals and Transfers change it and record Transactions
- The System accounts (
TELLER-ACCOUNTandVAULT-ACCOUNT) must already exist; without them Deposits and Withdrawals fail with422 SYSTEM_ACCOUNT_MISSING - Account numbers are 10 random digits unless you supply your own
Authentication
Every endpoint except the health check needs credentials: an API key or a staff login token. Without valid
credentials you get 401 UNAUTHORIZED.
Option 1: API Key (Machine clients)
Send your key in the x-api-key header:
curl "https://bkclatwirugxogrfbhno.supabase.co/functions/v1/corebanking/clients" \
-H "x-api-key: sk_live_your-api-key" - An Admin issues one key per Machine client from the API keys page of the web app (or
POST /api_keys). - The key is shown once, when it is issued. Copy it then: only its hash is stored, so it cannot be shown again.
- An Admin can revoke a key; it fails with
401from the next request on. Lost a key? Ask for it to be revoked and a new one issued. - An API key can call every endpoint except the Admin-only
/api_keysendpoints. - The Supabase
apikeyheader is ignored; onlyx-api-keycounts.
Option 2: Staff login token (the web app)
Send a Staff user's Supabase Auth access token as a bearer token:
curl "https://bkclatwirugxogrfbhno.supabase.co/functions/v1/corebanking/clients" \
-H "Authorization: Bearer your-staff-access-token"
A Staff user is a Teller or an Admin. Both can call every endpoint; only an Admin can call /api_keys.
If both headers are sent, x-api-key is used.
API Endpoints
Clients
GET /clients
List Clients.
Query Parameters:
client_type(optional) - Exact match: "Retail" or "Corporate"ussd_phone_number(optional) - Partial match on the USSD phone number
View Example Response (200)
{
"status": "success",
"data": [
{
"client_id": "a0000000-0000-0000-0000-000000000002",
"client_type": "Retail",
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"registration_number": null,
"contact_number": "233241000001",
"email": "kofi.mensah@example.com",
"address": "Osu, Accra",
"ussd_phone_number": "233241000001",
"ussd_status": "active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00"
}
]
} GET /clients/:id
Get one Client.
Path Parameters:
id- The client_id (UUID)
- Unknown id: 404 NOT_FOUND. Malformed id: 422 VALIDATION_FAILED.
View Example Response (200)
{
"status": "success",
"data": {
"client_id": "a0000000-0000-0000-0000-000000000002",
"client_type": "Retail",
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"registration_number": null,
"contact_number": "233241000001",
"email": "kofi.mensah@example.com",
"address": "Osu, Accra",
"ussd_phone_number": "233241000001",
"ussd_status": "active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00"
}
} GET /clients/ussd/search
Find the Client registered for a USSD phone number.
Query Parameters:
ussd_phone(required) - Exact match on the USSD phone number
- Missing ussd_phone: 422 VALIDATION_FAILED. No match: 404 NOT_FOUND.
View Example Response (200)
{
"status": "success",
"data": {
"client_id": "a0000000-0000-0000-0000-000000000002",
"client_type": "Retail",
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"registration_number": null,
"contact_number": "233241000001",
"email": "kofi.mensah@example.com",
"address": "Osu, Accra",
"ussd_phone_number": "233241000001",
"ussd_status": "active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00"
}
} POST /clients
Create a Client.
Request Body (JSON):
client_type(required) - "Retail" or "Corporate"first_name, last_name(Retail only) - Required for Retail, must be absent for Corporatecompany_name, registration_number(Corporate only) - Required for Corporate, must be absent for Retailcontact_number(required) - Digits only (no + or spaces), at least 5email(required) - A valid email addressaddress(required) - Non-emptyussd_phone_number(optional) - Digits only
{
"client_type": "Retail",
"first_name": "Ama",
"last_name": "Boateng",
"contact_number": "233241000009",
"email": "ama.boateng@example.com",
"address": "Labone, Accra",
"ussd_phone_number": "233241000009"
} - Breaking the Retail/Corporate field rules or leaving out address: 422 VALIDATION_FAILED, with errors[] naming each field.
- Other fields (such as ussd_status) are ignored.
View Example Response (201)
{
"status": "success",
"data": {
"client_id": "a0000000-0000-0000-0000-000000000009",
"client_type": "Retail",
"first_name": "Ama",
"last_name": "Boateng",
"company_name": null,
"registration_number": null,
"contact_number": "233241000009",
"email": "ama.boateng@example.com",
"address": "Labone, Accra",
"ussd_phone_number": "233241000009",
"ussd_status": null,
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00"
}
} Accounts
GET /accounts
List Accounts, including the System accounts, with the owning Client's name.
Query Parameters:
client_id(optional) - Filter by Clientaccount_type(optional) - "Savings", "Checking", "Current" or "Loan"status(optional) - "Active", "Inactive" or "Frozen"currency(optional) - Currency code, e.g. "GHS"
View Example Response (200)
{
"status": "success",
"data": [
{
"account_id": "b0000000-0000-0000-0000-000000000003",
"account_number": "1000000001",
"client_id": "a0000000-0000-0000-0000-000000000002",
"account_type": "Savings",
"currency": "GHS",
"balance": 2500,
"status": "Active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00",
"clients": {
"first_name": "Kofi",
"last_name": "Mensah"
}
}
]
} GET /accounts/:id
Get one Account, with its Client's details. Use this to read a balance.
Path Parameters:
id- The account_id (UUID), not the account number
- Unknown id: 404 NOT_FOUND. Malformed id: 422 VALIDATION_FAILED.
View Example Response (200)
{
"status": "success",
"data": {
"account_id": "b0000000-0000-0000-0000-000000000003",
"account_number": "1000000001",
"client_id": "a0000000-0000-0000-0000-000000000002",
"account_type": "Savings",
"currency": "GHS",
"balance": 2500,
"status": "Active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00",
"clients": {
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"client_type": "Retail",
"email": "kofi.mensah@example.com",
"contact_number": "233241000001",
"address": "Osu, Accra"
}
}
} POST /accounts
Open an Account for a Client.
Request Body (JSON):
client_id(required) - UUID of an existing Clientaccount_type(required) - "Savings", "Checking", "Current" or "Loan"currency(optional) - 3-letter code, default "GHS"account_number(optional) - Digits only, at least 10. If omitted, a random 10-digit number is generatedbalance(optional) - Opening balance, not negative, default 0
{
"client_id": "a0000000-0000-0000-0000-000000000002",
"account_type": "Savings",
"currency": "GHS"
} - Unknown client_id: 422 VALIDATION_FAILED. An account_number that is already taken: 409 CONFLICT.
- New Accounts start Active. Other fields (such as status) are ignored.
View Example Response (201)
{
"status": "success",
"data": {
"account_id": "b0000000-0000-0000-0000-000000000009",
"account_number": "4821937605",
"client_id": "a0000000-0000-0000-0000-000000000002",
"account_type": "Savings",
"currency": "GHS",
"balance": 0,
"status": "Active",
"created_at": "2026-10-01T08:00:00+00:00",
"updated_at": "2026-10-01T08:00:00+00:00"
}
} Transactions
GET /transactions
List Transactions, newest first.
Query Parameters:
account_id(optional) - Filter by Accountclient_id(optional) - Filter by the Account's Clienttransaction_type(optional) - "Deposit", "Withdrawal", "Transfer" or "Loan Payment"limit(optional) - Maximum rows, default 50
View Example Response (200)
{
"status": "success",
"data": [
{
"transaction_id": "e0000000-0000-0000-0000-000000000001",
"account_id": "b0000000-0000-0000-0000-000000000003",
"transaction_type": "Deposit",
"amount": 100,
"currency": "GHS",
"transaction_date": "2026-10-22T09:30:00+00:00",
"description": "Cash deposit",
"reference": "DEP-1761125400000",
"status": "Completed",
"accounts": {
"client_id": "a0000000-0000-0000-0000-000000000002"
}
}
]
} POST /transactions
Make a Deposit or Withdrawal on an Account.
Request Body (JSON):
account_id(required) - UUID of the Accounttransaction_type(required) - "Deposit" or "Withdrawal"amount(required) - Number greater than 0currency(required) - The Account's currency (3-letter code, case-insensitive)description(optional) - Up to 255 charactersreference(optional) - Letters, digits, - and _ only. Default DEP-<ms> or WDR-<ms>
{
"account_id": "b0000000-0000-0000-0000-000000000003",
"transaction_type": "Deposit",
"amount": 100,
"currency": "GHS",
"description": "Cash deposit",
"reference": "DEP-0001"
} - Withdrawal over the balance: 422 INSUFFICIENT_FUNDS, and the balance is unchanged.
- A currency other than the Account's: 422 CURRENCY_MISMATCH, and the balance is unchanged.
- Unknown Account: 404 NOT_FOUND. Missing System account: 422 SYSTEM_ACCOUNT_MISSING.
View Example Response (201)
{
"status": "success",
"data": {
"transaction_id": "e0000000-0000-0000-0000-000000000001",
"account_id": "b0000000-0000-0000-0000-000000000003",
"amount": 100,
"new_balance": 2600,
"reference": "DEP-0001"
}
} POST /transfers
Transfer money between two Accounts in the same currency.
Request Body (JSON):
from_account_id(required) - UUID of the source Accountto_account_id(required) - UUID of the destination Account, not the sourceamount(required) - Number greater than 0currency(required) - Must match both Accounts' currencydescription(optional) - Free text
{
"from_account_id": "b0000000-0000-0000-0000-000000000003",
"to_account_id": "b0000000-0000-0000-0000-000000000004",
"amount": 25,
"currency": "GHS",
"description": "Rent"
} - The response describes the debit Transaction on the source Account (negative amount). The credit on the destination has reference <reference>-CREDIT.
- Errors: 422 SAME_ACCOUNT, 422 CURRENCY_MISMATCH, 422 INSUFFICIENT_FUNDS, 404 NOT_FOUND.
View Example Response (201)
{
"status": "success",
"data": {
"transaction_id": "e0000000-0000-0000-0000-000000000002",
"account_id": "b0000000-0000-0000-0000-000000000003",
"amount": -25,
"reference": "TXF-1761125400000"
}
} Loans
GET /loans
List Loans with their Account and Client.
View Example Response (200)
{
"status": "success",
"data": [
{
"loan_id": "c0000000-0000-0000-0000-000000000001",
"account_id": "b0000000-0000-0000-0000-000000000006",
"loan_amount": 10000,
"interest_rate": 24,
"start_date": "2026-10-01",
"end_date": "2027-10-01",
"repayment_frequency": "Monthly",
"outstanding_balance": 10000,
"status": "Active",
"loan_type": "STANDARD",
"accounts": {
"account_number": "1000000004",
"balance": 0,
"clients": {
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"client_type": "Retail"
}
}
}
]
} GET /loans/:id
Get one Loan with its Account and Client.
Path Parameters:
id- The loan_id (UUID)
- Unknown id: 404 NOT_FOUND. Malformed id: 422 VALIDATION_FAILED.
View Example Response (200)
{
"status": "success",
"data": {
"loan_id": "c0000000-0000-0000-0000-000000000001",
"account_id": "b0000000-0000-0000-0000-000000000006",
"loan_amount": 10000,
"interest_rate": 24,
"start_date": "2026-10-01",
"end_date": "2027-10-01",
"repayment_frequency": "Monthly",
"outstanding_balance": 10000,
"status": "Active",
"loan_type": "STANDARD",
"accounts": {
"account_number": "1000000004",
"balance": 0,
"clients": {
"first_name": "Kofi",
"last_name": "Mensah",
"company_name": null,
"client_type": "Retail"
}
}
}
} POST /loans
Issue a Loan against an Account (normally a Loan-type Account; not enforced). Records the Loan only; no money moves.
Request Body (JSON):
account_id(required) - UUID of the Accountloan_amount(required) - Number greater than 0interest_rate(required) - Annual rate in percent: 24 means 24%. At least 0 and below 100start_date, end_date(required) - Dates (YYYY-MM-DD); end_date must be after start_dateoutstanding_balance(required) - Number, not negativerepayment_frequency(optional) - "Daily", "Weekly", "Monthly", "Quarterly" or "Yearly"; default "Monthly"loan_type(optional) - "STANDARD" or "BCB_TRADE_LOAN"; default "STANDARD"
{
"account_id": "b0000000-0000-0000-0000-000000000006",
"loan_amount": 10000,
"interest_rate": 24,
"start_date": "2026-10-01",
"end_date": "2027-10-01",
"repayment_frequency": "Monthly",
"outstanding_balance": 10000
} - Invalid dates or rate: 422 VALIDATION_FAILED, with errors[] naming the field. Unknown account_id: 422 VALIDATION_FAILED.
View Example Response (201)
{
"status": "success",
"data": {
"loan_id": "c0000000-0000-0000-0000-000000000001",
"account_id": "b0000000-0000-0000-0000-000000000006",
"loan_amount": 10000,
"interest_rate": 24,
"start_date": "2026-10-01",
"end_date": "2027-10-01",
"repayment_frequency": "Monthly",
"outstanding_balance": 10000,
"status": "Active",
"loan_type": "STANDARD"
}
} API keys (Admin only)
Only an Admin's staff login can call these. A Teller or an API key gets 403 FORBIDDEN.
GET /api_keys
List API keys, newest first. Metadata only: no endpoint ever returns a stored key.
View Example Response (200)
{
"status": "success",
"data": [
{
"id": "d0000000-0000-0000-0000-000000000001",
"name": "USSD Designer",
"key_prefix": "sk_live_3f9a",
"status": "active",
"created_at": "2026-10-22T09:30:00+00:00",
"created_by": "22222222-2222-2222-2222-222222222222"
}
]
} POST /api_keys
Issue an API key for a Machine client.
Request Body (JSON):
name(required) - The Machine client (product) the key is for
{
"name": "USSD Designer"
} - key is in this response only. Copy it now: only its hash is stored.
- Missing or blank name: 422 VALIDATION_FAILED.
View Example Response (201)
{
"status": "success",
"data": {
"id": "d0000000-0000-0000-0000-000000000001",
"name": "USSD Designer",
"key_prefix": "sk_live_3f9a",
"status": "active",
"created_at": "2026-10-22T09:30:00+00:00",
"created_by": "22222222-2222-2222-2222-222222222222",
"key": "sk_live_3f9a…(48 hex characters in all)"
}
} PUT /api_keys/:id/revoke
Revoke an API key. It fails with 401 from the next request on.
Path Parameters:
id- The key's id (UUID)
- Unknown id: 404 NOT_FOUND. Malformed id: 422 VALIDATION_FAILED.
View Example Response (200)
{
"status": "success",
"data": {
"id": "d0000000-0000-0000-0000-000000000001",
"name": "USSD Designer",
"key_prefix": "sk_live_3f9a",
"status": "revoked",
"created_at": "2026-10-22T09:30:00+00:00",
"created_by": "22222222-2222-2222-2222-222222222222"
}
} How Money Moves
Each Account has one running balance. Every money movement runs in a single database transaction: the balances change and the Transactions are recorded together, or nothing changes.
System Accounts
Two Accounts owned by the bank, which cash passes through:
- Teller account (
TELLER-ACCOUNT) - the teller's till - Vault account (
VAULT-ACCOUNT) - the bank's cash reserve
Deposit
- Refused with
422 CURRENCY_MISMATCHif not in the Account's currency - The Client's Account balance increases; a
DepositTransaction is recorded - Three
TransferTransactions record the cash going in to the Teller account and on to the Vault account (references ending-TELLER-IN,-TELLER-OUT,-VAULT-IN)
Withdrawal
- Refused with
422 INSUFFICIENT_FUNDSif the amount is more than the balance; with422 CURRENCY_MISMATCHif not in the Account's currency - The Client's Account balance decreases; a
WithdrawalTransaction is recorded - Three
TransferTransactions record the cash going from the Vault account through the Teller account (references ending-VAULT-OUT,-TELLER-IN,-TELLER-OUT)
The System account Transactions are records only: they do not change the System accounts' balances.
Transfer
- Refused if the Accounts are the same, their currencies differ, or the source balance is too low
- The source balance decreases and the destination balance increases
- Two
TransferTransactions are recorded: a negative one on the source, a positive one on the destination
Error Handling
Error Shape
Every error is JSON with the same shape. Branch on code, which is stable; message
is for people and may change.
{
"status": "error",
"code": "INSUFFICIENT_FUNDS",
"message": "Insufficient funds"
} A VALIDATION_FAILED for a request body also lists each problem:
{
"status": "error",
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"errors": [
{
"path": "amount",
"message": "Number must be greater than 0"
}
]
} Status Codes
| Status | Code | When |
|---|---|---|
401 | UNAUTHORIZED | No credentials, or an invalid or revoked API key, or an invalid staff token |
403 | FORBIDDEN | Authenticated but not allowed, e.g. a Teller or an API key calling /api_keys |
404 | NOT_FOUND | Unknown route, or no Client, Account, Loan or API key with that id |
409 | CONFLICT | A value that must be unique is already taken, e.g. an account_number |
422 | VALIDATION_FAILED | Invalid body, malformed JSON, malformed id, a missing required parameter, or a reference (such as client_id) to a record that does not exist. The message names the field |
422 | INSUFFICIENT_FUNDS | A Withdrawal or Transfer is more than the Account's balance |
422 | CURRENCY_MISMATCH | A Deposit or Withdrawal not in the Account's currency, or a Transfer between Accounts of different currencies |
422 | SAME_ACCOUNT | A Transfer from an Account to itself |
422 | NON_POSITIVE_AMOUNT | An amount of zero or less reached the money functions |
422 | SYSTEM_ACCOUNT_MISSING | The TELLER-ACCOUNT or VAULT-ACCOUNT System account does not exist |
500 | INTERNAL_ERROR | Anything unexpected (a fault on our side, not your request). The message is always "Internal server error"; the detail is only in the server log |
Data Types
Client Types
Retail: A personCorporate: A company
USSD Status
active: USSD banking enabledinactive: USSD banking disabled
Account Types
SavingsCheckingCurrentLoan: Holds a Loan
Account Status
Active: Default for new AccountsInactiveFrozen
Transaction Types
DepositWithdrawalTransfer: One leg of a Transfer, or a System account movementLoan Payment: Exists in the schema; the API does not create it
Transaction Status
Completed: Every Transaction the API createsPendingFailed
Loan Status
Active: DefaultPaid OffDefaulted
API Key Status
activerevoked: Fails authentication