Skip to main content
PaymentsDeprecatedBreaking
Virtual accounts are deprecated; inflows must now come in as a direct escrow payment

Virtual accounts are no longer issued

POST /v1/payment/virtual-account no longer issues an account. Every call now returns 410 Gone, whatever the customer’s country, active gateway or BVN status.CBN rules require every inflow to arrive as a direct escrow payment. Money paid into a virtual account lands in a wallet with no escrow behind it and nothing recording what it was for, so the account cannot be offered.

What to use instead

  1. Create an escrow with POST /v1/escrow/create.
  2. Collect payment for it with the Payluk Checkout SDK, or server-side with POST /v1/payment/create-intent.
  3. Confirm it with POST /v1/payment/verify.
The Checkout SDK opens an escrow first and settles the payment against it, so the money is attached to a transaction from the moment it arrives.
The path is kept and answers 410 rather than 404 on purpose: an integration still calling it is told what happened and what to use instead, where a 404 would read as a bad path and send you looking for a typo.
CryptoSandboxFixed
Test-mode crypto payments now run on BSC testnet instead of mainnet

Sandbox crypto no longer touches mainnet

Crypto payments previously resolved one blockchain configuration for every request, whatever environment the request was in. A payment made with a test key therefore settled on BSC mainnet, in real cNGN, from the same master wallet as live traffic. Test and live crypto are now fully separated.
  • A request in the test environment runs on BSC testnet (chain 97), against a separate cNGN contract and a separate pair of master wallets.
  • A request in the live environment is unchanged: BSC mainnet, chain 56, the same contract and wallets as before.
  • The network, networkId and explorerUrl fields on GET /v1/payment/get-master-wallet-address now describe the chain the address is actually on, so a sandbox address links to testnet.bscscan.com rather than to bscscan.com. The explorerUrl recorded on blockchain payments follows the same rule.
Sandbox crypto is refused until your Payluk deployment has a testnet contract and wallets configured; there is deliberately no fall back to the mainnet ones. If a test-mode crypto call returns a configuration error, that is why.

Fixed

  • Deposit and withdrawal master wallets are no longer interchangeable. Whichever wallet a process resolved first was reused for both roles, so a payout could be signed by the deposit wallet.
  • fromAddress on a blockchain payment intent is now the wallet address. It was previously recorded before the address had resolved, leaving the field empty on the stored payment.
Merchant AccountPaymentsNew
Read your own balance and ledger, and set a page size on transaction history

Your merchant account

Every endpoint so far acted on behalf of one of your customers. Two new ones answer for your own merchant account, so you no longer have to infer your earnings from your customers’ activity.
  • GET /v1/merchant/balance returns mainBalance and escrowBalance on your own merchant wallet. This is where Payluk settles your earnings.
  • GET /v1/merchant/transactions returns your own ledger: commission for your share of an escrow fee, delivery for a delivery fee you charged a buyer, and any withdrawals you made. It takes the same page, limit, type, reference, fromDate and toDate filters as the customer history.
  • Both require a merchant super-admin key, and both refuse a customer-id header. Sending one returns 400 rather than quietly answering for that customer, so you can never mistake a customer’s balance for your own.
  • Your customers’ balances are unaffected and are never counted in yours. Read those from Get customer wallet and Get payment history as before.
Both figures also arrive as webhooks the moment they settle, on the payment.commission.success and payment.delivery.success events added on August 21. Treat the webhook as the trigger and these endpoints as the source of truth, rather than polling for a balance change.

Page size on transaction history

GET /v1/payment/history and GET /v1/merchant/transactions now accept a limit query parameter.
  • limit sets the transactions per page, between 1 and 100.
  • The default is unchanged at 10, so existing calls return exactly what they returned before. Nothing on your side needs to change.
  • A value above 100 is capped at 100. An unusable value falls back to 10, so a malformed request still returns a sensible page rather than an error or an empty result.
  • limit applies to the paginated response only. Supplying reference, fromDate or toDate still returns a flat array, which is not paged.
