Subaccounts & Split Payments

Split a single payment across multiple bank accounts — marketplaces, commission models, revenue shares, vendor payouts. This is the complete field reference: every field states what it does, its units, whether it's required, and which other fields change its behaviour.

Concepts

The invariant that governs everything: the sum of all shares always equals the payment amount, exactly. No money is created or lost — rounding always lands on merchant-main.

TermMeaning
SubaccountA named beneficiary — a verified bank account under your merchant account.
Merchant-mainYour own account. It receives whatever the subaccounts don't, and it always pays the VAT.
SplitHow one payment's amount is divided between subaccounts and merchant-main.
ShareWhat one subaccount receives — either a percentage of the amount or a fixed sum.
Remainderamount − Σ subaccount shares. Goes to merchant-main by default.
Fee bearerWho absorbs ValuePay's processing fee inside the split.
Notes
  • All endpoints on this page use your secret API key (Authorization: Bearer sk_live_...). Checkout initialisation uses your public key instead — see the Authentication guide.
  • Never send merchantId in a request body. It's inferred from your API key and is rejected as an unknown field if sent.

Money Units

Units are not uniform across this API, and it's the single most common source of wrong splits. Check this table before writing any integration code.

WhereUnit₦30.00 is written as
amount at checkoutNaira, up to 2 decimals30.00
subValue on a subaccount (subType: "flat")Naira30
subValue on a subaccount (subType: "percent")Percent, 0–100, up to 2 decimals30 = 30%
share / shareValue (shareType: "percentage")Basis points, 0–100003000 = 30%
share / shareValue (shareType: "flat")Kobo3000 = ₦30.00
amount / fee on /splits/previewKobo3000 = ₦30.00
Money in responsesNaira numbers30.00
⚠️ Important
  • A subaccount's own subValue: 30 (percent) and a split entry's shareValue: 30 (30 basis points = 0.3%) look identical and mean wildly different things. Percentages inside a split are always basis points.

Response Conventions

Every response follows the same envelope, and every list endpoint wraps its results in a page object.

Success

{ "status": "Success", "data": { }, "message": "..." }

Failure — every 4xx and 5xx

{ "status": "Failed", "errors": [ { "message": "...", "error_code": "INVALID_INPUT", "code": 100000 } ] }

List response shape — GET /v1/subaccounts and GET /v1/splits

{
  "status": "Success",
  "data": {
    "results": [ /* the actual items */ ],
    "current_page": 1,
    "total_page": 1,
    "total_docs": 4,
    "has_next": false,
    "has_previous": false,
    "next_page": null,
    "previous_page": null
  },
  "message": "..."
}
Notes
  • Read data.results, not data. Treating data as the array is the most common first mistake against these endpoints — it yields nothing and looks like an empty account.
  • Single-resource reads (GET /v1/subaccounts/{id}, GET /v1/splits/{splitCode}) are not wrapped — data is the object itself.
  • Every endpoint validates the complete set of submitted keys (path params + query string + body). One unrecognised key fails the whole request — e.g. Extra fields submitted: merchantId, type. Seven params are accepted on every list endpoint without being listed: search, page, limit, from, to, sort_by, order.
  • 0 means genuinely zero. null means not known or not applicable. Render null as "—", never as "₦0.00" — format on != null, never on truthiness, or every legitimate zero renders as a bare currency sign.

Create a Subaccount

Registers a verified bank account as a payout beneficiary under your merchant account.

⚠️ Important
  • Subaccounts have no metaData field. The API accepts it without error and discards it silently — use description, or hold your own reference against the returned code. This is specific to subaccounts; the metaData you send when initialising a payment is a different field and is retained.
