Skip to main content
FluxanDeveloper docs
简体中文

Disbursements

Pay third-party payees from your PKR settleable balance through the API. Setup, funds and payee rules, idempotency, statuses, webhooks, testing and go-live.

Last updated 2026-11-02 · API contract v1.0.0

What a disbursement is

A disbursement pays a third-party payee you choose from your settleable balance in one currency, on your instruction. It isn't the same as a payout:

Disbursement (this page)Payout
Who starts itYou: through the API, or one by one or in batches in the merchant portalThe platform: it generates settlements on your settlement cycle and pays them out after review
Who gets the moneyA third-party payee you choose: an EasyPaisa or JazzCash wallet, or a bank account in PakistanYour own registered payout account
Source of fundsYour settleable balance in that currencyThe settleable balance reserved by a settlement
Object and endpointsDisbursement (dsb_), POST / GET /v1/disbursementsSettlement and Payout (po_), read-only

Disbursements don't go through manual review: once a request passes the acceptance checks, it's submitted. A disbursement isn't a withdrawal either. The platform has no self-service withdrawal of arbitrary amounts to your own account; your own funds reach you only through settlements and payouts. For now only PKR is supported, and there are no cross-currency disbursements. The platform doesn't store payees, so every disbursement carries the full payee details. Disbursements can't be cancelled.

How a disbursement runs
How a disbursement runs

Setup and prerequisites

PrerequisiteWho does itIf it's missing
Disbursements capability, enabled separately for test and live, off by defaultThe platform (contact your platform account manager)403 capability_disabled
A PKR disbursement channel and disbursement limits (minimum and maximum per disbursement, daily total amount, daily count)The platform422 currency_not_supported
Disbursement pricing (percentage plus fixed fee) in your pricing versionThe platform, as agreed in your contract422 currency_not_supported; once set, each disbursement's fee is in the response fee
Enough PKR settleable balanceComes from your PKR collections after the lock period (see Funds rules)422 insufficient_balance
API keys: a key with disbursements:write to create disbursements, disbursements:read to only query them; finance:read as well to check the settleable balance and balance transactionsAn Owner or Admin creates them (live keys need step-up verification). A Developer can only choose scopes within their "grantable API scopes", which don't include the disbursement scopes by default403 forbidden
Webhooks: subscribe your test and live endpoints to the 4 disbursement.* eventsYouNo outcome notifications; you can only query

Funds rules

WhenSettleable (available)Reserved for disbursements (disbursement_pending)Notes
Accepted (201)Minus amount + feePlus amount + feeIf it falls short, 422 insufficient_balance; error.details.shortfall is the shortfall and nothing is created
SucceededIf less than the amount was paid, plus amount − paid_amountMinus amount + feeFinal deduction; the fee is charged
FailedPlus amount + feeMinus amount + feeNo fee is charged
ReversedPlus paid_amount + fee—The payee didn't get the money; the original success record is kept
  • Example: your settleable balance is PKR 1,000.00 and you create a disbursement of 300.00 with a fee of 6.00. After acceptance, settleable is 694.00 and reserved for disbursements is 306.00. On success, the reserved amount drops to zero. If it fails, settleable goes back to 1,000.00.
  • Money returned to the settleable balance (failure, reversal, the shortfall of a partial payment) is available right away, without another lock period.
  • To check: available and disbursement_pending in GET /v1/balances. In GET /v1/balance-transactions, disbursement entries have source_type disbursement, and the posting types are disbursement_reserved (held), disbursement_succeeded (deducted on success, including any shortfall returned), reservation_released (returned on failure) and disbursement_reversed (returned on reversal). Both endpoints need finance:read.

Note: Where the settleable balance comes from

Collected funds first go into pending settlement. They move to settleable only after both the T+N days of your settlement terms and the minimum lock hours (counted from when the payment succeeded) have passed.

Create a disbursement

POST/v1/disbursements requires an Idempotency-Key and the scope disbursements:write. The request has no merchant ID, fee or channel fields: the API key alone determines the merchant and environment, and the platform calculates the fee.

