Split payments and subaccounts
Register the payees your collections are shared with, split each payment by percentage or flat amount, choose who absorbs the charge, and have each share paid out on its own schedule.
If you run a marketplace, a platform or any business that shares revenue, you can have a single customer payment divided between yourself and the people you work with - vendors, partners, branches - and have each of them paid out automatically. Each payee is a subaccount, and a reusable arrangement of several of them is a split group.
This works the same way as Paystack's subaccounts and multi-split, so if you have built on that before, the concepts carry over directly.
How it fits together
- Register a subaccount for each payee, with the bank account or mobile money wallet their share is paid to.
- Split a collection by adding a
splitobject toPOST /transactions/collect,POST /transactions/pay-bill-extorPOST /checkout/generate-payment-link. - When the payment is confirmed, each party's share is credited: yours to your ledger balance, each subaccount's to its own balance.
- Each subaccount's balance is paid out on its settlement schedule - or on demand.
Nothing changes for a collection without a split: the whole net amount is credited to your
ledger, exactly as before.
Scopes
Managing subaccounts needs SUBACCOUNTS_WRITE, and reading them needs SUBACCOUNTS_READ. These
are not part of the default set a new key receives - ask PayDirect to add them to the key your
backend uses. Splitting a collection needs only the scope the collection endpoint already requires.
Registering a subaccount
POST /subaccounts
Show the PayDirect client (defined once in Authentication)
PayDirect has no SDK, so write this once. Every example in these guides calls the client defined here, which means signing lives in exactly one place - and this block is itself a single shared file, so if it changes, it changes on every page at once.
Each version reads PAYDIRECT_HOST, PAYDIRECT_PUBLIC_KEY, PAYDIRECT_RAW_KEY and
PAYDIRECT_SECRET_KEY from the environment, signs every request, and sends an Idempotency-Key
on writes (see Idempotency).
cURL
# paydirect.sh - source this, then call `paydirect POST /transactions/collect "$BODY"`
# Works for GET, POST, PATCH and DELETE; a body is optional.
paydirect() {
local method="$1" path="$2" body="${3:-}"
local ts sig
ts=$(date +%s000) # milliseconds, not seconds
sig=$(printf '%s:%s' "$PAYDIRECT_RAW_KEY" "$ts" \
| openssl dgst -sha256 -hmac "$PAYDIRECT_SECRET_KEY" -hex \
| awk '{print $2}')
curl -sS -X "$method" "$PAYDIRECT_HOST$path" \
-H "Content-Type: application/json" \
-H "x-api-key-id: $PAYDIRECT_PUBLIC_KEY" \
-H "x-api-timestamp: $ts" \
-H "x-api-signature: $sig" \
${body:+-H "Idempotency-Key: $(uuidgen)"} \
${body:+-d "$body"}
}
Node.js
// paydirect.js - Node 18+, no dependencies
const crypto = require('crypto');
function signedHeaders() {
const timestamp = Date.now().toString(); // milliseconds
const signature = crypto
.createHmac('sha256', process.env.PAYDIRECT_SECRET_KEY)
.update(`${process.env.PAYDIRECT_RAW_KEY}:${timestamp}`)
.digest('hex');
return {
'Content-Type': 'application/json',
'x-api-key-id': process.env.PAYDIRECT_PUBLIC_KEY,
'x-api-timestamp': timestamp,
'x-api-signature': signature,
};
}
async function request(method, path, body, idempotencyKey) {
const headers = signedHeaders();
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const response = await fetch(`${process.env.PAYDIRECT_HOST}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
return { status: response.status, body: await response.json() };
}
const payDirect = {
get: path => request('GET', path),
post: (path, body, idempotencyKey) => request('POST', path, body, idempotencyKey),
patch: (path, body) => request('PATCH', path, body),
del: path => request('DELETE', path),
};
module.exports = { payDirect };
Python
# paydirect.py - requires `requests`
import hashlib, hmac, os, time, uuid
import requests
HOST = os.environ["PAYDIRECT_HOST"]
def _signed_headers() -> dict:
timestamp = str(int(time.time() * 1000)) # milliseconds, not seconds
signature = hmac.new(
os.environ["PAYDIRECT_SECRET_KEY"].encode(),
f'{os.environ["PAYDIRECT_RAW_KEY"]}:{timestamp}'.encode(),
hashlib.sha256,
).hexdigest()
return {
"Content-Type": "application/json",
"x-api-key-id": os.environ["PAYDIRECT_PUBLIC_KEY"],
"x-api-timestamp": timestamp,
"x-api-signature": signature,
}
def request(method: str, path: str, body: dict | None = None, idempotency_key: str | None = None):
headers = _signed_headers()
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
response = requests.request(method, f"{HOST}{path}", json=body, headers=headers, timeout=30)
return response.status_code, response.json()
def get(path: str):
return request("GET", path)
def post(path: str, body: dict, idempotency_key: str | None = None):
return request("POST", path, body, idempotency_key or str(uuid.uuid4()))
def patch(path: str, body: dict):
return request("PATCH", path, body)
def delete(path: str):
return request("DELETE", path)
PHP
<?php
// PayDirect.php - requires ext-curl, no packages
final class PayDirect
{
public function get(string $path): array
{
return $this->request('GET', $path);
}
public function post(string $path, array $body, ?string $idempotencyKey = null): array
{
return $this->request('POST', $path, $body, $idempotencyKey ?? bin2hex(random_bytes(16)));
}
public function patch(string $path, array $body): array
{
return $this->request('PATCH', $path, $body);
}
public function delete(string $path): array
{
return $this->request('DELETE', $path);
}
private function signedHeaders(): array
{
$timestamp = (string) round(microtime(true) * 1000); // milliseconds
$signature = hash_hmac(
'sha256',
getenv('PAYDIRECT_RAW_KEY') . ':' . $timestamp,
getenv('PAYDIRECT_SECRET_KEY')
);
return [
'Content-Type: application/json',
'x-api-key-id: ' . getenv('PAYDIRECT_PUBLIC_KEY'),
'x-api-timestamp: ' . $timestamp,
'x-api-signature: ' . $signature,
];
}
private function request(string $method, string $path, ?array $body = null, ?string $key = null): array
{
$headers = $this->signedHeaders();
if ($key !== null) {
$headers[] = 'Idempotency-Key: ' . $key;
}
$ch = curl_init(getenv('PAYDIRECT_HOST') . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return ['status' => $status, 'body' => json_decode($raw, true)];
}
}
Java
// PayDirect.java - JDK 11+, no dependencies
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;
public final class PayDirect {
private static final String HOST = System.getenv("PAYDIRECT_HOST");
private final HttpClient http = HttpClient.newHttpClient();
public HttpResponse<String> get(String path) throws Exception {
return request("GET", path, null, null);
}
public HttpResponse<String> post(String path, String jsonBody) throws Exception {
return request("POST", path, jsonBody, UUID.randomUUID().toString());
}
public HttpResponse<String> patch(String path, String jsonBody) throws Exception {
return request("PATCH", path, jsonBody, null);
}
public HttpResponse<String> delete(String path) throws Exception {
return request("DELETE", path, null, null);
}
private static String hmacHex(String message, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
StringBuilder hex = new StringBuilder();
for (byte b : mac.doFinal(message.getBytes(StandardCharsets.UTF_8))) {
hex.append(String.format("%02x", b)); // lowercase hex
}
return hex.toString();
}
private HttpResponse<String> request(String method, String path, String body, String key)
throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis()); // milliseconds
String signature = hmacHex(System.getenv("PAYDIRECT_RAW_KEY") + ":" + timestamp,
System.getenv("PAYDIRECT_SECRET_KEY"));
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(HOST + path))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.header("x-api-key-id", System.getenv("PAYDIRECT_PUBLIC_KEY"))
.header("x-api-timestamp", timestamp)
.header("x-api-signature", signature)
.method(method, body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (key != null) {
builder.header("Idempotency-Key", key);
}
return http.send(builder.build(), HttpResponse.BodyHandlers.ofString());
}
}
C#
// PayDirect.cs - .NET 6+, no packages
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
public sealed class PayDirect
{
private static readonly HttpClient Http = new();
private static readonly string Host = Environment.GetEnvironmentVariable("PAYDIRECT_HOST")!;
public Task<HttpResponseMessage> GetAsync(string path) =>
SendAsync(HttpMethod.Get, path, null, null);
public Task<HttpResponseMessage> PostAsync(string path, object body, string? idempotencyKey = null) =>
SendAsync(HttpMethod.Post, path, body, idempotencyKey ?? Guid.NewGuid().ToString());
public Task<HttpResponseMessage> PatchAsync(string path, object body) =>
SendAsync(HttpMethod.Patch, path, body, null);
public Task<HttpResponseMessage> DeleteAsync(string path) =>
SendAsync(HttpMethod.Delete, path, null, null);
private static string HmacHex(string message, string secret)
{
using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = mac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hash).ToLowerInvariant(); // lowercase hex
}
private static async Task<HttpResponseMessage> SendAsync(
HttpMethod method, string path, object? body, string? idempotencyKey)
{
// Milliseconds since the epoch, not seconds.
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
var signature = HmacHex(
$"{Environment.GetEnvironmentVariable("PAYDIRECT_RAW_KEY")}:{timestamp}",
Environment.GetEnvironmentVariable("PAYDIRECT_SECRET_KEY")!);
using var request = new HttpRequestMessage(method, Host + path);
request.Headers.Add("x-api-key-id", Environment.GetEnvironmentVariable("PAYDIRECT_PUBLIC_KEY"));
request.Headers.Add("x-api-timestamp", timestamp);
request.Headers.Add("x-api-signature", signature);
if (idempotencyKey is not null)
{
request.Headers.Add("Idempotency-Key", idempotencyKey);
}
if (body is not null)
{
request.Content = JsonContent.Create(body);
}
return await Http.SendAsync(request);
}
}
cURL
paydirect POST "/subaccounts" '{
"currency": "GHS",
"businessName": "Kofi Stores",
"subaccountSharePercent": 80,
"settlementSchedule": "DAILY",
"accountType": "BANK_ACCOUNT",
"accountNumber": "0011223344",
"swiftCode": "300591",
"primaryContactEmail": "accounts@kofistores.example"
}'
Node.js
const { status, body } = await payDirect.post(
'/subaccounts',
{
currency: 'GHS',
businessName: 'Kofi Stores',
subaccountSharePercent: 80,
settlementSchedule: 'DAILY',
accountType: 'BANK_ACCOUNT',
accountNumber: '0011223344',
swiftCode: '300591',
primaryContactEmail: 'accounts@kofistores.example',
},
);
Python
status, body = post(
"/subaccounts",
{
"currency": "GHS",
"businessName": "Kofi Stores",
"subaccountSharePercent": 80,
"settlementSchedule": "DAILY",
"accountType": "BANK_ACCOUNT",
"accountNumber": "0011223344",
"swiftCode": "300591",
"primaryContactEmail": "accounts@kofistores.example",
},
)
PHP
<?php
$result = (new PayDirect())->post('/subaccounts', [
'currency' => 'GHS',
'businessName' => 'Kofi Stores',
'subaccountSharePercent' => 80,
'settlementSchedule' => 'DAILY',
'accountType' => 'BANK_ACCOUNT',
'accountNumber' => '0011223344',
'swiftCode' => '300591',
'primaryContactEmail' => 'accounts@kofistores.example',
]);
Java
String payload = """
{
"currency": "GHS",
"businessName": "Kofi Stores",
"subaccountSharePercent": 80,
"settlementSchedule": "DAILY",
"accountType": "BANK_ACCOUNT",
"accountNumber": "0011223344",
"swiftCode": "300591",
"primaryContactEmail": "accounts@kofistores.example"
}
""";
HttpResponse<String> response = new PayDirect().post("/subaccounts", payload);
C#
var response = await new PayDirect().PostAsync("/subaccounts", new
{
currency = "GHS",
businessName = "Kofi Stores",
subaccountSharePercent = 80,
settlementSchedule = "DAILY",
accountType = "BANK_ACCOUNT",
accountNumber = "0011223344",
swiftCode = "300591",
primaryContactEmail = "accounts@kofistores.example",
});
accountTypeisBANK_ACCOUNT(withswiftCode, the GHIPSS routing code fromPOST /transactions/banks/list) orE_WALLET(withewalletType).subaccountSharePercentis the share the subaccount receives when you split a collection with it on its own. We name it for what the subaccount gets on purpose - Paystack'spercentage_chargeis described both ways in different places.- The response carries a
subaccountCode(SUB_...). That is what you use from then on.
Other endpoints: GET /subaccounts, GET /subaccounts/{code} (includes its current balance),
PATCH /subaccounts/{code}, POST /subaccounts/{code}/deactivate and /activate, and
GET /subaccounts/{code}/ledger for its entry history.
Destination checks and the payout hold
A payout destination is the sensitive part of a subaccount - it decides where money goes without anyone approving each payment. So:
- With a live key, the destination is confirmed by name enquiry when you register it or change
it. The name the bank or network returns is stored as
accountName; you cannot set it yourself. A destination that cannot be confirmed is rejected. - A new subaccount, and any change to its destination, starts a payout hold (24 hours by
default, shown as
payoutsHeldUntil). Shares keep accruing during the hold, but nothing is paid out until it ends. - Your contact email is notified each time, with the destination masked.
If you ever receive one of those emails for a change you did not make, deactivate the subaccount, revoke the key and contact PayDirect - the hold is there to give you that window.
Live subaccounts require a verified integration. Test keys never contact a bank or network: the destination is not checked, payouts are simulated, and there is no payout hold - so you can try settlement end to end in sandbox straight away.
Splitting a collection
Add a split object. It takes exactly one of two shapes.
With one subaccount:
cURL
paydirect POST "/transactions/collect" '{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R"
}
}'
Node.js
const { status, body } = await payDirect.post(
'/transactions/collect',
{
amountToSend: 150.75,
sendingCurrency: 'GHS',
receivingCurrency: 'GHS',
invoiceOrAccountNumber: 'order-10482',
description: 'Order #10482',
customerAccountType: 'E_WALLET',
customerEwalletType: 'TELECEL',
customerAccountName: 'Ama Serwaa',
customerAccountNumber: '0204000000',
customerEmailAddress: 'ama@example.com',
split: {
subaccountCode: 'SUB_4fT9kLm2Qx8vNp3R',
},
},
`collect:${order.id}`,
);
Python
status, body = post(
"/transactions/collect",
{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R",
},
},
idempotency_key=f"collect:{order.id}",
)
PHP
<?php
$result = (new PayDirect())->post('/transactions/collect', [
'amountToSend' => 150.75,
'sendingCurrency' => 'GHS',
'receivingCurrency' => 'GHS',
'invoiceOrAccountNumber' => 'order-10482',
'description' => 'Order #10482',
'customerAccountType' => 'E_WALLET',
'customerEwalletType' => 'TELECEL',
'customerAccountName' => 'Ama Serwaa',
'customerAccountNumber' => '0204000000',
'customerEmailAddress' => 'ama@example.com',
'split' => [
'subaccountCode' => 'SUB_4fT9kLm2Qx8vNp3R',
],
], "collect:{$order->id}");
Java
String payload = """
{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R"
}
}
""";
HttpResponse<String> response = new PayDirect().post("/transactions/collect", payload);
C#
var response = await new PayDirect().PostAsync("/transactions/collect", new
{
amountToSend = 150.75m,
sendingCurrency = "GHS",
receivingCurrency = "GHS",
invoiceOrAccountNumber = "order-10482",
description = "Order #10482",
customerAccountType = "E_WALLET",
customerEwalletType = "TELECEL",
customerAccountName = "Ama Serwaa",
customerAccountNumber = "0204000000",
customerEmailAddress = "ama@example.com",
split = new
{
subaccountCode = "SUB_4fT9kLm2Qx8vNp3R",
},
}, idempotencyKey: $"collect:{order.Id}");
The subaccount receives its subaccountSharePercent. For this one payment you can instead set
subaccountSharePercent to a different figure, or mainAccountFlatAmount - a flat amount you keep,
with the subaccount receiving the rest (Paystack's transaction_charge).
With a split group:
cURL
paydirect POST "/transactions/collect" '{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"splitCode": "SPL_8hQ2vB7cWz1kLm4N"
}
}'
Node.js
const { status, body } = await payDirect.post(
'/transactions/collect',
{
amountToSend: 150.75,
sendingCurrency: 'GHS',
receivingCurrency: 'GHS',
invoiceOrAccountNumber: 'order-10482',
description: 'Order #10482',
customerAccountType: 'E_WALLET',
customerEwalletType: 'TELECEL',
customerAccountName: 'Ama Serwaa',
customerAccountNumber: '0204000000',
customerEmailAddress: 'ama@example.com',
split: {
splitCode: 'SPL_8hQ2vB7cWz1kLm4N',
},
},
`collect:${order.id}`,
);
Python
status, body = post(
"/transactions/collect",
{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"splitCode": "SPL_8hQ2vB7cWz1kLm4N",
},
},
idempotency_key=f"collect:{order.id}",
)
PHP
<?php
$result = (new PayDirect())->post('/transactions/collect', [
'amountToSend' => 150.75,
'sendingCurrency' => 'GHS',
'receivingCurrency' => 'GHS',
'invoiceOrAccountNumber' => 'order-10482',
'description' => 'Order #10482',
'customerAccountType' => 'E_WALLET',
'customerEwalletType' => 'TELECEL',
'customerAccountName' => 'Ama Serwaa',
'customerAccountNumber' => '0204000000',
'customerEmailAddress' => 'ama@example.com',
'split' => [
'splitCode' => 'SPL_8hQ2vB7cWz1kLm4N',
],
], "collect:{$order->id}");
Java
String payload = """
{
"amountToSend": 150.75,
"sendingCurrency": "GHS",
"receivingCurrency": "GHS",
"invoiceOrAccountNumber": "order-10482",
"description": "Order #10482",
"customerAccountType": "E_WALLET",
"customerEwalletType": "TELECEL",
"customerAccountName": "Ama Serwaa",
"customerAccountNumber": "0204000000",
"customerEmailAddress": "ama@example.com",
"split": {
"splitCode": "SPL_8hQ2vB7cWz1kLm4N"
}
}
""";
HttpResponse<String> response = new PayDirect().post("/transactions/collect", payload);
C#
var response = await new PayDirect().PostAsync("/transactions/collect", new
{
amountToSend = 150.75m,
sendingCurrency = "GHS",
receivingCurrency = "GHS",
invoiceOrAccountNumber = "order-10482",
description = "Order #10482",
customerAccountType = "E_WALLET",
customerEwalletType = "TELECEL",
customerAccountName = "Ama Serwaa",
customerAccountNumber = "0204000000",
customerEmailAddress = "ama@example.com",
split = new
{
splitCode = "SPL_8hQ2vB7cWz1kLm4N",
},
}, idempotencyKey: $"collect:{order.Id}");
The split is checked against this payment's amount and charge before any money is collected - a subaccount that is not yours, is inactive or is in another currency, shares over 100%, or a bearer too small to absorb the charge are all rejected up front. It is then frozen with the transaction: editing the subaccount or split group afterwards never changes how an in-flight payment is credited. A payment link freezes its split when the link is generated.
Split groups
POST /splits
cURL
paydirect POST "/splits" '{
"name": "Marketplace orders",
"type": "PERCENTAGE",
"currency": "GHS",
"subaccounts": [
{
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R",
"share": 70
},
{
"subaccountCode": "SUB_9pQ1mN6vBx2kLc7T",
"share": 10
}
],
"bearerType": "ALL_PROPORTIONAL"
}'
Node.js
const { status, body } = await payDirect.post(
'/splits',
{
name: 'Marketplace orders',
type: 'PERCENTAGE',
currency: 'GHS',
subaccounts: [
{
subaccountCode: 'SUB_4fT9kLm2Qx8vNp3R',
share: 70,
},
{
subaccountCode: 'SUB_9pQ1mN6vBx2kLc7T',
share: 10,
},
],
bearerType: 'ALL_PROPORTIONAL',
},
);
Python
status, body = post(
"/splits",
{
"name": "Marketplace orders",
"type": "PERCENTAGE",
"currency": "GHS",
"subaccounts": [
{
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R",
"share": 70,
},
{
"subaccountCode": "SUB_9pQ1mN6vBx2kLc7T",
"share": 10,
},
],
"bearerType": "ALL_PROPORTIONAL",
},
)
PHP
<?php
$result = (new PayDirect())->post('/splits', [
'name' => 'Marketplace orders',
'type' => 'PERCENTAGE',
'currency' => 'GHS',
'subaccounts' => [
[
'subaccountCode' => 'SUB_4fT9kLm2Qx8vNp3R',
'share' => 70,
],
[
'subaccountCode' => 'SUB_9pQ1mN6vBx2kLc7T',
'share' => 10,
],
],
'bearerType' => 'ALL_PROPORTIONAL',
]);
Java
String payload = """
{
"name": "Marketplace orders",
"type": "PERCENTAGE",
"currency": "GHS",
"subaccounts": [
{
"subaccountCode": "SUB_4fT9kLm2Qx8vNp3R",
"share": 70
},
{
"subaccountCode": "SUB_9pQ1mN6vBx2kLc7T",
"share": 10
}
],
"bearerType": "ALL_PROPORTIONAL"
}
""";
HttpResponse<String> response = new PayDirect().post("/splits", payload);
C#
var response = await new PayDirect().PostAsync("/splits", new
{
name = "Marketplace orders",
type = "PERCENTAGE",
currency = "GHS",
subaccounts = new[]
{
new
{
subaccountCode = "SUB_4fT9kLm2Qx8vNp3R",
share = 70,
},
new
{
subaccountCode = "SUB_9pQ1mN6vBx2kLc7T",
share = 10,
},
},
bearerType = "ALL_PROPORTIONAL",
});
type: PERCENTAGE- eachshareis a percentage, and they must add up to 100 or less.type: FLAT- eachshareis an amount.- Whatever the subaccounts do not take is yours. In the example above you keep 20%.
Manage a group with GET /splits/{code}, PATCH /splits/{code} (including isActive),
PUT /splits/{code}/subaccounts to add a subaccount or change its share, and
DELETE /splits/{code}/subaccounts/{subaccountCode} to remove one.
Who absorbs the charge
When the PayDirect charge on a collection is taken out of the collected amount (the default for
collections - see Amounts and charges), someone's share has to absorb
it. You decide who, with bearerType on the split group or bearer on a single payment:
| Bearer | Who absorbs the charge |
|---|---|
ACCOUNT (default) |
You. Subaccounts receive their exact share. |
SUBACCOUNT |
One subaccount you name with bearerSubaccountCode. |
ALL |
You and every subaccount in the split, in equal parts. |
ALL_PROPORTIONAL |
Each party in proportion to its share. |
If the payer pays the charge on top of the amount instead, there is nothing to absorb and every party receives its full share.
If you charge your own commission on a split collection under a
RECIPIENT charge, it is absorbed exactly like the charge - by the same bearer - and each party's
feeShare covers its part of both. The commission itself is then credited to you separately, as
a COMMISSION_CREDIT, so with the ACCOUNT bearer you simply fund your own commission.
A worked example - GHS 100.00 collected, GHS 2.00 charge, split 70% / 10% with 20% to you:
| Bearer | You | Subaccount A (70%) | Subaccount B (10%) |
|---|---|---|---|
ACCOUNT |
18.00 | 70.00 | 10.00 |
SUBACCOUNT (A) |
20.00 | 68.00 | 10.00 |
ALL |
19.32 | 69.34 | 9.34 |
ALL_PROPORTIONAL |
19.60 | 68.60 | 9.80 |
Shares are calculated to the pesewa, rounding down, and any pesewa left over from rounding goes to
you - which is why ALL leaves you 19.32 rather than an even third.
Settlement
Each subaccount's balance is paid to its destination according to its settlementSchedule:
| Schedule | When it is paid |
|---|---|
AUTO (default) |
On every hourly settlement run |
DAILY |
At most once per UTC day |
WEEKLY |
At most once per week, from Monday |
MONTHLY |
At most once per calendar month |
MANUAL |
Only when you call POST /subaccounts/{code}/settle |
A settlement pays out the whole balance less the settlement charge, and creates a
SUBACCOUNT_SETTLEMENT transaction. You receive SUBACCOUNT_SETTLEMENT_SUCCEEDED or
SUBACCOUNT_SETTLEMENT_FAILED when it resolves - see Webhooks. A failed
settlement is returned to the subaccount's balance and, unless the schedule is MANUAL, tried
again on the next run. Very small balances wait until they are worth paying out.
POST /subaccounts/{code}/settle pays out immediately, for any schedule. Send an
Idempotency-Key, as for a withdrawal.
cURL
paydirect POST "/subaccounts/$SUBACCOUNT_CODE/settle" '{}'
Node.js
const { status, body } = await payDirect.post(
`/subaccounts/${subaccountCode}/settle`,
{},
`settle:${subaccountCode}:${runDate}`,
);
Python
status, body = post(
f"/subaccounts/{subaccount_code}/settle",
{},
idempotency_key=f"settle:{subaccount_code}:{run_date}",
)
PHP
<?php
$result = (new PayDirect())->post("/subaccounts/{$subaccountCode}/settle", [], "settle:{$subaccountCode}:{$runDate}");
Java
HttpResponse<String> response = new PayDirect().post("/subaccounts/" + subaccountCode + "/settle", "{}");
C#
var response = await new PayDirect().PostAsync($"/subaccounts/{subaccountCode}/settle", new { }, idempotencyKey: $"settle:{subaccountCode}:{runDate}");
A deactivated subaccount keeps its balance but is not paid out, and cannot be used in new splits, until you reactivate it.
Webhooks for split payments
The events for a split collection carry a split object: the subaccounts and shares it used, the
bearer, and an allocations list with each party's grossShare, feeShare and netCredited.
Settlement events carry the subaccountCode that was paid.
Refunds
There is no collection refund on the API yet, so a split collection cannot be refunded through the API either. Contact PayDirect if a split payment needs to be reversed.
Next
Rate limits - how many requests you can make, and what happens when you exceed it.