POST/v1/subaccounts
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Content-Type*StringSet value to application/json
Body Parameters
NameTypeDescription
accountNumber*String10–20 chars. Verified by NIBSS name enquiry on create — an unresolvable account is rejected.
bankCode*String2–6 chars. NIBSS bank code. Must pair with accountNumber to resolve, or create fails.
subType*String'percent' or 'flat'. Declares how subValue is read — changes its meaning and valid range.
subValue*NumberThis subaccount's default share, used only in direct-mode checkout. percent: >0 and ≤100, max 2dp. flat: ≥0 Naira, max 2dp.
businessNameString≤150 chars. Display label — does not override the NIBSS-verified account name, which is what gets paid.
descriptionString≤255 chars. Free text for your own records.
Request Body
{ "accountNumber": "0123456789", "bankCode": "000014", "subType": "percent", "subValue": 30, "businessName": "Vendor A Ltd", "description": "Marketplace vendor — electronics" }
Sample Response
{ "status": "Success", "data": { "id": "8a1ceac9-fe8a-4ee7-81e7-d3076c0655e7", "code": "sub_a1b2c3d", "accountNumber": "0123456789", "accountName": "VENDOR A LTD", "bankCode": "000014", "subType": "percent", "subValue": 30, "businessName": "Vendor A Ltd", "status": "Active" }, "message": "subaccount created" }
Notes
  • code format is sub_ + 7 lowercase base36 characters. Use it wherever a split references a subaccount — both code and id are accepted, but code is stable, shorter and safe to store.
  • accountName is the NIBSS-verified name, not your businessName — this is the name that appears on the beneficiary's statement.
  • Rejected subValue examples for percent: 0, 100.5, 30.555. Rejected for flat: negative values, 3+ decimals.

List Subaccounts

Retrieves your subaccounts.

GET/v1/subaccounts
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Query Parameters
NameTypeDescription
statusString'Active' or 'Inactive'. Closed subaccounts are 'Inactive'.
searchStringMatches name / account number.
pageIntegerPage number for pagination.
limitIntegerResults per page.
fromDatetimeISO 8601 date.
toDatetimeISO 8601 date. Rejected without 'from'.
sort_byStringField to sort by.
orderString'asc' or 'desc'.
Sample Response
{ "status": "Success", "data": { "results": [ { "id": "8a1ceac9-fe8a-4ee7-81e7-d3076c0655e7", "code": "sub_a1b2c3d", "accountName": "VENDOR A LTD", "subType": "percent", "subValue": 30, "status": "Active" } ], "current_page": 1, "total_page": 1, "total_docs": 1, "has_next": false, "has_previous": false, "next_page": null, "previous_page": null }, "message": "subaccounts fetched" }

Fetch a Subaccount

Retrieves a single subaccount.

⚠️ Important
  • Fetch, update and delete take the UUID id, not the sub_... code — the path is validated as a UUIDv4 and a code returns 400. The code is only for referencing a subaccount inside a split.
GET/v1/subaccounts/{id}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Path Parameters
NameTypeDescription
id*StringThe subaccount's UUID (not its sub_... code).

Update a Subaccount

Every create field is accepted, all optional, plus schedule.

⚠️ Important
  • This endpoint does not accept status at all — PUT ... { "status": "Active" } returns 400 Extra fields submitted: status. There is no reactivate; use schedule: manual to hold payouts instead of closing a subaccount.
PUT/v1/subaccounts/{id}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Content-Type*StringSet value to application/json
Path Parameters
NameTypeDescription
id*StringThe subaccount's UUID.
Body Parameters
NameTypeDescription
accountNumberStringSee Create a Subaccount.
bankCodeStringSee Create a Subaccount.
subTypeStringSee Create a Subaccount.
subValueNumberValidated against the stored subType if subType is omitted.
businessNameStringSee Create a Subaccount.
descriptionStringSee Create a Subaccount.
scheduleString'auto' or 'manual'. 'manual' means this subaccount never settles automatically — it waits for a human.
Notes
  • Sending subValue without subType keeps the stored subType, and subValue is validated against that — so changing a subaccount from percent 30 to flat 3000 requires sending both fields.

Delete a Subaccount

A soft close: the subaccount stops appearing in every read, can no longer receive a split, and its history is preserved.

⚠️ Important
  • Deletion is terminal. There is no reactivate endpoint, and a closed subaccount is rejected at checkout. To restore a beneficiary you deleted, create a new subaccount — you'll get a new sub_... code and id, and any split config that referenced the old one must be repointed.
