This is the public API/SDK guide for integrators. For internal implementation detail (Orchestrator state machine, signer roles), see docs/guides/disputes.md in the repo.
Disputes are a critical part of any marketplace. When a buyer and seller disagree, OFFER-HUB provides a structured process to resolve conflicts fairly while funds remain safely locked in escrow.
Disputes only apply to orders that have funded escrow. For orders still in FUNDS_RESERVED, use the cancel flow instead.
When to Use Disputes
A dispute should be opened when:
- — Seller missed the deadline or abandoned the order
- — Deliverables don't match the agreed specifications
- — Only some of the work was completed
- — Seller is unresponsive
Disputes should be a last resort. Encourage buyers and sellers to communicate and resolve issues directly when possible.
Dispute Lifecycle
IN_PROGRESS
↓
DISPUTED (awaiting resolution)
↓
┌───┴───┐
↓ ↓
REFUNDED RELEASED
(buyer) (seller)
↓
CLOSED
| State | Description |
|---|
DISPUTED | Funds locked, awaiting platform resolution |
REFUNDED | Dispute resolved in buyer's favor |
RELEASED | Dispute resolved in seller's favor |
CLOSED | Order completed after resolution |
Opening a Dispute
Only the buyer can initiate a dispute on an IN_PROGRESS order:
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/resolution/dispute \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"openedBy": "BUYER",
"reason": "NOT_DELIVERED"
}'
Request Body
| Field | Type | Required | Description |
|---|
openedBy | string | Yes | "BUYER" or "SELLER" |
reason | string | Yes | "NOT_DELIVERED", "QUALITY_ISSUE", or "OTHER" |
{
"data": {
"id": "ord_xyz789",
"status": "DISPUTED",
"dispute": {
"id": "dsp_abc123",
"openedBy": "BUYER",
"reason": "NOT_DELIVERED",
"status": "open",
"openedAt": "2026-02-25T10:00:00.000Z"
}
}
}
What Happens When a Dispute Opens
- — Becomes
DISPUTED
- — Neither party can release funds
- — Support team receives alert
- — Buyer and seller are notified (via your app's SSE integration)
- — Both parties can submit documentation
Opening a dispute requires a blockchain transaction:
Buyer signs "dispute_escrow" → Contract enters disputed state
This transaction:
- Records the dispute on-chain
- Prevents any release until resolution
- Creates an immutable record of the dispute
Resolving Disputes
Only the platform (using the master key or support scope) can resolve disputes.
If the dispute is valid, refund the buyer:
curl -X POST http://localhost:4000/api/v1/disputes/dsp_abc123/resolve \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"decision": "FULL_REFUND",
"note": "Seller failed to deliver work within agreed timeframe"
}'
If the seller did deliver properly:
curl -X POST http://localhost:4000/api/v1/disputes/dsp_abc123/resolve \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"decision": "FULL_RELEASE",
"note": "Buyer confirmed delivery via email, dispute was a misunderstanding"
}'
For partial delivery, split the funds:
curl -X POST http://localhost:4000/api/v1/disputes/dsp_abc123/resolve \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"decision": "SPLIT",
"releaseAmount": "20.00",
"refundAmount": "30.00",
"note": "Seller delivered 40% of agreed work"
}'
Split resolution requires both releaseAmount (seller) and refundAmount (buyer) to sum to the original escrow amount.
Resolution Request Body
| Field | Type | Required | Description |
|---|
decision | string | Yes | "FULL_RELEASE", "FULL_REFUND", or "SPLIT" |
note | string | No | Internal note for the decision |
releaseAmount | string | Split only | Amount to release to seller |
refundAmount | string | Split only | Amount to refund to buyer |
{
"data": {
"id": "dsp_abc123",
"status": "resolved",
"decision": "FULL_REFUND",
"resolvedBy": "usr_support",
"resolvedAt": "2026-02-25T14:00:00.000Z",
"note": "Seller failed to deliver work within agreed timeframe",
"order": {
"id": "ord_xyz789",
"status": "REFUNDED"
}
}
}
Both parties and support can add comments during the dispute:
curl -X POST http://localhost:4000/api/v1/disputes/dsp_abc123/comments \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "usr_buyer123",
"message": "Here is the original agreement showing the deadline was February 20th",
"attachments": ["https://example.com/contract.pdf"]
}'
{
"data": {
"id": "cmt_def456",
"disputeId": "dsp_abc123",
"userId": "usr_buyer123",
"message": "Here is the original agreement...",
"attachments": ["https://example.com/contract.pdf"],
"createdAt": "2026-02-25T11:00:00.000Z"
}
}
Listing Disputes
Get all disputes with filtering:
# All open disputes
curl "http://localhost:4000/api/v1/disputes?status=open" \
-H "Authorization: Bearer ohk_live_your_api_key"
# Disputes for a specific user
curl "http://localhost:4000/api/v1/disputes?user_id=usr_buyer123" \
-H "Authorization: Bearer ohk_live_your_api_key"
# Disputes in date range
curl "http://localhost:4000/api/v1/disputes?created_after=2026-02-01&created_before=2026-02-28" \
-H "Authorization: Bearer ohk_live_your_api_key"
{
"data": [
{
"id": "dsp_abc123",
"orderId": "ord_xyz789",
"openedBy": "usr_buyer123",
"reason": "Work not delivered",
"status": "open",
"openedAt": "2026-02-25T10:00:00.000Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}
Getting Dispute Details
curl http://localhost:4000/api/v1/disputes/dsp_abc123 \
-H "Authorization: Bearer ohk_live_your_api_key"
{
"data": {
"id": "dsp_abc123",
"orderId": "ord_xyz789",
"openedBy": "usr_buyer123",
"reason": "Seller has not delivered the agreed work after 2 weeks",
"status": "open",
"openedAt": "2026-02-25T10:00:00.000Z",
"order": {
"id": "ord_xyz789",
"amount": "50.00",
"currency": "USD",
"buyerId": "usr_buyer123",
"sellerId": "usr_seller456",
"title": "Logo Design"
},
"comments": [
{
"id": "cmt_def456",
"userId": "usr_buyer123",
"message": "Here is the original agreement...",
"createdAt": "2026-02-25T11:00:00.000Z"
}
]
}
}
Using the SDK
import { OfferHubSDK } from '@offerhub/sdk';
const sdk = new OfferHubSDK({
apiUrl: 'http://localhost:4000',
apiKey: 'ohk_live_your_api_key'
});
// Open a dispute
const dispute = await sdk.disputes.open('ord_xyz789', {
reason: 'Work not delivered as specified'
});
// Add a comment
await sdk.disputes.addComment(dispute.id, {
userId: 'usr_buyer123',
message: 'Additional evidence attached',
attachments: ['https://example.com/screenshot.png']
});
// Resolve the dispute (requires support scope)
await sdk.disputes.resolve(dispute.id, {
decision: 'FULL_REFUND',
note: 'Seller confirmed they could not complete the work'
});
const events = sdk.events.subscribe();
events.on('order.disputed', (data) => {
console.log('New dispute:', data.disputeId);
// Notify support team
notifySupport(data);
});
events.on('dispute.resolved', (data) => {
console.log('Dispute resolved:', data.decision);
// Notify both parties
notifyParties(data);
});
Real-Time Dispute Events (SSE)
Dispute lifecycle events are delivered over the SSE event stream at GET /api/v1/events — there is no outbound webhook API:
curl -N "http://localhost:4000/api/v1/events?types=order.disputed,dispute.opened,dispute.resolved" \
-H "Authorization: Bearer ohk_live_your_api_key"
Or with the SDK:
events.on('order.disputed', (data) => {
console.log('New dispute:', data.disputeId);
notifySupport(data);
});
events.on('dispute.resolved', (data) => {
console.log('Dispute resolved:', data.decision);
notifyParties(data);
});
The order.disputed event payload looks like this:
{
"id": "evt_abc123",
"type": "escrow.disputed",
"created_at": "2026-02-25T10:00:00.000Z",
"data": {
"escrow_id": "esc_xyz789",
"dispute_id": "dsp_abc123",
"opened_by": "BUYER",
"reason": "NOT_DELIVERED",
"order_id": "ord_xyz789"
}
}
When a dispute is resolved, the escrow settles and the corresponding order events (order.released or order.refunded) fire on the same stream.
Best Practices
- — Document when disputes are valid
- — Aim for resolution within 48-72 hours
- — Ask both parties for documentation
- — Apply the same standards to all disputes
- — Always include detailed resolution notes
- — Update your UI when disputes open/close
- — Send emails when dispute status changes
- — Make it easy to submit documentation
- — Display all comments and timeline
Error Handling
| Error | Cause | Solution |
|---|
ORDER_NOT_IN_PROGRESS | Order not in disputable state | Check order status first |
DISPUTE_ALREADY_EXISTS | Order already has open dispute | Get existing dispute details |
UNAUTHORIZED_DISPUTE | Non-buyer trying to open dispute | Only buyers can open disputes |
INVALID_RESOLUTION | Invalid decision value | Use FULL_RELEASE, FULL_REFUND, or SPLIT |
INSUFFICIENT_SCOPE | API key lacks support scope | Use key with support permissions |
Idempotency
Dispute operations support idempotency keys:
curl -X POST http://localhost:4000/api/v1/orders/ord_xyz789/resolution/dispute \
-H "Authorization: Bearer ohk_live_your_api_key" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440012" \
-H "Content-Type: application/json" \
-d '{"openedBy": "BUYER", "reason": "NOT_DELIVERED"}'
Dispute idempotency keys are stored — you cannot open the same dispute twice.
Next Steps
- Withdrawals Guide — Move funds off-platform
- Events (SSE) — SSE event stream and event catalog
- SDK Reference — Full SDK documentation