Test Scenarios
Before switching to live API keys, verify every scenario below using your sandbox environment. Each item lists how to trigger it, the expected outcome, and what to check in the response or webhook.
Category 1 — Wallet operations
Create a wallet successfully
Trigger: POST /v1/wallets with a new userRef.
Expected: 201 with status: "active" (or briefly "pending") and a populated nuban.
Verify: the id returned can be fetched immediately via GET /v1/wallets/{id}.
Attempt to create a duplicate wallet (same userRef)
Trigger: POST /v1/wallets twice with the same userRef and currency.
Expected: 409 DUPLICATE_WALLET on the second call.
Verify: the error body includes existing_wallet_id matching the first wallet's ID.
Get wallet balance after a credit
Trigger: fund a wallet via POST /v1/sandbox/bank-transfer simulation or the sandbox funding endpoint, then GET /v1/wallets/{id}.
Expected: balance_kobo reflects the credited amount exactly.
Verify: balance_naira matches balance_kobo / 100 formatted to two decimal places.
Verify balance is 0 on a new wallet
Trigger: GET /v1/wallets/{id} immediately after creation, before any funding.
Expected: balance_kobo: 0.
Verify: no ledger entries exist yet for this wallet (transaction history is empty).
Category 2 — P2P Transfers
Successful transfer between two wallets
Trigger: POST /v1/transfers/p2p between two active, funded wallets.
Expected: 200 with status: "completed".
Verify: source wallet balance decreased by amount + fee; destination wallet balance increased by exactly the transfer amount.
Transfer with insufficient funds
Trigger: attempt a transfer larger than the source wallet's balance.
Expected: 422 INSUFFICIENT_FUNDS.
Verify: neither wallet's balance changed — fetch both and confirm balances are unchanged from before the attempt.
Transfer to self (same wallet)
Trigger: set source_wallet_id and dest_wallet_id to the same value.
Expected: 400 SAME_WALLET_TRANSFER.
Verify: no transaction or ledger entry was created.
Duplicate idempotency key
Trigger: send the same POST /v1/transfers/p2p request twice with an identical Idempotency-Key header.
Expected: both calls return 200 with the same transaction.id.
Verify: the wallet was debited only once — check balance_kobo reflects a single transfer, not two.
Transfer to a frozen wallet
Trigger: freeze a wallet (POST /v1/wallets/{id}/freeze), then attempt to transfer into it.
Expected: this should succeed — frozen wallets can still receive credits. Confirm your understanding of this matches actual behaviour before assuming it's an error case.
Verify: instead, test transferring out of a frozen wallet, which should return 422 WALLET_FROZEN.
Transfer to a closed wallet
Trigger: close a wallet, then attempt any transfer involving it (source or destination).
Expected: 422 WALLET_CLOSED.
Verify: the error is returned regardless of whether the closed wallet was the source or destination.
Category 3 — Bank Transfers
Initiate a bank transfer
Trigger: POST /v1/transfers/bank with a resolved destination account.
Expected: 202 Accepted with status: "processing".
Verify: the source wallet is debited immediately, even though the transaction itself is still processing.
Simulate completion
Trigger: POST /v1/sandbox/bank-transfer/complete with the transaction ID from the previous step.
Expected: the transaction's status transitions to completed.
Verify: a transaction.completed webhook is delivered to your registered endpoint with matching id and reference.
Simulate failure
Trigger: POST /v1/sandbox/bank-transfer/fail with the transaction ID.
Expected: the transaction's status transitions to failed.
Verify: a transaction.failed webhook is delivered, reversal_status is "reversed", and the source wallet's balance is credited back to its pre-transfer amount.
Transfer with insufficient funds
Trigger: attempt a bank transfer larger than the source wallet's balance.
Expected: 422 INSUFFICIENT_FUNDS.
Verify: no transaction is created and no webhook fires.
Category 4 — QR Payments
Create a QR intent
Trigger: POST /v1/payments/qr/intent.
Expected: 201 with a qr_payload field containing a JSON string.
Verify: parsing qr_payload with JSON.parse() succeeds and contains pc, id, amt, and ref fields.
Pay a QR intent from a funded wallet
Trigger: POST /v1/payments/qr/pay with a valid, unexpired intent_id and a wallet with sufficient balance.
Expected: 200 with status: "completed".
Verify: the merchant wallet's balance increased by the intent amount minus fees; a transaction.completed webhook fires.
Pay an expired QR intent
Trigger: wait past the 15-minute expiry (or use a pre-expired test intent if your sandbox supports backdating), then attempt to pay it.
Expected: 410 INTENT_EXPIRED.
Verify: no transaction is created.
Pay an already-paid intent
Trigger: successfully pay an intent once, then attempt to pay the same intent_id again.
Expected: 409 INTENT_ALREADY_PAID.
Verify: the payer's wallet was not debited a second time.
Category 5 — USSD Payments
Initiate a USSD session
Trigger: POST /v1/payments/ussd/initiate.
Expected: 201 with a session_id and a dial_code string.
Verify: expires_at is exactly 3 minutes after created_at (or the current time if created_at isn't returned directly).
Simulate confirmation
Trigger: POST /v1/sandbox/ussd/simulate with action: "confirm".
Expected: the session's transaction completes.
Verify: a transaction.completed webhook fires with type: "ussd_payment".
Simulate cancellation
Trigger: POST /v1/sandbox/ussd/simulate with action: "cancel".
Expected: the session's transaction fails with no charge.
Verify: the payer wallet's balance is unchanged; a transaction.failed webhook fires.
Let a session expire
Trigger: create a session and wait past its 3-minute TTL without simulating any action.
Expected: subsequent simulation attempts against that session return an expiry error.
Verify: GET /v1/payments/ussd/{sessionId} shows status: "expired".
Category 6 — KYC
Submit a valid BVN
Trigger: POST /v1/kyc/bvn with any 11-digit number other than the reserved test values.
Expected: 202 Accepted with status: "processing".
Verify: shortly after, a kyc.verified webhook fires for the same kyc_id.
Webhook fires with verified status
Trigger: same as above — this is the follow-up check.
Expected: kyc.verified event with status: "verified" and a populated verified_at.
Verify: GET /v1/kyc/{userRef}/status independently confirms status: "verified".
Submit using the "always fail" test BVN
Trigger: POST /v1/kyc/bvn with bvn: "00000000000".
Expected: a kyc.failed webhook fires with failure_reason: "BVN_NOT_FOUND".
Verify: GET /v1/kyc/{userRef}/status shows status: "failed" with the matching failure_reason.
Category 7 — Webhooks
Register an endpoint
Trigger: POST /v1/webhooks with a valid HTTPS URL and an events array.
Expected: 201 with a secret field (shown once) and status: "active".
Verify: GET /v1/webhooks (or paykore.webhooks.list()) shows the new endpoint in the returned list.
Trigger a transaction and verify delivery
Trigger: perform any transaction matching a subscribed event type.
Expected: the webhook fires within seconds.
Verify: check Webhooks → Deliveries in the dashboard — confirm the delivery shows a 200 response and a reasonable response time.
Return a non-2xx from your handler temporarily
Trigger: configure your webhook handler to return 500 for one test request, then trigger a transaction.
Expected: PayKore retries according to the documented backoff schedule (1s, 5s, 30s, ...).
Verify: the dashboard delivery log shows multiple attempts for the same event, with increasing intervals between them.
Category 8 — Pre-launch checklist
Confirm every item below before requesting live API keys:
- All webhook endpoints use HTTPS — no plain HTTP URLs registered.
- Signature verification is implemented and tested against both valid and tampered payloads.
- Idempotency keys are generated per distinct transaction attempt, not reused across unrelated requests.
- API keys are stored in environment variables or a secrets manager — never committed to source code.
- Error handling branches on
error.code, not solely on HTTP status code. - Webhook handlers return
200before running business logic, not after. - Every flow has been tested through both its success path and at least one realistic failure path.
Skipping signature verification or hardcoding API keys are the two most common causes of security incidents in production payment integrations. Do not proceed to live until both are confirmed working.
Next steps
- Sandbox Environment → — Simulation endpoints and automated test examples referenced throughout this checklist.
- Authentication → — Requesting live API keys once this checklist is complete.