DELETE/v1/subaccounts/{id}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Path Parameters
NameTypeDescription
id*StringThe subaccount's UUID.
Notes
  • Blocked while a live split config still references the subaccount — the error names the offending split codes. Delete those configs or point them elsewhere first.

Create a Split Configuration

A reusable, named split. Create it once, then reference it at checkout by splitId.

⚠️ Important
  • Not accepted — sending any of these returns 400: merchantId (comes from your API key), type (derived from your entries — all percentage → percentage, all flat → flat, mixed → hybrid), rules (removed along with the tiered and rule_based types, which were never implemented).
POST/v1/splits
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Content-Type*StringSet value to application/json
Body Parameters
NameTypeDescription
name*String1–256 chars. Display label.
entries*ArrayNon-empty. The subaccount shares — order is irrelevant.
entries[].subaccountId*Stringsub_... code or UUID. Must be an active subaccount you own. Duplicates across entries are rejected.
entries[].shareType*String'percentage' or 'flat'. Sets the unit of share. Entries may mix types freely within one config.
entries[].share*Integer≥0, integer only. percentage → basis points (3000 = 30%). flat → kobo (2000 = ₦20). Decimals are rejected.
bearerTypeString'main' (default) | 'subaccount' | 'all_equal' | 'all_proportional'. Who absorbs the processing fee — see Fees and VAT.
bearerSubaccountIdStringRequired when bearerType is 'subaccount', ignored otherwise. Must match one of the entries.
remainderPolicyString'main' (default) or 'named_subaccount'. Where the leftover goes.
remainderSubaccountIdStringRequired when remainderPolicy is 'named_subaccount', ignored otherwise. Must match one of the entries.
Request Body
{ "name": "Standard 70/30", "bearerType": "main", "remainderPolicy": "main", "entries": [ { "subaccountId": "sub_a1b2c3d", "shareType": "percentage", "share": 3000 }, { "subaccountId": "sub_e4f5g6h", "shareType": "flat", "share": 2000 } ] }
Sample Response
{ "status": "Success", "data": { "id": "split-cfg-test1", "splitCode": "SPLIT1", "name": "Standard 70/30", "currency": "NGN", "usage": "LIVE", "type": "hybrid", "bearerType": "main", "remainderPolicy": "main", "entries": [ { "subaccountId": "sub_a1b2c3d", "shareType": "percentage", "share": 3000 }, { "subaccountId": "sub_e4f5g6h", "shareType": "flat", "share": 2000 } ], "isActive": true }, "message": "split configuration created" }
Notes
  • splitCode is what you pass as splitId at checkout, and as {splitCode} in the URLs below.

The 100% Trap

Percentages must total under 100%, never exactly 100%. VAT is always taken from merchant-main, regardless of your split. A config whose percentages sum to 10000 bps leaves main with ₦0, and every charge against it then fails at checkout.

Checkout failure once main's remainder hits ₦0

Split is not feasible for amount 1000: Fee borne by main (324) exceeds its gross (0)
⚠️ Important
  • The single exception: a VAT-exempt merchant whose customer pays the fee owes main nothing, so a 100% split allocates cleanly. Don't design around this — it breaks the moment either condition changes.
Notes
  • Creation-time validation is structural only, so a 100%-allocated config saves successfully and only fails later, at payment time. Always leave at least the fee and VAT in main.
  • remainderPolicy: named_subaccount is not subject to this trap — main first retains exactly what it owes in fee and VAT, and the named subaccount receives the surplus. Main nets zero (the intent) rather than failing.

List Split Configurations

Returns config headers without entries — see Fetch a Split Configuration to read a config's entries.

⚠️ Important
  • A UI bound to entries on this response will always show empty — entries is only present on GET /v1/splits/{splitCode}.
GET/v1/splits
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Sample Response
{ "id": "split-cfg-test1", "splitCode": "SPLIT1", "name": "Standard 70/30", "currency": "NGN", "usage": "LIVE", "type": "hybrid", "bearerType": "main", "remainderPolicy": "main", "bearerSubaccountId": null, "remainderSubaccountId": null, "isActive": true, "deletedAt": null, "createdAt": "2026-07-19T23:32:49.886Z" }

Fetch a Split Configuration

