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:
| # | Account | Type | Amount (kobo) |
|---|---|---|---|
| 1 | wlt_9f3kA2mXpQ (sender) | DEBIT | -502500 |
| 2 | wlt_3pRsT7uVwX (recipient) | CREDIT | +500000 |
| 3 | wallet_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
| Status | Can debit | Can credit | Notes |
|---|---|---|---|
PENDING | No | No | MFB account being provisioned (usually <5s) |
ACTIVE | Yes | Yes | Normal operating state |
FROZEN | No | Yes | Can still receive funds; outbound blocked |
CLOSED | No | No | Terminal state. Create a new wallet if needed. |
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.
| Transaction | Ledger Entry | |
|---|---|---|
| What it is | The business event | The accounting record |
| Example | "User A sent ₦5,000 to User B" | "Debit wlt_A by 502500 kobo" |
| ID prefix | tx_ | le_ |
| Count per event | 1 | 2 or more |
| Holds | Fee breakdown, PSP reference, status | Amount, 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
| Status | Meaning |
|---|---|
pending | Request received; funds not yet reserved |
processing | Funds reserved; request sent to MFB/PSP |
completed | PSP confirmed. Funds are settled. This is the only state where recipients can consider funds available. |
failed | PSP rejected the transfer. Funds are returned to source wallet. |
reversed | A completed transaction was reversed (rare; requires PayKore support). |
expired | Request timed out before processing (USSD sessions only). |
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 type | Fee | Cap |
|---|---|---|
| Wallet funding (inbound) | Free | — |
| P2P Transfer | 0.5% of amount | ₦200 (20000 kobo) |
| Bank Transfer (outbound NIP) | ₦100 flat | — |
| USSD Payment | 1.5% of amount | ₦500 (50000 kobo) |
| QR Payment | 1.5% of amount | ₦500 (50000 kobo) |
| Split Payment | 2.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 amountplatform_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
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" \
...
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
| Sandbox | Live | |
|---|---|---|
| API key prefix | sk_test_ | sk_live_ |
| Money | Test money only | Real NGN |
| Bank transfers | Complete instantly with fake PSP ref | Real NIP transfer (seconds to minutes) |
| KYC | Returns verified for any BVN/NIN | Real identity check via Mono/Smile |
| Webhooks | Fire normally | Fire 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
- Authentication → — API key types, security rules, and key rotation.
- Wallets → — Full wallet lifecycle with all API calls.
- Webhooks → — Receiving and verifying real-time events.