Skip to main content

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 200 before running business logic, not after.
  • Every flow has been tested through both its success path and at least one realistic failure path.
warning

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