Transfers & Payouts
Send money to Nigerian bank accounts with single or bulk transfers.
List Banks
Fetch the list of supported banks and their codes.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Request
GET /payouts/bank/list
- cURL
- Node.js
- Python
- Go
curl -X GET https://sandbox.payscribe.ng/api/v1/payouts/bank/list \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/bank/list', {
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/payouts/bank/list',
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/payouts/bank/list", 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))
}
Response
Status: 200 OK
{
"status": true,
"description": "Bank lookup successfully.",
"message": {
"details": [
{
"code": "058",
"name": "GTBank"
},
{
"code": "044",
"name": "Access Bank"
},
{
"code": "057",
"name": "Zenith Bank"
},
{
"code": "011",
"name": "First Bank"
},
{
"code": "033",
"name": "United Bank for Africa"
},
{
"code": "032",
"name": "Union Bank"
}
]
},
"status_code": 200
}
Test safely in sandbox
Do not paste an API key into a documentation page or browser. Instead, call this read-only endpoint from your server with a ps_pk_test_... key and inspect the response in your application logs or test suite. Use the Sandbox testing guide to verify the full payout lifecycle, including recipient validation and final reconciliation.
Account Name Lookup
Verify the account name before sending a transfer.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Content-Type | application/json |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
account | string | Yes | The NUBAN account number to look up (10 digits) |
bank | string | Yes | The bank code of the receiving bank |
Request
POST /payouts/account/lookup
- cURL
- Node.js
- Python
- Go
curl -X POST https://sandbox.payscribe.ng/api/v1/payouts/account/lookup \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account": "0123456789",
"bank": "058"
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/account/lookup', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"account": "0123456789",
"bank": "058"
}),
});
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'''{
"account": "0123456789",
"bank": "058"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/payouts/account/lookup',
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(`{
"account": "0123456789",
"bank": "058"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/payouts/account/lookup", 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: 200 OK
{
"status": true,
"description": "Account name retrieved successfully",
"message": {
"details": {
"account_number": "0123456789",
"account_name": "JOHN DOE",
"bank_code": "058"
}
},
"status_code": 200
}
Get Transfer Fee
Calculate the transfer fee for a given amount. amount is in naira (not kobo) — the same unit Single Transfer expects.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Transfer amount in naira, minimum 100 |
currency | string | Yes | Currency code. Sandbox always prices in NGN regardless of the value sent |
Request
GET /payouts/fee
- cURL
- Node.js
- Python
- Go
curl -X GET "https://sandbox.payscribe.ng/api/v1/payouts/fee?amount=5000¤cy=NGN" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/fee?amount=5000¤cy=NGN', {
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/payouts/fee',
params={'amount': 5000, 'currency': 'NGN'},
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/payouts/fee?amount=5000¤cy=NGN", 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))
}
Response
Status: 200 OK
{
"status": true,
"description": "Transfer fee lookup successful.",
"message": {
"details": {
"amount": "5000",
"currency": "ngn",
"fee": 150
}
},
"status_code": 200
}
There is no total field — add amount and fee yourself to get the debit total. fee is a percentage of amount capped at a fixed maximum, both configured server-side; call this endpoint to get the exact figure rather than hardcoding a rate. Sandbox always prices in ngn (lowercase) regardless of the currency value you send.
Single Transfer
Send money to a single bank account.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Content-Type | application/json |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in naira, not kobo (e.g. 5000 = ₦5,000.00), minimum 100 |
bank_code | string | Yes | Recipient bank code (bank also accepted) |
account_number | string | Yes | Recipient NUBAN account number, 10 digits (account also accepted) |
ref | string | No | Your transaction reference. Not validated as required, and not auto-generated if omitted — pass one to enable duplicate-transfer protection |
narration | string | No | Transfer description; defaults to "Payscribe Payout" |
The recipient account name is always resolved server-side via Account Name Lookup — there is no account_name input field; any value sent is ignored.
Request
POST /payouts/transfer
- cURL
- Node.js
- Python
- Go
curl -X POST https://sandbox.payscribe.ng/api/v1/payouts/transfer \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"ref": "ref_abc123",
"narration": "Payment for services"
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/transfer', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"ref": "ref_abc123",
"narration": "Payment for services"
}),
});
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'''{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"ref": "ref_abc123",
"narration": "Payment for services"
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/payouts/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(`{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"ref": "ref_abc123",
"narration": "Payment for services"
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/payouts/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: 200 OK
{
"status": true,
"description": "Transfer Successful.",
"message": {
"details": {
"event_id": "a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42",
"event_type": "payouts.created",
"trans_id": "a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42",
"session_id": "9c8f3b2a-5d1e-4c7a-9b0f-2e6d4a8c1f30",
"ref": "ref_abc123",
"amount": 5000,
"fee": 150,
"total": 5150,
"beneficiary": {
"bank": "058",
"bank_name": "GTBank",
"account_number": "0123456789",
"account_name": "JOHN DOE"
},
"currency": "",
"narration": "Payment for services",
"status": "success",
"created_at": "2026-07-20T10:30:00.000Z"
}
},
"status_code": 200
}
Sandbox transfers settle immediately with "status": "success" — there is no pending intermediate state in sandbox.
currency is currently always emptyThe currency field in this response is empty rather than "NGN" — the request-normalizing layer (Endpoints::bankTransfer()) never forwards a currency key to the transfer processor, only country. All amounts are still NGN in practice (there is no way to select another currency), but the field itself is not populated. Flagging as a code-level gap, not a documentation one.
Bulk Transfer
Send up to 100 transfers in a single request.
Tracing POST /payouts/transfer with more than one rows entry through the live code path shows it cannot succeed against sandbox.payscribe.ng. The request-normalizing bridge (Endpoints::bankTransfer()) puts multiple rows under a rows key with no top-level bank/account/amount, but the sandbox processor it's routed to (SandboxApiEndpoints::bankTransfer()) has no rows-handling branch at all — it always reads flat bank/account/amount fields, finds them empty, and returns a 406 "Bank not found" error. Bulk batching is implemented only in the live processor (App\Libraries\ApiEndpoints::bankTransfer()). Test bulk transfers by calling Single Transfer once per recipient in sandbox; this needs an engineering fix (or an explicit "live only" note) before it can be documented as sandbox-testable.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Content-Type | application/json |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
ref | string | No | One reference applied to the whole batch. Not validated as required, and not auto-generated if omitted |
rows | array | Yes | Array of transfer objects (max 100) |
Transfer object fields:
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in naira, not kobo |
bank_code | string | Yes | Recipient bank code |
account_number | string | Yes | Recipient NUBAN account number, 10 digits |
narration | string | No | Per-transfer description; defaults to "Payscribe Payout" |
When rows contains more than one transfer, the request is processed as a batch (live only — see above).
Request
POST /payouts/transfer
- cURL
- Node.js
- Python
- Go
curl -X POST https://sandbox.payscribe.ng/api/v1/payouts/transfer \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ref": "bulk_ref_001",
"rows": [
{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"narration": "Invoice INV-001"
},
{
"amount": 10000,
"bank_code": "044",
"account_number": "9876543210",
"narration": "Invoice INV-002"
}
]
}'
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/transfer', {
method: 'POST',
headers: {Authorization: `Bearer ${process.env.PAYSCRIBE_API_KEY}`, 'Content-Type': 'application/json'},
body: JSON.stringify({
"ref": "bulk_ref_001",
"rows": [
{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"narration": "Invoice INV-001"
},
{
"amount": 10000,
"bank_code": "044",
"account_number": "9876543210",
"narration": "Invoice INV-002"
}
]
}),
});
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": "bulk_ref_001",
"rows": [
{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"narration": "Invoice INV-001"
},
{
"amount": 10000,
"bank_code": "044",
"account_number": "9876543210",
"narration": "Invoice INV-002"
}
]
}''')
response = requests.post(
'https://sandbox.payscribe.ng/api/v1/payouts/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": "bulk_ref_001",
"rows": [
{
"amount": 5000,
"bank_code": "058",
"account_number": "0123456789",
"narration": "Invoice INV-001"
},
{
"amount": 10000,
"bank_code": "044",
"account_number": "9876543210",
"narration": "Invoice INV-002"
}
]
}`)
request, err := http.NewRequest(http.MethodPost, "https://sandbox.payscribe.ng/api/v1/payouts/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: 200 OK
{
"status": true,
"description": "Bulk transfer queued.",
"message": {
"details": {
"batch_id": "batch_uuid",
"ref": "bulk_ref_001",
"gross": 15000,
"count": 2,
"created_at": "2026-07-20T10:35:00.000Z"
}
},
"status_code": 200
}
Verify Transfer
Check the status of a transfer using the trans_id (also returned as event_id) from its Single Transfer response — not your own ref.
Headers
| Header | Value |
|---|---|
Authorization | Bearer $PAYSCRIBE_API_KEY |
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The trans_id returned by Single Transfer |
Request
GET /payouts/verify/{id}
- cURL
- Node.js
- Python
- Go
curl -X GET "https://sandbox.payscribe.ng/api/v1/payouts/verify/a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42" \
-H "Authorization: Bearer $PAYSCRIBE_API_KEY"
const response = await fetch('https://sandbox.payscribe.ng/api/v1/payouts/verify/a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42', {
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/payouts/verify/a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42',
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/payouts/verify/a3f2c9d1-7e4b-4f2a-9c3e-1b8d2f6a0c42", 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))
}
Response
Status: 200 OK
{
"status": true,
"description": "Verification Successful! ",
"message": {
"details": {
"status": "success",
"description": "Money transfer sent to JOHN DOE GTBank ",
"amount": 5000,
"session_id": "9c8f3b2a-5d1e-4c7a-9b0f-2e6d4a8c1f30"
}
},
"status_code": 200
}
This is a much smaller payload than the initial transfer response — it does not repeat the bank/account/fee details, only status, an internal description, amount, and the transfer's session_id. The transaction is looked up by trans_id scoped to your account, so passing your own ref or a bank reference in {id} returns a 404 "Transaction does not exist for this account."
Webhooks
| Event | Description |
|---|---|
payouts.created | Transfer initiated and pending processing (single transfer or batch) |
See Webhooks for payload format and verification.
Was this page helpful?