FieldTypeRequiredRules
merchant_disbursement_nostringYesYour merchant disbursement ID: 1–64 letters, digits, dots, underscores, colons or hyphens. Unique within the merchant and environment, case-sensitive. Generate and save it before sending
amountintegerYesIn the smallest currency unit. PKR amounts must be multiples of 100: 30000 (300.00) is fine, 30050 isn't
currencystringYesThree uppercase letters; only PKR for now
methodstringYeseasypaisa, jazzcash or bank
payee.accountstringYesPayee account; see Payee rules. Responses only return the last 4 digits
payee.namestringYesPayee account name, 2–64 characters; must match the real name on the account
payee.bank_codestringRequired when method=bankBank code; not allowed for other methods
customer_ipstringNoThe IP of the payee (your user) when they submitted this payout request in your system. IPv4 or IPv6, up to 45 characters. Resolve it through your trusted proxy chain; don't send your server's IP. Used for risk checks
descriptionstringNoUp to 256 characters, for your own reconciliation
metadataobjectNoSame as Payment: up to 20 keys, keys up to 40 characters, values strings up to 200 characters

For every field and constraint, see API reference: Create a disbursement. Replace the <TEST_SERVER_API_KEY> placeholder with a test key that has disbursements:write. The fee in the examples is for illustration only; your pricing decides the real fee.

curl
curl https://api.fluxan.example/v1/disbursements \
  -H "Authorization: Bearer <TEST_SERVER_API_KEY>" \
  -H "Idempotency-Key: disbursement.create:DSB-DEMO-001:1" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_disbursement_no": "DSB-DEMO-001",
    "amount": 30000,
    "currency": "PKR",
    "method": "easypaisa",
    "payee": { "account": "03001234567", "name": "Test Payee" },
    "customer_ip": "203.0.113.10",
    "description": "Example disbursement",
    "metadata": { "customer_ref": "CUSTOMER-DEMO-001" }
  }'

Placeholders to replace

  • <TEST_SERVER_API_KEY>: Test-environment server API key, created on the "API Key" page of the merchant portal

The 201 response (a replay with the same key returns 200 and the latest state of the same disbursement):

201 response
{
  "id": "dsb_demo_001",
  "version": 1,
  "merchant_disbursement_no": "DSB-DEMO-001",
  "environment": "test",
  "amount": 30000,
  "currency": "PKR",
  "fee": 600,
  "method": "easypaisa",
  "payee": {
    "account_last4": "4567",
    "name": "Test Payee",
    "bank_code": null,
    "bank_name": null,
    "utr": null
  },
  "customer_ip": "203.0.113.10",
  "status": "processing",
  "resolution_status": "known",
  "failure": null,
  "paid_amount": null,
  "succeeded_at": null,
  "failed_at": null,
  "reversed_at": null,
  "description": "Example disbursement",
  "metadata": { "customer_ref": "CUSTOMER-DEMO-001" },
  "source": "api",
  "batch_id": null,
  "created_at": "2026-10-29T08:00:00Z",
  "updated_at": "2026-10-29T08:00:00Z",
  "request_id": "req_demo_101"
}

Caution: HTTP 2xx only means accepted

HTTP 2xx only means the platform accepted the disbursement. Whether the money was paid depends on status or a verified disbursement.* event.

For a bank account, the request body differs like this: "method": "bank", "payee": { "account": "012345678901", "name": "Test Payee", "bank_code": "MEEZAN" }.

Disbursement object

FieldDescription
id, environmentDisbursement ID (prefix dsb_); environment test or live
versionObject version, increased on every effective change. Compare it when you handle events; don't order by status name
merchant_disbursement_no, amount, currency, method, payee.name, payee.bank_code, customer_ip, description, metadataSame as in the request. bank_code is null when the method isn't bank; customer_ip is null for disbursements made in the merchant portal
feeDisbursement fee, calculated and fixed at acceptance from the pricing version in force at the time; later pricing changes don't affect it
payee.account_last4, payee.bank_name, payee.utrLast 4 digits of the payee account (the full account never appears in any response or event); the payee's bank name as reported by the disbursement channel, may be null; the reference number from the payee's wallet or bank, set only after success, which the payee can use to trace the money
status, resolution_statusSee Status and outcomes
failure{ code, message } when status=failed, otherwise null
paid_amountAmount actually paid, set only after success. It may be less than amount; the difference is already back in the settleable balance
succeeded_at, failed_at, reversed_at, created_at, updated_atWhen it entered each status, when it was accepted, and when it last changed. After a reversal, succeeded_at, paid_amount and payee.utr are kept
source, batch_idSource: api for the API, portal for one-off disbursements in the merchant portal, batch for portal batches. batch_id is the batch ID (prefix dsbb_), set only for batches