The only endpoint that returns a config's entries.

GET/v1/splits/{splitCode}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Path Parameters
NameTypeDescription
splitCode*StringThe config's splitCode, e.g. SPLIT1.

Update a Split Configuration

Every field is optional.

PATCH/v1/splits/{splitCode}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Content-Type*StringSet value to application/json
Path Parameters
NameTypeDescription
splitCode*StringThe config's splitCode.
Body Parameters
NameTypeDescription
nameStringSee Create a Split Configuration.
entriesArrayReplaces the entry set wholesale — there is no per-entry patch. Omitting an existing entry deletes it.
bearerTypeStringSee Create a Split Configuration.
bearerSubaccountIdStringSee Create a Split Configuration.
remainderPolicyStringSee Create a Split Configuration.
remainderSubaccountIdStringSee Create a Split Configuration.
Notes
  • Validation runs against the merged result and type is re-derived.
  • New payments pick up a PATCH immediately. In-flight payments keep the split instruction that was frozen into their splitSnapshot at initialisation — see Reading a Split Result.

Delete a Split Configuration

Soft and terminal.

⚠️ Important
  • The config vanishes from every read and from checkout resolution in both LIVE and TEST. isActive is a separate flag reserved for a future reversible pause — a deleted config is gone, not "inactive".
DELETE/v1/splits/{splitCode}
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Path Parameters
NameTypeDescription
splitCode*StringThe config's splitCode.

Preview a Split

Dry-runs the allocation engine. No payment is created, nothing is charged. Use it to render a breakdown before the customer confirms, and to catch an infeasible config before checkout fails.

⚠️ Important
  • Preview does not model VAT — it takes amount and fee only, there's no vat parameter. Main's actual net on a real payment is lower than preview shows, by the VAT. See Reading a Split Result for the full response shape and its invariants.
POST/v1/splits/preview
Headers
NameTypeDescription
Authorization*StringSet value to Bearer SECRET_KEY
Content-Type*StringSet value to application/json
Body Parameters
NameTypeDescription
amount*IntegerKobo, ≥1. Note: kobo here, unlike amount at checkout, which is Naira.
feeIntegerKobo, ≥0. Omit to preview without a processing fee.
splitCodeStringA saved config to preview. Supply this or directive.
directiveObjectAn inline split (same shape as the checkout 'split' object) to preview without saving it.
Request Body
{ "splitCode": "SPLIT1", "amount": 100000, "fee": 280 }

Splitting a Payment at Checkout

Choose exactly one of three methods when calling POST /v1/checkouts/inline_initiation (public key) or POST /v1/transactions/initialize (secret key) — see the Transactions API for the rest of that endpoint's fields.

MethodFieldValueUse when
DirectsubaccountIda sub_... codeRouting to one subaccount at its own default share (its stored subValue).
SavedsplitIda splitCodeReusing a predefined multi-party split.
InlinesplitobjectDefining a one-off split for this payment only.

Direct

{ "amount": 1000, "subaccountId": "sub_a1b2c3d", "customer": { "email": "buyer@example.com" } }

Inline

"split": {
  "bearerType": "main",
  "remainderPolicy": "main",
  "entries": [
    { "subaccount": "sub_a1b2c3d", "shareType": "percentage", "shareValue": 3000 },
    { "subaccount": "sub_e4f5g6h", "shareType": "flat",       "shareValue": 2000 }
  ]
}
Saved config field (§ Create a Split Configuration)Inline 'split' field
entries[].subaccountIdentries[].subaccount
entries[].shareentries[].shareValue
bearerSubaccountIdbearerSubaccount
remainderSubaccountIdremainderSubaccount
⚠️ Important
  • Sending more than one of subaccountId, splitId, split returns 400 — "Provide only one of subaccountId, splitId, or split — they are mutually exclusive".