See Merchant account and Pagination.
Merchant CustomersFixed
Phone numbers accept any spelling, and countryId is now optional

Phone numbers and countryId

Creating a customer rejected every phone number with Unable to validate phone number, whatever format you sent. That message was wrong: the failure was in resolving countryId, not in the phone number, and the request never reached a phone rule at all. It is fixed, along with the request example in these docs, which showed a + that the API has never accepted.
  • Send phone as digits only, with no leading +. Local and international spellings of the same number are all accepted: 08012345678, 2348012345678 and 8012345678 are the same customer. Each is stored in the local form (08012345678), which is what the customer endpoints return.
  • countryId is now optional. Omit it and the customer inherits the country on your merchant account. It also accepts an ISO code, so NG works as well as an id from Get countries.
  • A country that cannot be resolved now says so, with Invalid country selected instead of blaming the phone number.
  • Phone lookups match any spelling. GET /v1/customers?phone= and GET /v1/payment/verify-phone/{phone} find the same customer whether you send 08012345678 or 2348012345678, so you no longer have to remember the format you onboarded them with. This replaces the exact-match behaviour described below on August 15. Only the country prefix is interchangeable: a different subscriber number is still a different customer.
  • Customers created before this change keep the number as it was stored. Lookups find them either way, so nothing on your side needs to change.
See Merchant customers.
WebhooksPaymentsNew
Webhooks now cover every transaction your customers settle

Transaction webhooks

Your callback URL only ever received escrow updates, so a customer funding their wallet or taking a payout was invisible until you polled for it. Every transaction a merchant customer settles now reaches the same endpoint, on a new payment.* stream.
  • Deposits, bank transfers into a reserved account, withdrawals, wallet-to-wallet sends and escrow fundings all emit an event when they settle. The event name is payment.<transactionType>.<outcome>, for example payment.deposit.success or payment.withdrawal.failed.
  • The two payouts Payluk makes into your own merchant wallet are on the same stream: payment.commission.success for your share of an escrow fee, and payment.delivery.success for an additionalFee you charged the buyer.
  • The payload does not name the bank or processor behind a transaction. Which rail Payluk settles through is not part of this contract, so nothing in your handler should branch on it.
  • Failures are reported too, so a declined payout no longer looks like a payout still in flight.
  • Nothing is sent while a transaction is pending, and each transaction announces itself once. De-duplicate on data.reference.
  • The envelope, the x-payluk-signature HMAC-SHA512 header, the callback URL and the retry behaviour are unchanged and shared with escrow.* events. Switch on the event field and return 2xx for names you do not handle.
  • Funding an escrow now produces two webhooks: payment.escrow.success for the ledger entry and escrow.ongoing for the deal. Act on one stream, not both.
  • Existing escrow integrations need no changes.
See Webhooks.
PaymentsTestingNew
Pay a staging virtual account from the Payluk Test Bank

Test a bank transfer end to end on staging

Virtual accounts on staging were issued by a sandbox bank with no way to send money into them, so a deposit could be generated but never paid. The Payluk Test Bank closes that gap: it issues the account and gives you a funded float to pay it from.
  • On staging, Generate virtual account now returns an account you can actually pay. Simulated accounts read Payluk Test Bank in bank, carry the bank code 999999, and their account numbers begin with 99.
  • Pay one at https://staging.live.payluk.ng/v1/checkout/test-bank. Paste the account number, confirm the resolved name and the expected amount, and send.
  • Paying runs the same settlement your live traffic runs: the same credit notification, the same verification, the same wallet credit. Read the balance back with Get customer wallet, or find the deposit in Get payment history as a transfer.
  • The float carries three source accounts (₦1,000,000, ₦250,000, and ₦100), so an insufficient-funds rejection is one click away. Reset balances restores them.
  • Nothing changes on production, where accounts are issued by the real bank and every test bank route returns 404.