For the type and nullability of every field, see API reference: Disbursement object.

Query disbursements

GET/v1/disbursements/{disbursement_id} retrieves one by disbursement ID, and GET/v1/disbursements lists them by filter. Both need disbursements:read.

curl
# Query by disbursement ID (scope disbursements:read)
curl https://api.fluxan.example/v1/disbursements/dsb_demo_001 \
  -H "Authorization: Bearer <TEST_SERVER_API_KEY>"

# Query by merchant disbursement ID: after a timeout, check whether it was accepted
curl "https://api.fluxan.example/v1/disbursements?merchant_disbursement_no=DSB-DEMO-001" \
  -H "Authorization: Bearer <TEST_SERVER_API_KEY>"

# Reconciliation: disbursements from one day that are still processing
curl "https://api.fluxan.example/v1/disbursements?status=processing&currency=PKR&created_gte=2026-10-29T00:00:00Z&created_lt=2026-10-30T00:00:00Z&limit=100" \
  -H "Authorization: Bearer <TEST_SERVER_API_KEY>"

Placeholders to replace

  • <TEST_SERVER_API_KEY>: Test-environment server API key, created on the "API Key" page of the merchant portal
  • List filters: status, currency, created_gte (inclusive), created_lt (exclusive), merchant_disbursement_no (exact match, case-sensitive) and batch_id. limit defaults to 20, up to 100. For cursor, pass the previous page's next_cursor. Results are sorted by creation time, newest first, then by id descending for the same time (same as pagination). The response has data, has_more, next_cursor and request_id.
  • Only disbursements of the current merchant and environment are returned. A disbursement ID from another merchant or environment returns 404, without revealing whether it exists. Requests rejected at acceptance have no disbursement record, so you won't find them.
  • Disbursements made in the merchant portal, one by one or in batches, show up too (see source): View disbursements (opens the merchant portal in a new tab) (Test environment; Requires Owner, Admin, Finance or Developer).

Payee rules

Methodpayee.account ruleExample
easypaisa, jazzcash11 digits starting with 03; the API doesn't normalize anything03001234567
bank (local account)9–24 letters and digits after removing spaces and converting to uppercase012345678901
bank (IBAN)Starting with PK, it's checked as an IBAN: 24 characters with a valid mod 97 check digitPK29HABB0001234567890123
  • If a rule fails, the API returns 400 invalid_request with field set to payee.account, payee.name or payee.bank_code.
  • Normalize wallet numbers in your system before you submit them: remove spaces and hyphens; replace a leading +92 or 92 followed by 10 digits with 0 and those 10 digits; add a leading 0 to a 10-digit number starting with 3 (a spreadsheet dropped the leading zero). The merchant portal forms and batch files normalize automatically; the API doesn't.
  • payee.name can't contain 13–19 consecutive digits (card-number-like). If the name doesn't match the real name on the account, the disbursement is accepted and then fails (payee_account_invalid). The platform doesn't run card-number (Luhn) checks on payee accounts; an account with a valid format that doesn't exist is also accepted and then fails.
  • The full payee account appears only in the create request. After that, API responses, events, the merchant portal and exports show only the last 4 digits. Don't record the full account in your logs, error reports or tickets either.

Warning: A disbursement can't be pulled back once paid

The platform pays the account you submit. If the account number is wrong but the name happens to match, the money goes into that account. Check the account and name before you submit.

Bank codes

When method=bank, set payee.bank_code to a code from this table (taken from the contract, case-sensitive):

CodeBank name
HBLHabib Bank Limited
UBLUnited Bank Limited
MCBMCB Bank Limited
MCB_ARIF_HABIBMCB Arif Habib
NBPNational Bank of Pakistan
ALFALAHBank Alfalah Limited
ALLIEDAllied Bank Limited
ASKARIAskari Bank Limited
FAYSALFaysal Bank Limited
MEEZANMeezan Bank
AL_HABIBBank Al Habib Limited
HABIB_METROHabib Metropolitan Bank
BANK_ISLAMIBank Islami Pakistan Limited
DUBAI_ISLAMICDubai Islamic Bank Pakistan
PUNJAB_BANKThe Bank of Punjab
SINDH_BANKSindh Bank Limited
SONERISoneri Bank Limited
SUMMITSummit Bank Limited
SILKSilk Bank Limited
SAMBASamba Bank Limited
JSJS Bank Limited
NIBNIB Bank Limited
APNAApna Microfinance Bank
CITICiti Bank NA
NRSPNRSP Microfinance Bank
CDNSCentral Directorate of National Savings