Body Parameters
NameTypeDescription
amount*Number≥50, max 2 decimals, Naira. The base every share is computed from.
chargesBearerStringWho pays the ValuePay processing fee — the customer or the merchant. When the merchant pays, bearerType (on the split) decides who inside the split absorbs it.
subaccountIdStringDirect mode — see table above. Mutually exclusive with splitId and split.
splitIdStringSaved mode — a splitCode. Mutually exclusive with subaccountId and split.
splitObjectInline mode — a one-off split object. Mutually exclusive with subaccountId and splitId. Uses different field names than a saved config — see table above.
Notes
  • Units and semantics inside split are identical to a saved config — bps for percentage, kobo for flat, integers only. Only the field names differ. Using a saved-config name inside split returns 400.
  • An empty string counts as absent, so "splitId": "" will not trip the exclusivity check.
  • Splits are NGN-only in practice.

Fees and VAT

Two independent settings decide the money out. They compose; neither overrides the other.

chargesBearer settingEffect
Customer paysThe customer is charged amount + fee. No split party is charged the fee.
Merchant paysThe fee is deducted from whoever bearerType names.
bearerType (only applies when the merchant pays)Who bears the processing fee
main (default)Merchant-main.
subaccountOne designated subaccount — requires bearerSubaccountId / bearerSubaccount.
all_equalSplit equally across the subaccounts.
all_proportionalSplit across subaccounts in proportion to their gross.
Case — ₦200 payment, fee ₦2.80, VAT ₦0.21, split: sub_a1b2c3d 30%, remainder → mainSubaccount receivesMain receivesWhy
Customer pays fee₦60.00₦139.79Customer paid the fee; only VAT comes off main (₦140 − ₦0.21).
Merchant pays, bearerType: main₦60.00₦136.99Fee and VAT both off main (₦140 − ₦2.80 − ₦0.21).
Merchant pays, bearerType: subaccount₦57.20₦139.79Fee off the chosen subaccount (₦60 − ₦2.80); VAT still off main.
Notes
  • VAT (7.5% of the fee) is deducted from merchant-main in every configuration — regardless of chargesBearer, regardless of bearerType, and it is never charged to a subaccount. It's 0 for VAT-exempt merchants.
  • platform exists in the bearerType enum but is not merchant-settable — it waives ValuePay's fee entirely and is an internal arrangement. Sending it returns 400.

Reading a Split Result

Everything here is available with your secret key on GET /v1/transactions/{transactionRef}. Four fields carry the split — all are null on a payment that carried no split.

FieldTypeMeaning
subaccountIdstring | nullSet in direct mode — the value exactly as you sent it, sub_... code or UUID.
splitIdstring | nullSet in saved mode — the splitCode you referenced.
chargesBearerstring | nullWho paid the processing fee on this payment.
splitSnapshotobject | nullThe frozen split instruction — see below.
⚠️ Important
  • splitSnapshot.entries[].subaccountId is always the canonical UUID, even when you posted a sub_... code — the resolver normalises it. Map by UUID, or keep your own code↔id lookup.
Notes
  • The snapshot is written at initialisation and never changes. Editing or deleting the split config afterwards does not alter it, and payout reads the snapshot rather than the live config — a config edit can never retroactively re-allocate a payment already in flight.

Split Preview Response Shape

The transaction only records the instruction, not the resulting Naira. To render a per-party breakdown, call POST /v1/splits/preview with the same amount and fee — it runs the same allocation engine as capture, so identical inputs give identical numbers. The response carries two objects: the snapshot that would be frozen, and the result of allocating it.

FieldTypeMeaning
distributions[].subaccountIdstringCanonical UUID, or the literal "MAIN" for your own account.
distributions[].isMainbooleantrue on the merchant-main row. Prefer this over string-matching "MAIN".
distributions[].shareTypestring'percentage' | 'flat' | 'remainder'. 'remainder' appears only on the row that absorbed the leftover — it's not a value you can send.
distributions[].grossAmountinteger (kobo)Allocated to this party, before its borne fee.
distributions[].feeAmountinteger (kobo)Fee borne by this party. 0 for every subaccount when bearerType is main.
distributions[].netAmountinteger (kobo)grossAmount − feeAmount — what is actually payable.
totalGrossinteger (kobo)Always equals the amount you sent.
totalFee / totalNetinteger (kobo)
Sample Response
{ "status": "Success", "data": { "snapshot": { "source": "config", "splitId": "SPLIT1", "bearerType": "main", "remainderPolicy": "main", "entries": [ /* ... */ ], "frozenAt": "2026-08-17T09:12:44.201Z" }, "result": { "distributions": [ { "subaccountId": "8a1ceac9-fe8a-4ee7-81e7-d3076c0655e7", "isMain": false, "shareType": "percentage", "shareValue": 3000, "grossAmount": 30000, "feeAmount": 0, "netAmount": 30000 }, { "subaccountId": "MAIN", "isMain": true, "shareType": "remainder", "shareValue": 0, "grossAmount": 70000, "feeAmount": 301, "netAmount": 69699 } ], "totalGross": 100000, "totalFee": 301, "totalNet": 99699 } } }
Notes
  • Every amount here is kobo, matching the amount you sent to preview — not Naira like checkout.
  • Invariants worth asserting in your own tests: totalGross === amount, and per row grossAmount − feeAmount === netAmount.

