Disputes
A dispute freezes an order: the buyer's payment stays held and nobody is paid until it ends. It can end three ways, and your software can take part in two of them. You and the buyer can end it yourselves, or a person at Freeport reads the record and decides. There is no fourth way, and no route that decides a dispute the two of you have not agreed on.
The one rule
Alone, each side can only give up its own side of the money. You can return all of it. The buyer can release all of it to you. Anything in between takes both of you: one side puts a figure on the table, the other accepts that same figure, and nothing moves until they do. Nobody can reach into the other side's pocket, which is why none of this waits for an operator.
Scopes
disputes:read reads a case and disputes:write makes every move below. Neither is part of orders:write, on purpose: the key you hand a deliverer's bot should not be able to give money back. The fulfilment preset can read disputes and cannot move them. A key minted before these scopes existed does not have them, whatever preset it was minted with; mint a new one.
Knowing about one
order.disputed arrives when a dispute opens, and carries the case under data.dispute in the same shape GET /v1/orders/{id}/dispute returns. The three fields to act on:
respondent: who is being asked to answer. When the buyer opened it, that is you.respond_by: when the respondent's time to answer ends, 24 hours after opening.answered: whether the respondent has said anything or put a figure down. It is worked out from the record on every read, so it cannot say yes over an empty one.
A respondent who lets the window pass reads as did not answer on the record the person deciding it works from. That is not an automatic loss, and it is never a good look. GET /v1/disputes?state=open is the queue to work, newest first.
Say your side
POST /v1/orders/{id}/dispute/statements with { "body" } puts your account on the record: what was delivered, to which character, at what time. The buyer can read it and so can the person who decides. A statement cannot be edited or removed once added, by anyone, and each side may add ten. Screenshots go in the order chat, which staff read during a dispute.
Refund in full
POST /v1/orders/{id}/dispute/concede, with an optional { "note" }. The whole total goes back to the buyer, you keep nothing from the order, the stock returns to the listing, and the dispute closes as decided_via: "seller_conceded". That is recorded as your concession and not as a dispute decided against you; only a person's ruling counts against anybody. It cannot be undone. order.dispute_resolved and order.refunded follow.
Settle
A settlement is one figure, buyer_refund: what goes back to the buyer, as a decimal string in the order's currency. You keep the rest, less the order's fees.
GET /v1/orders/{id}/dispute/quote?buyer_refund=4.00shows what a figure would do:buyer_refund,seller_netandfees. It is the arithmetic that then posts the money, not an estimate of it. Fees are never more than what you keep, so a settlement cannot make you owe fees on money you gave back.POST /v1/orders/{id}/dispute/offerswith{ "buyer_refund", "note" }puts your figure on the table. It replaces any offer standing, from either side. While it stands the buyer can accept it at any moment, so offer what you mean. Each side may make five.POST .../offers/{offerId}/acceptwith{ "buyer_refund" }accepts the buyer's offer. You send the figure you are agreeing to. If the offer on the table is no longer that one, because it was replaced or withdrawn while your tool was deciding, the answer is a 409offer_changedand nothing moves. Read the dispute again.POST .../offers/{offerId}/declineturns the buyer's offer down. The dispute stays open.POST .../offers/{offerId}/withdrawtakes your own offer back, while it still stands.
The offer on the table is standing_offer on the dispute; at most one is ever open. The figure must be more than nothing and less than the total: those two ends are the buyer releasing the payment and you refunding in full, and asking for them as a settlement is a 400 not_a_settlement. You cannot accept your own offer (403 your_own_offer).
When a settlement is accepted the money moves at once, the order becomes partially_refunded, and the dispute closes as decided_via: "settlement".
What the buyer does
order.dispute_updated tells you when the buyer adds a statement, makes an offer, or declines or withdraws one, with the case under data.dispute as it then stands. Your own moves are not sent back to you. However a dispute ends, order.dispute_resolved carries the outcome, and decided_via says how: operator, seller_conceded, buyer_released or settlement. On a closed dispute buyer_refund is what went back to the buyer, whether that was all of it, none of it or a share, and resolution_note is the reason a person at Freeport gave when they decided it.
Send it twice safely
Every move takes an Idempotency-Key. A retried concession or acceptance gets the first answer back and moves no money a second time. Without a key, a second attempt on a closed dispute is a 409 no_open_dispute, which is also safe: the money moved once.
Rehearse it
On the sandbox the buyer's half lives at the same paths with /sandbox in front: POST /v1/sandbox/orders/{id}/dispute/statements, /offers, /offers/{offerId}/accept, /decline, /withdraw, and /release, which is the buyer dropping the dispute and paying you. Each answers with the dispute as you then read it, and each reaches your endpoint by the production path.