Idempotency and retries

SituationWhat the platform doesWhat you should do
Same key, same request bodyReturns the latest state of the same disbursement (200) and doesn't pay againHandle the returned status
Same key, different request body (any field, including payee details)409 idempotency_conflict; the original disbursement is unchanged. related_resource_id is the disbursement accepted with this keyAn integration bug: fix how you generate keys. Query that disbursement before deciding whether to pay again
Same key, concurrent requestsOnly one runs; the others wait and return the same disbursement, or get 409 operation_in_progressRetry later with the same key
New key, same merchant disbursement ID409 disbursement_no_conflict; related_resource_id is the existing disbursementQuery the existing disbursement; this ID has been paid or is being paid
Rejected with 400, 401, 403 or 422Nothing is created; the idempotency key and ID aren't used upFix it and submit with a new key; you can keep the ID
503 temporarily_unavailable, 429 rate_limitedNot accepted this time; nothing is createdRetry later with the same key (for 429, after Retry-After)
Timeout, 500, 502 or 503 upstream_unavailableMay have been acceptedRetry with the same key, or query by ID (see the diagram below)
  • Derive the idempotency key from the merchant disbursement ID: disbursement.create:<ID>:<attempt>. Increase the attempt number only when you fix and resubmit a request rejected with 4xx. If you use random UUIDs instead, write the key to your database before sending and read it back for retries; never generate a new one.
  • All request fields are compared. Idempotency records are kept for 7 days; the merchant disbursement ID stays unique forever.
  • For the retry wrapper, reuse postWithRetry from the reference implementation for retries.

Warning: After a timeout or 5xx, don't resend with a new ID or idempotency key

When a request times out or returns 5xx, the disbursement may already be accepted, and the money may already be on its way to the payee. If you resend with a new idempotency key or a new merchant disbursement ID, the platform treats it as a new disbursement. The payee may receive the money twice, and money that has been paid out can't be pulled back. There are only two correct options: retry with the original idempotency key, or query by merchant disbursement ID. See Don't charge again with a new key.

After a timeout or 5xx
After a timeout or 5xx

Status and outcomes

statusresolution_statusMeaningWhat you should do
processingknownAccepted and being paidWait for events; don't treat it as failed
processingneeds_queryOutcome unknown; the platform is querying the disbursement channelSame as above; don't create another one
processingmanual_reviewStill unknown after 72 hours of automatic status queries, so it's under manual review. The platform still queries every 24 hours and never marks it failed on its ownKeep it as "processing". The outcome arrives as an event; to chase it, contact the platform with the disbursement ID
succeededknownThe disbursement succeededBook paid_amount; give payee.utr to the payee
failedknownThe disbursement failed; the amount and fee are back in the settleable balanceHandle by failure code; to pay again, create a new one with a new ID
reversedknownReturned by the payee's wallet or bank after success, so the payee didn't get the money; the amount and fee are back in the settleable balanceCheck first, then decide whether to create a new one with a new ID; don't resend automatically
Unknown value—A status the platform addedTreat it as "not finished", query and alert; never treat it as succeeded
  • There's no cancel, pull-back or retry endpoint. failed and reversed are final. A disbursement becomes failed only when the disbursement channel clearly fails it, when it can't be submitted for 30 minutes in a row, or after the platform's manual review.
  • A reversal can appear after success: the platform checks again on day 1 and day 3 after success, and handles reversal notices whenever they arrive.
  • Query schedule: rely on webhooks first. If a disbursement is still processing 10 minutes after acceptance and you haven't received an event, query at 30 minutes, 2 hours, 6 hours and 24 hours, then once a day until it reaches a final status. Leave at least 10 seconds between two queries of the same disbursement. Queries only read the platform's current status, so querying more often won't get you a result sooner.
  • View one disbursement or export them: Disbursements (opens the merchant portal in a new tab) (Requires Owner, Admin, Finance or Developer).