See Simulate bank transfers with the Payluk Test Bank.
PaymentsChanged
Dedicated accounts are issued only under Paystack

A dedicated virtual account now depends on the active gateway

Generate virtual account: a dedicated account is a Paystack product, so one is now only issued while Paystack is the active gateway. Previously any customer with a BVN on file was routed to Paystack regardless of which gateway was collecting.
  • Under Safe Haven, every merchant customer now receives the temporary account, whether or not they have a BVN. Safe Haven opens a reserved account only against an identity it verified itself, which requires an OTP sent to the phone behind the BVN. A server-side integration has no way to collect that, so the BVN could never be verified through this API and the old routing produced an account the gateway could not settle.
  • Under Paystack nothing changes: a customer with a BVN still gets a dedicated account, one without still gets a temporary account.
  • Check dedicated in the response rather than inferring the account type from whether you sent a BVN. Dedicated accounts have no expiresIn; temporary ones expire after 24 hours and must be re-read.
CustomersNew
Look customers up by email or phone

Find a customer without their ID

List merchant customers: GET /v1/customers accepts email and phone. Both are unique among your customers, so either one identifies a single record — useful when a support ticket or a webhook gives you an address or a number but not the customerId the rest of the API wants.
  • The response keeps the same shape as the plain listing: the customer is the only row in data, under a one-page pagination. Nothing needs to special-case the lookup.
  • email is case-insensitive. phone is matched exactly as it was stored when the customer was created, so send it in the format you onboarded them with — 08012345678 will not find a customer saved as +2348012345678. URL-encode a leading + as %2B.
    Superseded on August 23, 2026: phone lookups now match any spelling of the same number.
  • Sending both narrows to the customer matching both, not to two results.
  • If nothing matches, the request fails with 400 and No customer found with the provided email (or phone, or email and phone) rather than returning an empty list. Treat that as “no such customer”, not as an outage.
  • page and limit are ignored on a lookup, and the 50-per-page ceiling does not apply to it.
  • Fully additive. Omit both and the endpoint pages through your customers exactly as before.
DisputesNew
Split dispute resolution

Split a disputed escrow between the seller and the buyer

Resolve dispute: POST /v1/escrow/dispute/resolve/{escrowId} accepts a third outcome, status: SPLIT, for the cases where neither side is wholly right. Send sellerAmount and buyerAmount and the held funds are divided between the two in one pass, instead of all of it going to one party.
  • The two amounts must add up to exactly what the escrow still holds, which is not always its amount. A standard escrow holds amount less the seller’s share of the escrow fee, so a buyer-paid escrow holds the full amount and a whoPays: both one holds slightly less. A milestone escrow holds only the milestones the buyer never confirmed. Amounts that do not add up are rejected and no money moves. Up to two decimal places are accepted, because the held pool can end in a half unit.
  • The escrow closes with the new status SPLIT and carries a split object (sellerAmount, buyerAmount, pool, resolvedAt) on every later read. A new escrow.split webhook fires, with the same object in its payload. On a milestone escrow, each milestone that was still held is marked SPLIT too.
  • The escrow fee is retained in full, exactly as on a refund, and commission is charged once, exactly as on a completion. additionalFeeRefundable works on a split as it does on a refund, so you still choose who keeps the delivery fee.
  • Vault escrows cannot be split: they are pooled stakes with no buyer and seller to divide between, and are settled by declaring a winner.
  • Fully additive. COMPLETED and REFUNDED are unchanged, sellerAmount and buyerAmount are rejected on those, and existing integrations are unaffected. Handlers that switch on escrow status should add a SPLIT case.
EscrowNew
Additional (delivery) fee

Charge a delivery fee on a standard escrow

