Answer disputes on store orders
Follow the chargebacks shoppers open on store orders, then send evidence to their bank or accept the dispute, with the Commerce API.
A dispute, or chargeback, is a shopper asking their bank to take back what they paid for a store order. The bank holds the money while it decides, so we take the amount from your balance. You answer with evidence that the order was genuine and reached the shopper, or you accept the dispute and the shopper keeps the money.
This page covers disputes on store orders. For disputes on the invoices your customers pay, see Follow payment disputes. The routes are under https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/disputes, and the API reference lists them under Commerce.
Before you begin
Section titled Before you begin- Commerce must be on for your organization, as Sell with the Commerce API describes.
- Reading disputes takes an organization API key with the
commerce.finance:readscope, or a member's token with the Billing role or above. - Changing evidence, sending it and accepting a dispute take
commerce.disputes:write, or the Billing role or above. A key that answers disputes also needscommerce.finance:readto read them. - Set your store's policies, so that the evidence quotes them. Add your policies to the evidence shows how.
What a dispute does to your balance
Section titled What a dispute does to your balanceWhen a shopper opens a dispute:
- We take the disputed
amountfrom your balance at once, as achargebackrow in the ledger, and the dispute fee as a secondchargebackrow when there is one. The rows areavailablestraight away, so they come off your next payout. - We put the order on risk hold with the reason
dispute_opened, so nothing more ships until you release it. An order that is cancelled, already held or partly shipped stays as it is. - We send the
commerce.dispute.openedwebhook.
If you win, we give the amount back as a positive chargeback row. The fee comes back too when Stripe returns it, and a PayPal fee never does. Losing or accepting the dispute keeps the rows, and so does a dispute that closes without an outcome.
A bank can ask about a payment before it disputes it. Stripe calls this an inquiry, and the dispute shows inquiry: true. An inquiry takes no money and sends no commerce.dispute.opened, though we still hold the order. You cannot accept an inquiry: answer it with evidence, or refund the order. If the bank turns it into a chargeback, inquiry becomes false, and we take the money and send commerce.dispute.opened.
| Event | When |
|---|---|
commerce.dispute.opened |
A chargeback opens, or an inquiry becomes one. |
commerce.dispute.won |
The dispute ends in your favour. |
commerce.dispute.lost |
The dispute ends in the shopper's favour, or you accept it. |
Each event's data holds the order_id, the dispute_id, the amount and the currency_code. When the provider reports a win after a loss, commerce.dispute.won follows commerce.dispute.lost and the money comes back. Receive commerce webhooks covers delivery.
Find disputes
Section titled Find disputesGET /commerce/disputes lists the store's disputes newest first, with count for every dispute the filters match.
| Filter | Takes |
|---|---|
status |
A status from the table below, or open for needs_response and under_review together |
order_id |
An order's numeric id |
livemode |
true for real payments, false for test ones |
limit, offset |
Up to 200 a page; 50 by default |
curl "https://api.coritan.com/api/v1/orgs/acme/commerce/disputes?status=needs_response&livemode=true" \
-H "X-API-Key: $ORG_API_KEY"
| Status | What it means |
|---|---|
needs_response |
The bank is waiting for your evidence, until the time in due_by. |
under_review |
The bank is deciding, usually because you sent evidence. |
won |
The dispute ended in your favour, and any money we took comes back. |
lost |
The dispute ended in the shopper's favour. |
accepted |
You accepted the dispute. |
closed |
It ended without an outcome, such as an inquiry that went no further. |
Each dispute has the order it is about, with the order's id, public_id and display_id, and the provider the shopper paid through: stripe or paypal. It has the amount and fee_amount in minor units of its currency_code, the bank's reason, and an outcome once there is one. can_submit says whether you can still send evidence, and can_accept whether you can accept the dispute. ledger_booked_at and ledger_reversed_at say when we took the money and when we gave it back.
Read the evidence
Section titled Read the evidenceGET /commerce/disputes/{dispute_id} answers the dispute with its evidence, the provider's last 20 events about it, the submission once evidence has gone, and last_error when the provider refused a request.
We assemble the evidence from the order when the dispute opens. While the dispute waits for evidence, each read assembles it again and keeps your changes, so it shows what sending it now would send. Reading changes nothing.
evidence field |
What it holds |
|---|---|
fields |
The text we would send, by field name. |
edited |
The fields you set yourself. |
urls |
The links you added. |
facts |
What we read from the order: its items, invoice, credit notes, refunds and shipments, the customer and their addresses, the terms they accepted, your policies, what the shopper was told, and the payment. |
missing |
What the order lacks that would make the evidence stronger. |
assembled_at, updated_at |
When we last assembled the evidence, and when you last changed it. |
From the order, we fill in these fields:
customer_email_address,customer_nameandcustomer_purchase_ip.billing_addressandshipping_address. Without a billing address,billing_addressrepeats the shipping address.shipping_carrier,shipping_tracking_number, andshipping_date, the day the first shipment left.product_description, with each item written as2 x Logo T-shirt (SKU TSHIRT-M).refund_policy_disclosure: when the shopper accepted which version of your terms, with yourterms_url, then your refund policy and your returns window.uncategorized_text: the order and its invoice, each shipment, each refund with its credit note, your shipping policy, and the updates the shopper could see on their order.
missing |
What it means |
|---|---|
shipping_tracking_number |
The order ships, and no shipment has a tracking number. |
delivery |
The order ships, and no shipment is marked delivered. |
terms |
The order has no record that the shopper accepted your terms. |
refund_policy |
The store has neither a refund policy nor a returns window. |
customer_purchase_ip |
The order has no IP address for the shopper. |
Tracking and delivery come from the order's fulfilments, as Ship an order yourself describes. A dispute that waits for evidence picks up a change to them the next time you read it.
Add your policies to the evidence
Section titled Add your policies to the evidenceThe evidence quotes three store settings. Set them with PATCH /commerce/store, which takes commerce.store:write or the Admin role:
| Setting | What the evidence does with it |
|---|---|
refund_policy |
Quotes it in refund_policy_disclosure. It is text of up to 5,000 characters. |
shipping_policy |
Quotes it in uncategorized_text. It is text of up to 5,000 characters. |
returns_window_days |
Says in refund_policy_disclosure that you accept returns within that many days. |
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/store" \
-H "X-API-Key: $ORG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"settings": {"refund_policy": "Return any unused item within 30 days of delivery for a full refund.", "shipping_policy": "Orders leave our Leeds warehouse within 2 working days.", "returns_window_days": 30}}'
A longer policy answers 422. Set terms_url as well, so that shoppers accept your terms before they pay, as Change the store's settings describes. A dispute that waits for evidence picks up new policies the next time you read it.
Send evidence
Section titled Send evidenceRead the dispute. Check its
evidence.fieldsandevidence.missing, and thatcan_submitistrue.Correct or add to the text with
PUT /commerce/disputes/{dispute_id}/evidence. Name only the fields you change, and sendurlsto replace the links:Shellcurl -X PUT "https://api.coritan.com/api/v1/orgs/acme/commerce/disputes/31/evidence" \ -H "X-API-Key: $ORG_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fields": { "shipping_tracking_number": "00340434161234567890", "cancellation_rebuttal": "The shopper did not ask to cancel before the order shipped." }, "urls": ["https://shop.example.com/proof/1043-delivery.pdf"] }'Text sets a field, and an empty string leaves it out.
nullgives the field back to the text we assemble from the order. The fields you do not name keep their text, and leaving outurlskeeps the links. The answer is the dispute in full.Send the evidence with
POST /commerce/disputes/{dispute_id}/submit:Shellcurl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/disputes/31/submit" \ -H "X-API-Key: $ORG_API_KEY" \ -H "Idempotency-Key: dispute-31-evidence"We assemble the evidence once more and send it through the payment provider. The answer is
200with the dispute:evidence_submitted_atis set, thestatusmoves on, usually tounder_review, andsubmissionrecords when the evidence went, who sent it and which fields. The order's timeline notes it too.
Caution
The bank takes one submission. Once the evidence has gone, you cannot change it or send more.
fields takes these names, which are the text fields of Stripe's dispute evidence: access_activity_log, billing_address, cancellation_policy_disclosure, cancellation_rebuttal, customer_email_address, customer_name, customer_purchase_ip, duplicate_charge_explanation, duplicate_charge_id, product_description, refund_policy_disclosure, refund_refusal_explanation, service_date, shipping_address, shipping_carrier, shipping_date, shipping_tracking_number and uncategorized_text. The evidence also has these limits, and the message of a 422 names the one you broke:
- Each field is text of up to 20,000 characters.
- You can add up to 20 links, each an
httporhttpsaddress of up to 2,000 characters. We add them to the end ofuncategorized_textwhen we send it. - Everything together, links included, is up to 150,000 characters.
evidence_via says what submitting does:
evidence_via |
What submitting does |
|---|---|
api |
Sends the evidence to the bank through the payment provider. |
dashboard |
Records the evidence as sent, for sending in the provider's own dashboard. |
unavailable |
Nothing. The answer is 409 with provider_unavailable. |
With dashboard, the dispute stays needs_response, and submission.note says to send the evidence in the provider's dashboard before the deadline. Coritan holds the payment account, so ask support to send it there.
PayPal takes evidence as one note of up to 2,000 characters. We write uncategorized_text first, and each other field after it with its name as a label. When the note is too long, we leave out refund_policy_disclosure, cancellation_policy_disclosure, refund_refusal_explanation, billing_address, customer_name and service_date, in that order, until it fits, and say how many we left out. If it still does not fit, we cut the end of uncategorized_text, which is where the links are. For a PayPal dispute, put what matters most at the start of uncategorized_text.
Accept a dispute
Section titled Accept a disputeAccept a dispute when you agree with the shopper, or when the order is not worth contesting. The shopper keeps the money, and the chargeback rows stay on your ledger.
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/disputes/31/accept" \
-H "X-API-Key: $ORG_API_KEY" \
-H "Idempotency-Key: dispute-31-accept" \
-H "Content-Type: application/json" \
-d '{"note": "The parcel was lost in transit, so we do not contest this."}'
The answer is 200 with the dispute, now accepted with the outcome lost, and we send commerce.dispute.lost. The note is optional and up to 2,000 characters. PayPal receives it, and Stripe takes no note. You can accept a dispute only while it is needs_response and is not an inquiry, which can_accept shows.
Warning
Accepting a dispute cannot be undone, and you cannot send evidence for it afterwards.
Limits and retries
Section titled Limits and retriesSending evidence and accepting share a limit of 30 a minute for the organization, retries included. Beyond it, the answer is 429 with rate_limited and a Retry-After of 60 seconds.
Both take an optional Idempotency-Key header of up to 128 characters. A retry with the same key within 24 hours gets the first answer back, and we send nothing again. The same key sent for another dispute, or with another note, answers 409 with idempotency_key_reused. While the first request is still running, a retry answers 409 with idempotency_in_progress. A request that fails keeps no key, so a retry with the same key runs again.
Result
Section titled ResultOnce the evidence has gone, the dispute shows evidence_submitted_at and waits in under_review for the bank. The bank's decision moves it to won or lost and sends commerce.dispute.won or commerce.dispute.lost. After a win, GET /commerce/ledger?entry_type=chargeback shows the positive rows that gave the money back.
Troubleshooting
Section titled Troubleshooting409withdispute_not_open- The dispute no longer waits for evidence, and the answer's
statussays where it is. You can change evidence, send it or accept the dispute only while it isneeds_responseand before evidence has gone. 409withevidence_already_submitted- The evidence has gone, and the bank takes one submission. Wait for its decision.
409withevidence_overdue- The bank's deadline, in the answer's
due_by, has passed, and it takes no more evidence. 409withsubmission_in_progress- Another request is sending this dispute's evidence or accepting it. Read the dispute again in a few minutes.
409withprovider_unavailableoraccept_not_supported- We cannot reach the payment account for this dispute right now, or its provider cannot accept a dispute through the API. Ask support.
409withdispute_is_inquiry- An inquiry cannot be accepted. Send evidence, or refund the order.
409withorder_missing- The order the dispute is about no longer exists. Ask support.
409withidempotency_key_reused- You sent the key before for another dispute or with another note. Use a new key for each request.
422withfieldset toevidence- The
messagesays why: a field name we do not know, with the names we take inallowed; no evidence to send, because every field is empty; or more than 150,000 characters together. Give fields back to the assembled text withnull, or shorten them. 429withrate_limited- The organization sent evidence or accepted disputes more than 30 times in a minute. Wait for the time in
Retry-After. 502withpayment_provider_error- The payment provider refused the evidence or the acceptance. Its reason is in the
messageand in the dispute'slast_error. The dispute still waits, so correct what the reason names and try again.
Related
Section titled RelatedAPI operations on this page
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/orgs/{org_slug}/commerce/disputes | Newest first; count is every dispute the filters match |
GET | /api/v1/orgs/{org_slug}/commerce/disputes/{dispute_id} | The dispute, its evidence and what the provider has said about it |
PUT | /api/v1/orgs/{org_slug}/commerce/disputes/{dispute_id}/evidence | Set evidence text fields and links while the dispute waits for them |
POST | /api/v1/orgs/{org_slug}/commerce/disputes/{dispute_id}/submit | Send the evidence to the bank |
POST | /api/v1/orgs/{org_slug}/commerce/disputes/{dispute_id}/accept | Concede the dispute: the shopper keeps the money and the debit stays |