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.
- 01Create customerCreate and persist the Payscribe customer ID.
- 02Create accountIssue a virtual account and link it to your internal order.
- 03Trigger paymentTest with an approved sandbox transfer simulation.
- 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.
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
- Node.js
- Python
- Go
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"
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/customers/create', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "08012345678"
}),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
import json
payload = json.loads(r'''{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "08012345678"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/customers/create',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}", 'Content-Type': 'application/json'},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"strings"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := strings.NewReader(`{
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "08012345678"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/customers/create", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
Step 2: Create a virtual account
Assign a static virtual bank account number that can receive unlimited payments:
- cURL
- Node.js
- Python
- Go
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"}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/create', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"customer_id": "cus_abc123",
"bank": "9psb"
}),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
import json
payload = json.loads(r'''{
"customer_id": "cus_abc123",
"bank": "9psb"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/create',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}", 'Content-Type': 'application/json'},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"strings"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := strings.NewReader(`{
"customer_id": "cus_abc123",
"bank": "9psb"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/create", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
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:
- cURL
- Node.js
- Python
- Go
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"'"
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/simulate-transfer', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({ "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"'" }),
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
import json
payload = json.loads(r'''{ "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 = requests.post(
'https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/simulate-transfer',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}", 'Content-Type': 'application/json'},
json=payload,
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"strings"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := strings.NewReader(`{ "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"'" }`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/collections/virtual-accounts/simulate-transfer", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
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-transferreturnsPayment Confirmed!without dispatching a webhook. To also exercise webhook delivery in sandbox, callconfirm-paymentwith the returnedsession_idandaccount_number— that endpoint dispatchesaccounts.payment.statusto 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:
- Node.js
- Python
- Go
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);
});
import hmac, hashlib
payload = request.get_data().decode()
ts = request.headers['X-Payscribe-Timestamp']
event_id = request.headers['X-Payscribe-Event-Id']
sig = request.headers['X-Payscribe-Signature']
expected = 'v1=' + hmac.new(secret.encode(), f'{ts}.{event_id}.{payload}'.encode(), hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, sig):
return ('OK', 200)
else:
return ('', 401)
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func payscribeWebhook(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
timestamp := r.Header.Get("X-Payscribe-Timestamp")
eventID := r.Header.Get("X-Payscribe-Event-Id")
received := r.Header.Get("X-Payscribe-Signature")
mac := hmac.New(sha256.New, []byte(os.Getenv("PAYSCRIBE_WEBHOOK_SECRET")))
mac.Write([]byte(timestamp + "." + eventID + "." + string(payload)))
expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
if received == "" || !hmac.Equal([]byte(expected), []byte(received)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// Store eventID once, queue reconciliation, then acknowledge.
w.WriteHeader(http.StatusOK)
}
Step 5: Check the wallet balance
Verify that the funds were credited (after fee deduction):
- cURL
- Node.js
- Python
- Go
curl -X GET https://sandbox.payscribe.ng/api/v1/my-account/balances \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/my-account/balances', {
method: 'GET',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`},
});
const result = await response.json().catch(() => null);
if (!response.ok) throw new Error(`Payscribe request failed: ${response.status}`);
console.log(result);
import os
import requests
response = requests.get(
'https://sandbox.payscribe.ng/api/v1/my-account/balances',
headers={'Authorization': f"Bearer {os.environ['PAYSCRIBE_API_KEY']}"},
timeout=20,
)
response.raise_for_status()
print(response.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
var body io.Reader
request, err := http.NewRequest(http.MethodGet, "https://sandbox.payscribe.ng/api/v1/my-account/balances", body)
if err != nil { panic(err) }
request.Header.Set("Authorization", "Bearer "+os.Getenv("PAYSCRIBE_API_KEY"))
response, err := http.DefaultClient.Do(request)
if err != nil { panic(err) }
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)
if response.StatusCode < 200 || response.StatusCode > 299 { panic(fmt.Sprintf("Payscribe request failed: %s", response.Status)) }
fmt.Println(string(responseBody))
}
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?