Update additional fee: PUT /v1/escrow/additional-fee/{paymentToken} sets your own charge on top of one of your standard escrows: delivery, handling, anything the escrow amount itself shouldn’t include. Every escrow starts at additionalFee: 0.
  • The buyer is charged amount + their fee share + additionalFee. Send that full total as amount on Pay escrow, or the request is rejected with Amount mismatch. The hosted checkout picks it up automatically and shows it as its own line.
  • You receive it in full when the escrow completes (Payluk takes no cut), credited to your merchant wallet as a new delivery transaction type, kept separate from your commission. The escrow amount, the escrow fee and the seller’s payout are all unchanged by it.
  • Settable only while the escrow is PENDING, and only by you: super-admin key, no customer-id header. Adjust it as often as you need before the buyer pays, or send 0 to remove it. Once paid, it is frozen.
  • If a dispute is later resolved in the buyer’s favour, you decide who keeps the charge when you resolve it: Resolve dispute now accepts additionalFeeRefundable. true returns it to the buyer with the principal; false credits it to you because the delivery cost was already incurred. Your choice is saved on the escrow. Omit it and the escrow’s existing setting (true by default) applies, so nothing changes for existing integrations.
  • Standard escrows only; milestone and vault escrows are rejected.
CustomersNew
Update customer

Edit a customer’s profile

Update customer: PUT /v1/customer/update/{customerId} lets you correct a customer’s firstname, lastname or email after onboarding. Send any one of them, and the fields you omit are left as they are.
  • phone and bvn stay read-only here; both carry verification state and change through their own flows.
  • Email remains unique per merchant, so reusing an address another of your customers already holds is rejected with a validation error.
  • The customer is resolved scoped to your merchant account, so an ID belonging to another merchant is denied. Super-admin key required.
EscrowNew
Vault escrow

Vault escrow: merchant-only pooled stakes

A new escrow type for competition-style products: two or more of your customers each stake an amount into one pot, and you declare the winner, who receives the whole pot minus the platform fee.
  • New settlement type VAULT, alongside STANDARD and MILESTONE.
  • Four merchant-only endpoints under /v1/escrow/vault/*: create, declare winner, cancel, and get.
  • Vaults are created already funded: every stake is verified against the participant’s main balance and locked into their own escrow balance in one all-or-nothing call.
  • Tournament mode: any number of participants from 2 up.
  • The platform fee is collected whether the vault completes or is cancelled: cancellation refunds each stake minus its pro-rata fee share.
  • Escrow responses now carry participants[] and winnerId fields (populated for vault escrows only).
See Vault escrows for the full concept.

Milestone due dates must be in the future

dueDate on milestone creation, editing, and conversion is now rejected unless it parses to a future date.
Escrow
Milestone endpoints on the merchant API

Milestone escrow endpoints for merchants

The milestone escrow surface is now available on the merchant (/v1) API:
Escrow
Sequential milestone release

Milestones now release in order

A buyer can only confirm the earliest still-pending milestone; milestone 3 cannot be released before 1 and 2. Dispute-driven settlement (release-remaining or refund) still settles every remaining milestone at once.
Payments
Dedicated virtual accounts

Dedicated virtual accounts for Nigerian customers

Generate virtual account now issues a permanent, dedicated virtual account for merchant customers who supplied a BVN. Customers without a BVN keep receiving 24-hour temporary accounts. Virtual accounts remain Nigerian-only.
EscrowNew
Milestone escrows introduced

Milestone escrows

Escrows can now hold the full amount upfront and release it in parts as the buyer confirms each milestone; see Milestone escrows.
  • New settlement type MILESTONE; escrow responses carry a milestones[] array.
  • At least 2 milestones, positive integer amounts summing to the escrow amount, and whoPays: buyer only.
  • Each milestone may name an optional customerId beneficiary who receives that milestone’s funds instead of the seller.
  • The escrow completes automatically when the final milestone is released.
PaymentsDisputes
Payments & disputes improvements

Batch payment verification and dispute lookup

  • Verify payment accepts multiple references in one call, and escrow payment intents support multiple escrow IDs, so a buyer can pay several escrows at once.
  • New endpoint: Get dispute by escrow ID.
  • Escrow listings accept a status filter.