Skip to content
PayDirect Docs

Docs › Core concepts

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

  1. Register a subaccount for each payee, with the bank account or mobile money wallet their share is paid to.
  2. Split a collection by adding a split object to POST /transactions/collect, POST /transactions/pay-bill-ext or POST /checkout/generate-payment-link.
  3. When the payment is confirmed, each party's share is credited: yours to your ledger balance, each subaccount's to its own balance.
  4. 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

text
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",
});
  • accountType is BANK_ACCOUNT (with swiftCode, the GHIPSS routing code from POST /transactions/banks/list) or E_WALLET (with ewalletType).
  • subaccountSharePercent is 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's percentage_charge is 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

text
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 - each share is a percentage, and they must add up to 100 or less.
  • type: FLAT - each share is 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.