Finding the Payments That Used a Split

Both filters match the value stored on the transaction — the form you sent at checkout.

⚠️ Important
  • Settled amounts, subaccount balances and payout statements are not on the public API — they are dashboard features. Build reconciliation on this section and your own records; talk to your account manager for programmatic settlement data.
GET/v1/transactions
Query Parameters
NameTypeDescription
subaccountIdStringe.g. sub_a1b2c3d — matches transactions charged with that exact identifier form.
splitIdStringe.g. SPLIT1.
Notes
  • Filtering by UUID will not find payments you created with a sub_... code, or the reverse.
  • Combines with the usual status, from/to, page, limit, sort_by, order. Also available on GET /v1/transactions/exports/csv.

Scenarios

Every figure below is the actual output of the allocation engine for a ₦1,000 payment with a ValuePay fee of ₦3.01 and VAT of ₦0.23, merchant paying the fee, unless noted otherwise. A party's feeAmount of ₦3.24 is the fee plus VAT.

Marketplace — one vendor per order

Goal: the vendor keeps 90%, you keep 10% commission. Create the vendor as a subaccount with subType: percent, subValue: 90, then charge with subaccountId — no split config needed.

chargesBearerVendor getsYou getWhy
Customer pays fee₦900.00₦99.77Customer was charged ₦1,003.01; only VAT comes off you.
Merchant pays fee₦900.00₦96.76Fee and VAT off your remainder.

How far you can push the commission down — your remainder must cover ₦3.24 (fee + VAT), and the engine rejects the charge the moment it cannot. The ceiling moves with the payment size, because the fee does:

Vendor shareYour remainderResult
90%₦100.00fine — you net ₦96.76
99%₦10.00still fine — you net ₦6.76
99.7%₦3.00400 — Fee borne by main (324) exceeds its gross (300)
100%₦0.00400 — same error, gross 0
A flat platform fee instead of a percentage

Goal: you take ₦50 per order regardless of basket size. Take a flat entry of 5000 kobo to your own collection subaccount and send the remainder to the vendor with remainderPolicy: named_subaccount. The remainder subaccount must also appear in entries — add the vendor with shareType: "flat", "share": 0; the remainder is what actually pays them.

BasketYour ₦50 cutVendor (remainder)Merchant-main
₦1,000₦50.00₦946.76₦0.00
₦200₦50.00₦146.76₦0.00
₦60₦50.00₦6.76₦0.00

Your ₦50 arrives whole — flat means flat, and the fee does not touch it. Merchant-main retains exactly its ₦3.24 obligation out of the remainder and nets zero; it's the vendor, not your platform cut, that absorbs the cost of a small basket.

Multi-party — vendor, logistics, affiliate

One saved config with three entries and remainderPolicy: main, referenced by splitId on every charge. PATCH the percentages later and new payments pick the change up immediately, while in-flight ones keep their frozen snapshot.

PartyEntryGross
Vendorpercentage 6000 bps₦600.00
Logisticsflat 15000 kobo₦150.00
Affiliatepercentage 500 bps₦50.00
You (remainder)—₦196.76
Making a subaccount absorb the fee

Goal: the vendor, not you, carries ValuePay's cost. Set bearerType: subaccount with bearerSubaccountId, or spread it across every subaccount — all_proportional in proportion to gross, all_equal in equal parts. Using the multi-party split above:

bearerTypeVendor (60%)Logistics (₦150)Affiliate (5%)You
main (default)₦600.00₦150.00₦50.00₦196.76
subaccount — vendor₦596.99₦150.00₦50.00₦199.77
all_proportional₦597.73₦149.44₦49.82₦199.77
all_equal₦598.99₦149.00₦49.00₦199.77

VAT never moves — in every row it comes off you, which is why your column reads ₦199.77 and not ₦200.00 even when a subaccount bears the entire fee. Note what all_equal does to a small party: the affiliate on ₦50 pays the same ₦1.00 as the vendor on ₦600. Use all_proportional unless equal shares of the cost are genuinely what you want.

One-off splits

Goal: the parties differ on every order, so nothing is worth saving. Use the inline split object — no config is created and there's nothing to clean up. Feasibility is checked at initialize, not at settlement, against the real amount plus the estimated fee and VAT. An impossible split fails immediately and no payment is created — see Validation Reference for every rejection reason.

Holding one party's money back

Goal: pause payouts to a subaccount without stopping its sales. PUT /v1/subaccounts/{id} with schedule: manual — the subaccount keeps accruing on every payment; it simply never settles automatically, and release becomes a manual step. Allocation, the split itself and the customer experience are unchanged, only payout timing moves.

Validation Reference

Everything that returns 400. The last two rows are checked at payment time, not at config creation — a structurally valid config can still be infeasible for a given amount. Use /v1/splits/preview to catch it early, remembering that preview omits VAT.

ConditionMessage
Unknown field anywhere in the requestExtra fields submitted: ...
More than one split methodProvide only one of subaccountId, splitId, or split ...
Amount below the floorAmount must be at least 50
More than 2 decimals on amountAmount can only have up to two decimal places
Non-integer share / shareValueentry share must be a non-negative integer (bps or kobo)
Percentages summing above 100%percentage shares exceed 100% (11000 bps)
Duplicate subaccount in one splitduplicate subaccount in split: <id>
bearerType: subaccount whose bearer is not an entrybearerSubaccountId must reference an entry when bearerType=subaccount
remainderPolicy: named_subaccount whose subaccount is not an entryremainderSubaccountId must reference an entry when remainderPolicy=named_subaccount
bearerType: platformInvalid bearerType
Empty entriesentries must be a non-empty array (route) / split requires at least one entry (engine)
'to' without 'from' on a list'to' cannot be provided without 'from'
Unknown subaccount at checkoutsubaccount ... not found
Closed subaccount referenced at checkoutsubaccount ... is closed and cannot receive splits
Subaccount with no share configuredsubaccount ... is not configured for splits (set feeType/feeValue)
Deleting a subaccount a live split config still usessubaccount is referenced by split configuration(s) ... — delete them before deleting this subaccount
status sent to PUT /v1/subaccounts/{id}Extra fields submitted: status — there is no reactivate
Flat shares exceeding the amountSplit is not feasible for amount ...: flat shares (200000) exceed amount (100000)
Main's remainder cannot cover fee + VATSplit is not feasible for amount ...: Fee borne by main (324) exceeds its gross (300)

Refunds

A refund is borne by the merchant: on refund you are debited the full refunded amount including the processing fee. ValuePay does not return its fee — standard across the industry.

Integration Checklist

Before you ship a split-payments integration, confirm each of these.

Notes
  • Units correct everywhere — bps in splits, Naira at checkout, kobo in preview
  • Not sending merchantId, type or rules to the split config endpoints
  • Percentages total under 100%, leaving room for VAT
  • Exactly one split method per payment
  • Inline field names, not saved-config names, inside split
  • Fetching a config by {splitCode} to get entries — the list omits them
  • /splits/preview before charging, to surface infeasible splits early
  • Mapping splitSnapshot.entries[].subaccountId by UUID — it's never the sub_... code you sent
  • Filtering /v1/transactions with the same identifier form you charged with
  • Not depending on settled amounts or balances from the public API — they're dashboard-only
  • Handling deletion as terminal; using schedule: manual when you only mean "pause"