Failure codes

When status=failed, failure.code takes a value from this table (taken from the contract):

failure.codeMeaningWhat to do
payee_account_invalidThe payee account is invalid or doesn't exist, the account name doesn't match, or the bank code is wrongAsk the payee to check their payee details, then create a new disbursement with a new merchant disbursement ID
payee_account_restrictedThe payee account is restricted, frozen, over its receiving limit, or has stopped receiving paymentsOnce the payee's restriction is lifted, create a new disbursement, or use another account
upstream_rejectedRejected by the disbursement channel (a business rejection, a manual rejection, the channel's quota running out, and so on)Create a new disbursement later; if it keeps happening, contact the platform
upstream_unavailableThe platform couldn't submit it for 30 minutes in a row and marked it as failed; it shares its name with the HTTP error code upstream_unavailable but means something differentCreate a new disbursement later
upstream_failedOther failures, including ones the platform confirms as failed after a manual check; handle any failure code you don't recognize as this oneContact the platform
  • Every failure has already returned the full amount and fee to the settleable balance.
  • Tolerate new failure codes; handle unknown ones as upstream_failed.

Caution: The two upstream_unavailable codes mean different things

The HTTP error code upstream_unavailable (the create request returned 502 or 503, the outcome is unknown, retry with the original key) and the failure code upstream_unavailable (the disbursement failed and the funds are back) aren't the same thing. Check error.code for the first and failure.code for the second (failure reasons versus API errors).

Webhook events

EventWhen it's sentWhat you usually do
disbursement.processingAfter acceptance, for disbursements made through the API and in the merchant portal (one by one or in batches)Record the disbursement ID and mark your business record as "disbursement processing"
disbursement.succeededConfirmed success, with paid_amount and payee.utrMark it paid with the amount actually paid
disbursement.failedA clear failure, with failureMark it failed; the funds are back in the settleable balance
disbursement.reversedReturned after success, with reversed_atMark it unpaid again and tell your operations team; don't resend automatically

data.object is a snapshot of the Disbursement when the event occurred (same fields as the Disbursement object). To fetch a missed disbursement event with GET/v1/events/{event_id}, you need disbursements:read. Subscribe to events in Webhook settings (opens the merchant portal in a new tab) (Requires Owner, Admin or Developer).

Example payload (a reversal event in the live environment; reversals can't be triggered in the test environment):

disbursement.reversed
{
  "id": "evt_demo_203",
  "type": "disbursement.reversed",
  "api_version": "v1",
  "created_at": "2026-10-30T02:10:00Z",
  "environment": "live",
  "merchant_id": "mer_demo_001",
  "data": {
    "object": {
      "id": "dsb_demo_009",
      "version": 3,
      "merchant_disbursement_no": "DSB-DEMO-009",
      "environment": "live",
      "amount": 30000,
      "currency": "PKR",
      "fee": 600,
      "method": "easypaisa",
      "payee": {
        "account_last4": "4567",
        "name": "Test Payee",
        "bank_code": null,
        "bank_name": null,
        "utr": "UTR-DEMO-001"
      },
      "customer_ip": "203.0.113.10",
      "status": "reversed",
      "resolution_status": "known",
      "failure": null,
      "paid_amount": 30000,
      "succeeded_at": "2026-10-29T08:01:04Z",
      "failed_at": null,
      "reversed_at": "2026-10-30T02:09:58Z",
      "description": "Example disbursement",
      "metadata": { "customer_ref": "CUSTOMER-DEMO-001" },
      "source": "api",
      "batch_id": null,
      "created_at": "2026-10-29T08:00:00Z",
      "updated_at": "2026-10-30T02:09:58Z"
    }
  }
}

Handle disbursement events (first verify the signature and environment, deduplicate by event ID and return 2xx):

Node.js
// Verify the signature, check the environment, deduplicate by event ID and return 2xx first (see the Webhooks
// guide), then call this function asynchronously
export async function onDisbursementEvent(event) {
  const d = event.data.object;
  const local = await findDisbursement(d.id); // Your saved snapshot; disbursements made in the portal (source portal or batch) may be missing
  if (local && local.version >= d.version) return; // Older version: discard
  await saveDisbursementSnapshot(d);
  if (d.status === 'succeeded')
    return markPaid(d.merchant_disbursement_no, d.paid_amount, d.payee.utr); // Book the amount actually paid
  if (d.status === 'failed') return markFailed(d.merchant_disbursement_no, d.failure.code); // Funds are back in the settleable balance
  if (d.status === 'reversed') return markReversed(d.merchant_disbursement_no); // Mark it unpaid and alert your ops team; don't resend automatically
  // processing or an unknown status: keep waiting, never treat it as succeeded
}

Events for the same disbursement can arrive out of order or more than once. reversed (version 3) may arrive before succeeded (version 2). If you handle events by version, the late succeeded is discarded and never turns a reversed disbursement back into paid (out-of-order events and version comparison). To pay again after a reversal, create a new disbursement with a new ID. For disbursements made in the merchant portal without an ID, merchant_disbursement_no equals the disbursement ID.

Error codes and rate limits

Creating a disbursement runs the checks below in order. When a check fails, the API returns right away without running the rest. Except for 201, nothing is created and no funds are held.

OrderCheckIf it failsWhat you should do
1Request format: missing Idempotency-Key, missing fields, wrong type or length; card-number-like digits in description or metadata400 invalid_request (field names the field); 400 sensitive_data_rejectedFix it and submit with a new key
2Idempotency: same key with a different body; a request with the same key still running409 idempotency_conflict; 409 operation_in_progressFor the first, fix how you generate keys; for the second, retry later with the same key
3Merchant status and the disbursements capability403 merchant_not_active; 403 capability_disabledContact the platform
4The currency is enabled for you, a disbursement channel is available, and limits and disbursement pricing are set422 currency_not_supportedUse only PKR for now; contact the platform
5Integer amount; payee details and customer_ip format400 invalid_request (field is amount, payee.* or customer_ip)Fix it and submit with a new key
6Unique merchant disbursement ID409 disbursement_no_conflict (related_resource_id is the existing disbursement)Query the existing disbursement; don't resend with a new ID
7Fee less than the amount422 fee_exceeds_amountIncrease the amount
8Limits: minimum and maximum per disbursement, daily total amount, daily count422 amount_out_of_range, without the thresholdAdjust the amount or wait for the next business day; ask the platform about your limits
9Disbursements paused by the platform503 temporarily_unavailable, without a reasonRetry later with the same key
10Risk lists422 risk_rejectedDon't try to get around it; contact the platform with questions
11Hold the settleable balance422 insufficient_balance; error.details carries shortfall and currencyOnce the settleable balance is enough, resubmit with the same ID and a new idempotency key
—All checks pass201 processing, then disbursement.processing is sentSave the disbursement ID and wait for the outcome
422 insufficient_balance
{
  "error": {
    "code": "insufficient_balance",
    "message": "The settleable balance isn't enough to cover the disbursement amount and fee. Shortfall: PKR 56.00",
    "field": null,
    "related_resource_id": null,
    "retryable": false,
    "details": { "shortfall": 5600, "currency": "PKR" }
  },
  "request_id": "req_demo_102"
}

General errors: 401 unauthenticated; 403 forbidden (the key lacks the required scope) and ip_not_allowed; 404 not_found when you query a disbursement that doesn't exist or doesn't belong to the current merchant and environment; 429 rate_limited; 500 internal_error, and 502 or 503 upstream_unavailable (outcome unknown, handle as in Idempotency and retries). For all error codes, see the error code reference.

Rate limits

Disbursement creation has its own per-merchant rate limit and doesn't use up the payment or refund allowance. A different API key doesn't get around it. Going over returns 429 and the request isn't executed; wait Retry-After seconds and retry with the original idempotency key. When you have many disbursements to make at once, limit concurrency, or use batch disbursements in the merchant portal. Query endpoints follow the API reference rate limits.

Test in the test environment

  • The prerequisites are the same as in live (see Setup and prerequisites): the platform enables the disbursements capability for the test environment and sets up a PKR channel and limits, and you use a test key with the disbursement scopes. The test environment moves no real money.
  • The test settleable balance also comes from PKR collections, after the test environment's lock period. To rehearse sooner, ask the platform to shorten the lock period for the test environment.
  • Disbursements under PKR 500.00 (amount < 50000) succeed; PKR 500.00 and above fail. Take the failure code from failure.code; it isn't fixed to one code.
  • Bank accounts that aren't 9–20 characters long are accepted and then fail right away, so a 24-character IBAN always fails in the test environment. Test bank disbursements with a 9–20 digit local account. Live has no such limit.
  • Reversals and unknown outcomes (needs_query, manual_review) can't be simulated yet. To test them yourself: in a unit test, pass the example payload to your event handler and confirm it marks the disbursement unpaid again without resending; pass a processing + needs_query response to your code and confirm it isn't treated as a failure and no new disbursement is created; and use "Send test event" in Webhook settings (opens the merchant portal in a new tab) (Test environment; Requires Owner, Admin or Developer) to send a disbursement.reversed, checking only receipt and signature verification (test events don't match a real disbursement, so don't change business data based on them).
#ScenarioActionExpected
T1Wallet disbursement succeedsThe create example, amount 30000201 processing → disbursement.succeeded with paid_amount and payee.utr; the settleable balance drops by amount plus fee
T2Disbursement failsamount 60000201 processing → disbursement.failed; amount plus fee back in the settleable balance
T3Bank account disbursementmethod=bank, bank_code, 12-digit local accountSame as T1
T4IBAN (test limit)24-character IBANAccepted, then fails right away
T5Settleable balance too lowAmount plus fee above the settleable balance422 insufficient_balance with details.shortfall; no disbursement record
T6Format errorsamount 30050; or payee.account +923001234567400 invalid_request with field amount or payee.account
T7Replay with the same keySend T1's key and body again200, the same disbursement
T8New key, same IDA new key with T1's merchant disbursement ID409 disbursement_no_conflict; related_resource_id is T1's disbursement
T9Retry with the same key after a timeoutSend with a 100 ms client timeout, then retry with the same key or query by IDOnly one disbursement
T10Create with a read-only keyCall the create example with a key that only has disbursements:read403 forbidden

For the general test environment notes, see the testing guide.

Go-live checks

Before you go live with disbursements, confirm each item (checkmarks are saved only in this browser; you can copy the list as Markdown). For the general go-live checks, see the go-live checklist.

0 of 8 completed

Setup and permissions 0/2Idempotency and outcomes 0/3Funds and payee details 0/2Self-test 0/1

  • Setup and permissions

    DC-01The platform has enabled disbursements in the live environment and set up the PKR channel, limits and disbursement fee rateWhere: Setup and prerequisitesSetup and permissions
  • DC-02The live key that creates disbursements is created by an Owner or Admin and gets only the scopes disbursements need: disbursements:write, disbursements:read to query outcomes, and finance:read if you check settleable as in DC-06. Also set up an IP allowlistWhere: API Key (opens the merchant portal in a new tab) (Requires Owner, Admin or Developer)Setup and permissions · Required role: Owner or Admin
  • Idempotency and outcomes

    DC-03Each disbursement's merchant disbursement ID is generated and saved before the request, and the idempotency key is derived from it; after a timeout or a 5xx, the code only retries with the original key or looks the disbursement up by its ID, and has no path that resends with a new ID or a new keyWhere: Idempotency and retriesIdempotency and outcomes · Acceptance criteria AC-069
  • DC-04The live endpoint subscribes to the 4 disbursement.* events, deduplicates by event ID and processes by versionWhere: Webhook (opens the merchant portal in a new tab) (Requires Owner, Admin or Developer), Out-of-order events and version comparisonIdempotency and outcomes · Required role: Owner, Admin or Developer
  • DC-05Book by paid_amount; on failed, handle it by failure code, and handle a code you don't recognize as upstream_failed; on reversed, change it from paid back to unpaid and notify your operations team, without resending automaticallyWhere: Failure codeIdempotency and outcomes
  • Funds and payee details

    DC-06Check settleable with GET /v1/balances before you create a disbursement; alert on insufficient_balance and don't reduce the amount automaticallyWhere: Funds rulesFunds and payee details
  • DC-07Full payee account numbers stay out of logs, error reports and support tickets; your payee entry page says "The account name must match the real name on the account"Where: Payee rulesFunds and payee details · Acceptance criteria AC-064
  • Self-test

    DC-08All of T1 to T10 in the test-environment checklist pass, and reversals and unknown outcomes are verified with the methods describedWhere: Test in the test environmentSelf-test

FAQ

Was this page helpful?