Skip to main content

Core Concepts

This page explains how money moves through PayKore. Read it before building anything non-trivial.


The Ledger

PayKore uses double-entry bookkeeping. Every movement of money produces at least two ledger entries that net to zero. No money is ever created or destroyed — it moves between accounts.

A ₦5,000 P2P transfer produces three ledger entries:

#AccountTypeAmount (kobo)
1wlt_9f3kA2mXpQ (sender)DEBIT-502500
2wlt_3pRsT7uVwX (recipient)CREDIT+500000
3wallet_platform (PayKore fee)CREDIT+2500

The three entries sum to zero. The sender is debited the transfer amount plus the fee (502500 kobo / ₦5,025). The recipient receives the full 500000 kobo (₦5,000). PayKore collects 2500 kobo (₦25) in platform fee.

Wallet balances are computed, not stored. The balance you see in GET /v1/wallets/:id is the sum of all ledger entries for that wallet. The balance_after field on each ledger entry is a cached snapshot for performance — it is always consistent with the true sum.


Wallets

A wallet is a ₦-denominated account scoped to one of your users. Each wallet has a real Nigerian bank account number (NUBAN) issued by PayKore's MFB partner, which means external banks can send money directly to it.

Your Partner Account
└── Wallet (wlt_9f3kA2mXpQ) — userRef: "user_123"
├── LedgerEntry: CREDIT +1000000 kobo (funding)
├── LedgerEntry: DEBIT -502500 kobo (P2P transfer)
└── balance: 497500 kobo (₦4,975)

Wallet status lifecycle

StatusCan debitCan creditNotes
PENDINGNoNoMFB account being provisioned (usually <5s)
ACTIVEYesYesNormal operating state
FROZENNoYesCan still receive funds; outbound blocked
CLOSEDNoNoTerminal state. Create a new wallet if needed.
warning

A FROZEN wallet can receive credits but cannot send. This is intentional — it allows you to freeze a user's spending while still accepting incoming payments.


Transactions vs Ledger Entries

These are distinct objects. Understanding the difference prevents confusion when reading API responses.

TransactionLedger Entry
What it isThe business eventThe accounting record
Example"User A sent ₦5,000 to User B""Debit wlt_A by 502500 kobo"
ID prefixtx_le_
Count per event12 or more
HoldsFee breakdown, PSP reference, statusAmount, account, direction, balance_after

One transaction always produces one or more ledger entry pairs. The transaction is what you show to your user in their activity feed. The ledger entries are your financial audit trail.


Transaction Lifecycle

StatusMeaning
pendingRequest received; funds not yet reserved
processingFunds reserved; request sent to MFB/PSP
completedPSP confirmed. Funds are settled. This is the only state where recipients can consider funds available.
failedPSP rejected the transfer. Funds are returned to source wallet.
reversedA completed transaction was reversed (rare; requires PayKore support).
expiredRequest timed out before processing (USSD sessions only).
note

For P2P transfers (wallet to wallet), the transition from pending to completed happens within milliseconds — no PSP round trip. The processing state is only meaningful for bank transfers and USSD/QR payments.


Fees

Every transaction type has a fee. Fees are always charged to the initiating party (the sender or the merchant's platform).

Transaction typeFeeCap
Wallet funding (inbound)Free
P2P Transfer0.5% of amount₦200 (20000 kobo)
Bank Transfer (outbound NIP)₦100 flat
USSD Payment1.5% of amount₦500 (50000 kobo)
QR Payment1.5% of amount₦500 (50000 kobo)
Split Payment2.0% of amount₦1,000 (100000 kobo)
Wallet Maintenance₦200/month per wallet

Every transaction response includes a fee_breakdown object:

{
"fee_breakdown": {
"customer_fee_kobo": 2500,
"platform_fee_kobo": 2500,
"mfb_cost_kobo": 0,
"net_amount_kobo": 500000
}
}
  • customer_fee_kobo — what the payer is charged above the transfer amount
  • platform_fee_kobo — PayKore's net revenue (credited to the platform wallet)
  • mfb_cost_kobo — what PayKore pays the MFB (for bank transfers and USSD)
  • net_amount_kobo — what the recipient receives
note

Fee rates may differ based on your partner tier (Starter, Growth, Scale). Check your dashboard for your current rates.


Idempotency

Network failures happen. If your server times out while sending a transfer request, you don't know if PayKore received it. Retrying without an idempotency key will create a duplicate transaction.

Every state-changing request (POST, DELETE) should include an Idempotency-Key header. PayKore returns the same response for any request with the same key, within 24 hours.

// TypeScript — generate a safe idempotency key
const idempotencyKey = crypto.randomUUID();
// e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479"
// Go — generate a safe idempotency key
import "github.com/google/uuid"
idempotencyKey := uuid.New().String()
# Pass it as a header
curl -X POST https://api.paykore.dev/v1/transfers/p2p \
-H "Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479" \
...
tip

Use a UUID generated at the start of the user's action (e.g. when they tap "Send"). If the request fails, retry with the same key. If it succeeds, generate a new key for the next distinct action.


Environments

SandboxLive
API key prefixsk_test_sk_live_
MoneyTest money onlyReal NGN
Bank transfersComplete instantly with fake PSP refReal NIP transfer (seconds to minutes)
KYCReturns verified for any BVN/NINReal identity check via Mono/Smile
WebhooksFire normallyFire normally

Sandbox is fully deterministic. Bank transfers settle immediately. KYC always passes. This makes automated test suites reliable — no flaky tests waiting for real PSP callbacks.


Next steps