Skip to main content

Receive a Payment via Virtual Account

This recipe walks through creating a customer, assigning a virtual bank account, receiving a payment (or simulating one in sandbox), and confirming the funds.

Engineering flowBuild, verify, then reconcile
Test this flow in sandbox
  1. 01Create customerCreate and persist the Payscribe customer ID.
  2. 02Create accountIssue a virtual account and link it to your internal order.
  3. 03Trigger paymentTest with an approved sandbox transfer simulation.
  4. 04Verify and reconcileVerify the signed event and update the order exactly once.

Use a dashboard-issued sandbox API key on your server. Do not enter credentials into this documentation site.

Start in sandbox

The commands below use sandbox values. Replace the placeholder with a ps_pk_test_... key held only in your server environment. Switch both base URL and key together only after the go-live checklist.

Prerequisites

  • A Payscribe business account
  • Your API key

Step 1: Create a customer

Virtual accounts are linked to customers. Create one:

curl -X POST https://sandbox.payscribe.ng/api/v1/customers/create \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "08012345678"
}'

Step 2: Create a virtual account

Assign a static virtual bank account number that can receive unlimited payments:

curl -X POST https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/create \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer_id": "cus_abc123", "bank": "9psb"}'

Response:

{
"status": true,
"description": "Virtual account created successfully.",
"message": {
"details": {
"customer": {
"id": "cus_abc123",
"name": "John Doe"
},
"account": [
{
"id": "acc_xyz789",
"account_number": "1234567890",
"account_name": "Acme Ltd",
"bank_name": "9PSB",
"bank_code": "120001",
"currency": "NGN",
"account_type": "static"
}
],
"status": "active",
"created_at": "2026-07-20T10:30:00.000Z",
"updated_at": "2026-07-20T10:30:00.000Z"
}
},
"status_code": 200
}

Give this account number to your customer. Any incoming transfer will be detected automatically.

Step 3: Simulate a payment (sandbox)

In sandbox, use the simulate transfer endpoint to test the full flow. The request must be signed — hash is the uppercase SHA512 of your sandbox test secret key (ps_sk_test_..., distinct from the ps_pk_test_... public key you send in the Authorization header) concatenated with sender_account_number, account, bank, amount, and ref:

HASH=$(printf '%s' "$PAYSCRIBE_SECRET_KEY" "0123456789" "1234567890" "9psb" "5000" "SIM-001" \
| openssl dgst -sha512 | awk '{print $2}' | tr 'a-f' 'A-F')

curl -X POST https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/simulate-transfer \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ref": "SIM-001",
"account": "1234567890",
"amount": 5000,
"currency": "NGN",
"bank": "9psb",
"sender_account_number": "0123456789",
"sender_name": "Jane Doe",
"description": "Test transfer",
"name": "John Doe",
"hash": "'"$HASH"'"
}'

Response:

{
"status": true,
"description": "Payment Confirmed!",
"message": {
"details": {
"trans_id": "a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42",
"amount": 5000,
"fee": 32.5,
"currency": "NGN",
"transaction": {
"session_id": "sess_5f4c2b1a9d8e",
"date": "2026-07-20T12:00:00.000Z",
"bank_name": "9PSB",
"sender_account": "0123456789",
"sender_name": "Jane Doe",
"amount": 5000,
"currency": "NGN",
"description": "Test transfer"
},
"customer": {
"id": "cus_abc123",
"name": "John Doe",
"number": "1234567890"
}
}
},
"status_code": 200
}

After simulation:

  • The business wallet is credited with amount - fee
  • A settlement is recorded in the ledger
  • Sandbox simulate-transfer returns Payment Confirmed! without dispatching a webhook. To also exercise webhook delivery in sandbox, call confirm-payment with the returned session_id and account_number — that endpoint dispatches accounts.payment.status to your configured test webhook URL.

Step 4: Confirm the payment via webhook

When a real inbound transfer is credited to your virtual account, Payscribe sends an accounts.payment.status webhook to your endpoint:

{
"event_id": "evt_015",
"event_type": "accounts.payment.status",
"trans_id": "txn_xyz789",
"account_number": "1234567890",
"amount": 5000,
"fee": 32.5,
"net_amount": 4967.5,
"customer_id": "cus_abc123",
"status": "success",
"created_at": "2026-07-29T10:30:00Z"
}

Verify the signature:

import crypto from 'node:crypto';

app.post('/webhooks/payscribe', express.raw({type: 'application/json'}), (req, res) => {
const timestamp = req.headers['x-payscribe-timestamp'];
const eventId = req.headers['x-payscribe-event-id'];
const signature = req.headers['x-payscribe-signature'];
const expected = `v1=${crypto.createHmac('sha256', process.env.PAYSCRIBE_WEBHOOK_SECRET)
.update(`${timestamp}.${eventId}.${req.body.toString('utf8')}`).digest('hex')}`;

if (!signature || signature.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.sendStatus(401);
}
// Store eventId once, queue reconciliation, then acknowledge.
return res.sendStatus(200);
});

Step 5: Check the wallet balance

Verify that the funds were credited (after fee deduction):

curl -X GET https://sandbox.payscribe.ng/api/v1/my-account/balances \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"

Full example (Python)

import requests, json

BASE = "https://sandbox.payscribe.ng/api/v1"
KEY = "ps_pk_test_your_api_key"
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}

# Create customer
cust = requests.post(f"{BASE}/customers/create", headers=HEADERS, json={
"first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "08012345678"
}).json()
cid = cust["message"]["details"]["customer_id"]
print(f"Customer: {cid}")

# Create virtual account
va = requests.post(f"{BASE}/collections/virtual-accounts/create", headers=HEADERS, json={
"customer_id": cid, "bank": "9psb"
}).json()
acct = va["message"]["details"]["account_number"]
print(f"VA: {acct} at {va['message']['details']['bank_name']}")

# Check balance
bal = requests.get(f"{BASE}/my-account/balances", headers=HEADERS).json()
print(f"Balance: {bal['message']['details']}")

Next steps

Was this page helpful?

Report a docs issue →