curl --request POST \
--url https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form 'resolution=Refund approved after review' \
--form status=REFUNDED \
--form sellerAmount=30000 \
--form buyerAmount=20000 \
--form additionalFeeRefundable=true \
--form file='@example-file'import requests
url = "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"resolution": "Refund approved after review",
"status": "REFUNDED",
"sellerAmount": "30000",
"buyerAmount": "20000",
"additionalFeeRefundable": "true"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('resolution', 'Refund approved after review');
form.append('status', 'REFUNDED');
form.append('sellerAmount', '30000');
form.append('buyerAmount', '20000');
form.append('additionalFeeRefundable', 'true');
form.append('file', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Dispute resolution submitted successfully",
"data": {
"id": "6948fb43cb570eeef76285e3",
"amount": 1000,
"purpose": "iPhone 13 Pro Max",
"description": "Black Titanium, White Titanium, Natural Titanium, Desert Titanium",
"whoPays": "buyer",
"imageUrl": [
"https://mediacloud.me/media/bWVkaWEvaW1hZ2VzL29wdGltaXplL3Z0VjhwN0psSzlIMzhEVWxicWpmV1JZdUpuSUZNWlhEdU03UkVmeHEuanBn.jpg"
],
"fee": 15,
"paymentToken": "PY_KrWLqPd90314",
"paidAt": "1766391073",
"status": "REFUNDED",
"state": "CLOSED",
"logs": [
"DISPUTED"
],
"channel": "API",
"isSeller": false,
"dispute": [
{
"name": "Ezumah",
"profile": null,
"message": "Item not as described; the seal was broken on arrival",
"proofUrl": "https://mediacloud.me/media/dispute-evidence.jpg",
"type": "buyer",
"createdAt": "2025-12-22 08:20:00"
},
{
"name": "Customer support",
"profile": null,
"message": "Refund approved after review of the buyer's evidence.",
"proofUrl": "https://mediacloud.me/media/resolution-note.pdf",
"type": "customer support",
"createdAt": "2025-12-22 09:05:00"
}
],
"paymentDetails": {
"id": "6948fd20cb570eeef76286a4",
"amount": 1015,
"status": "success",
"reference": "9500364766547485",
"fee": 15,
"transactionType": "escrow",
"currency": "NGN",
"createdAt": "2025-12-22T08:11:12.605Z"
},
"category": null,
"completedAt": "1766394300",
"approvedClaimBy": null,
"refundedBy": null,
"maxDelivery": 20,
"deliveryTimeline": "minutes",
"totalQuantity": 1,
"settlementType": "STANDARD",
"milestones": [],
"createdAt": "2025-12-22T08:03:15.124Z",
"updatedAt": "2025-12-22T09:05:00.000Z"
}
}Resolve dispute
Merchant resolves a dispute: COMPLETED releases to the seller; REFUNDED returns funds to the buyer; SPLIT divides the held funds between them in amounts you name. Must not send a customer-id header; the escrow must belong to one of the merchant’s customers. Sent as multipart/form-data.
Splitting the held funds
When neither side is wholly right, send status: SPLIT with sellerAmount and buyerAmount. The two must add up to exactly what the escrow still holds, which is not always amount:
- Standard escrow:
amountminus the seller’s share of the escrow fee. On a buyer-paid escrow the seller pays no share, so the pool is the fullamount. - Milestone escrow: the total of the milestones the buyer never confirmed. Already-released milestones stay with the seller and are untouched.
Read the pool off the escrow before you rule, and send amounts with at most two decimal places. Amounts that do not add up are rejected and no money moves. Vault escrows cannot be split.
The escrow closes as SPLIT, its still-held milestones are marked SPLIT, a split object records the division, and an escrow.split webhook fires.
The additional (delivery) fee
If the escrow carries an additional fee, this is where you decide who keeps it. By the time you rule on a dispute you know whether the delivery actually happened, which you could not know when you priced it.
Send additionalFeeRefundable with the resolution:
true: the fee goes back to the buyer along with the refunded principal. Nothing was delivered.false: the fee is credited to your merchant wallet as if the escrow had completed. The delivery cost was already incurred.
Your choice is applied to this settlement and saved on the escrow, overriding whatever was set before payment. Omit the field to settle on the escrow’s existing additionalFeeRefundable (which defaults to true). REFUNDED and SPLIT resolutions are affected; a COMPLETED one always pays the fee to you.
The escrow fee itself is never refunded to either party, on a split any more than on a refund; see Fees & settlement.
curl --request POST \
--url https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form 'resolution=Refund approved after review' \
--form status=REFUNDED \
--form sellerAmount=30000 \
--form buyerAmount=20000 \
--form additionalFeeRefundable=true \
--form file='@example-file'import requests
url = "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}"
files = { "file": ("example-file", open("example-file", "rb")) }
payload = {
"resolution": "Refund approved after review",
"status": "REFUNDED",
"sellerAmount": "30000",
"buyerAmount": "20000",
"additionalFeeRefundable": "true"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('resolution', 'Refund approved after review');
form.append('status', 'REFUNDED');
form.append('sellerAmount', '30000');
form.append('buyerAmount', '20000');
form.append('additionalFeeRefundable', 'true');
form.append('file', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://staging.api.payluk.ng/v1/escrow/dispute/resolve/{escrowId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"resolution\"\r\n\r\nRefund approved after review\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"status\"\r\n\r\nREFUNDED\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"sellerAmount\"\r\n\r\n30000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"buyerAmount\"\r\n\r\n20000\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"additionalFeeRefundable\"\r\n\r\ntrue\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Dispute resolution submitted successfully",
"data": {
"id": "6948fb43cb570eeef76285e3",
"amount": 1000,
"purpose": "iPhone 13 Pro Max",
"description": "Black Titanium, White Titanium, Natural Titanium, Desert Titanium",
"whoPays": "buyer",
"imageUrl": [
"https://mediacloud.me/media/bWVkaWEvaW1hZ2VzL29wdGltaXplL3Z0VjhwN0psSzlIMzhEVWxicWpmV1JZdUpuSUZNWlhEdU03UkVmeHEuanBn.jpg"
],
"fee": 15,
"paymentToken": "PY_KrWLqPd90314",
"paidAt": "1766391073",
"status": "REFUNDED",
"state": "CLOSED",
"logs": [
"DISPUTED"
],
"channel": "API",
"isSeller": false,
"dispute": [
{
"name": "Ezumah",
"profile": null,
"message": "Item not as described; the seal was broken on arrival",
"proofUrl": "https://mediacloud.me/media/dispute-evidence.jpg",
"type": "buyer",
"createdAt": "2025-12-22 08:20:00"
},
{
"name": "Customer support",
"profile": null,
"message": "Refund approved after review of the buyer's evidence.",
"proofUrl": "https://mediacloud.me/media/resolution-note.pdf",
"type": "customer support",
"createdAt": "2025-12-22 09:05:00"
}
],
"paymentDetails": {
"id": "6948fd20cb570eeef76286a4",
"amount": 1015,
"status": "success",
"reference": "9500364766547485",
"fee": 15,
"transactionType": "escrow",
"currency": "NGN",
"createdAt": "2025-12-22T08:11:12.605Z"
},
"category": null,
"completedAt": "1766394300",
"approvedClaimBy": null,
"refundedBy": null,
"maxDelivery": 20,
"deliveryTimeline": "minutes",
"totalQuantity": 1,
"settlementType": "STANDARD",
"milestones": [],
"createdAt": "2025-12-22T08:03:15.124Z",
"updatedAt": "2025-12-22T09:05:00.000Z"
}
}Authorizations
Your secret key as a Bearer token. The key prefix selects the environment: sk_test_... (staging) or sk_live_... (production); a key on the wrong host is refused with 403 Unauthorized Access. Each key is limited to 10 requests per minute (429 beyond that) and, on production, to the IP addresses on your dashboard allowlist when one is configured.
Path Parameters
The escrow's unique identifier.
Body
"Refund approved after review"
COMPLETED, REFUNDED, SPLIT "REFUNDED"
Required on a SPLIT resolution, rejected otherwise. The part of the held funds released to the seller. At most two decimal places. Together with buyerAmount it must equal exactly what the escrow still holds.
30000
Required on a SPLIT resolution, rejected otherwise. The part of the held funds returned to the buyer.
20000
Optional, and only meaningful on a REFUNDED or SPLIT resolution. true returns the escrow's additionalFee to the buyer, false credits it to your merchant wallet. Saved on the escrow, overriding the value set before payment. Omit to use whatever the escrow already carries.
true
Optional supporting document.