Disputes

These APIs are designed for you to provide your users with the ability to raise transaction disputes directly on Wise with our front-end libraries.

The Reason resource

This resource contains the reason code that you need to pass to the front-end library in order to render the form with the correct information for your users.

codetext

The type of reasons available for the dispute. It can be one of: ATM_DISPENSED_NO_FUNDS, WRONG_AMOUNT, TROUBLE_WITH_GOODS_SERVICES, MERCHANT_CHARGED_AGAIN, NO_REFUND, UNAUTHORIZED. Please use the subOption reason as dispute reason if it is available

descriptiontext

The description of the dispute reason

isFraudboolean

Flag indicating that the dispute reason is fraud-related

tooltiptext

The text to be displayed in the dynamic form when displaying the form to your users

supportsMultipleTransactionsboolean

A boolean flag indicating that the dispute includes multiple transactions

subOptionsarray

Optional list of sub reasons that should be used as the dispute reason

Reason Resource
{
"code": "UNAUTHORIZED",
"description": "I did not make, authorize, or participate in this transaction",
"tooltip": "Choose this if you don't know the merchant or have never purchased anything from them",
"subOptions": [
{
"code": "UNEXPECTEDLY_CHARGED_AGAIN",
"description": "A past merchant unexpectedly charged me again",
"isFraud": false,
"supportsMultipleTransactions": false
},
{
"code": "UNWANTED_SUBSCRIPTION",
"description": "I've been charged for a subscription without my permission",
"isFraud": false,
"supportsMultipleTransactions": false
},
{
"code": "CARD_POSSESSION",
"description": "I don't recognise a transaction",
"isFraud": true,
"supportsMultipleTransactions": true,
"tooltip": "Choose this if you had your card at the time of the transactions, or if you think your card details have been compromised"
},
{
"code": "CARD_NO_POSSESSION",
"description": "My card was lost or stolen",
"isFraud": true,
"supportsMultipleTransactions": true,
"tooltip": "Choose this if you didn't have your card at the time of the transactions"
}
]
}

Retrieve a list of dispute reasons

GET /v3/spend/profiles/{{profileId}}/dispute-form/reasons

Retrieves the list of possible reasons for submitting a dispute.

Example Request
curl -X GET https://api.sandbox.transferwise.tech/v3/spend/profiles/{{profileId}}/dispute-form/reasons \
-H 'Authorization: Bearer <your api token>'

Response

Returns a collection of Reason.

Example Response
[
{
"code": "ATM_DISPENSED_NO_FUNDS",
"description": "I didn't receive the money from the ATM or cash machine",
"isFraud": false,
"supportsMultipleTransactions": false,
"tooltip": "Choose this if the ATM did not dispense the money or gave you less than expected"
},
{
"code": "WRONG_AMOUNT",
"description": "I was charged the wrong amount or currency",
"isFraud": false,
"supportsMultipleTransactions": false,
"tooltip": "Choose this if you were overcharged or the payment was in a different currency than you were expecting"
},
{
"code": "TROUBLE_WITH_GOODS_SERVICES",
"description": "There's a problem with the goods or service I ordered",
"isFraud": false,
"supportsMultipleTransactions": false,
"tooltip": "Choose this if the goods or service never arrived, or if the product was defective or different from what you expected"
},
{
"code": "MERCHANT_CHARGED_AGAIN",
"description": "I got an unexpected charge from a merchant",
"isFraud": false,
"supportsMultipleTransactions": false,
"tooltip": "Choose this for subscription charges, when you've paid twice for one purchase, or when you know the merchant from a past transaction but aren't sure why they charged you"
},
{
"code": "NO_REFUND",
"description": "I haven't received the refund",
"isFraud": false,
"supportsMultipleTransactions": false,
"tooltip": "Choose this when you've been promised a refund and it hasn't arrived"
},
{
"code": "UNAUTHORIZED",
"description": "I did not make, authorize, or participate in this transaction",
"tooltip": "Choose this if you don't know the merchant or have never purchased anything from them",
"subOptions": [
{
"code": "UNEXPECTEDLY_CHARGED_AGAIN",
"description": "A past merchant unexpectedly charged me again",
"isFraud": false,
"supportsMultipleTransactions": false
},
{
"code": "UNWANTED_SUBSCRIPTION",
"description": "I've been charged for a subscription without my permission",
"isFraud": false,
"supportsMultipleTransactions": false
},
{
"code": "CARD_POSSESSION",
"description": "I don't recognise a transaction",
"isFraud": true,
"supportsMultipleTransactions": true,
"tooltip": "Choose this if you had your card at the time of the transactions, or if you think your card details have been compromised"
},
{
"code": "CARD_NO_POSSESSION",
"description": "My card was lost or stolen",
"isFraud": true,
"supportsMultipleTransactions": true,
"tooltip": "Choose this if you didn't have your card at the time of the transactions"
}
]
}
]

Retrieving the dynamic form for disputes

POST /v3/spend/profiles/{{profileId}}/dispute-form/flows/step/{{scheme}}/{{reason}}?transactionId={{transactionId}}

Retrieves the JSON for initiating the dispute flow. This endpoint should be used in conjuction with Wise's Dynamic Flows framework.

The JSON data in the response must be passed into the Dynamic Flows Framework which handles the rest of the multi-step dispute submission including the generation of the subsequent pages (if needed) and the creation of the dispute, along with all the required documents.

A sample implementation of the dynamic flow for Disputes can be found here.

Path and Request Parameters
schemetext

The network of the card that was used to make this transaction. One of MASTERCARD or VISA

reasontext

One of the reason codes returned from the /reasons endpoint.

transactionIdtext

The ID of the transaction that is being disputed. It can be a comma separated list of IDs if the reason code has the supportsMultipleTransactions flag

Request Body
emailtext

Email used to receive communications regarding the dispute from Wise (ex. your support team's email)

Setting up the API

You will need to implement a GET API with the following format:
GET https://{{yourApiUrl}}/v3/spend/profiles/{{profileId}}/dispute-form/flows/step/{{scheme}}/{{reason}}?transactionId={{transactionId}}

This API should forward the call to POST https://{{wiseUrl}}/v3/spend/profiles/{{profileId}}/dispute-form/flows/step/{{scheme}}/{{reason}}transactionId={{transactionId}} along with the request parameters. This is required as the dynamic form returned by Wise will automatically be configured to call your GET API In order to redirect the Dynamic Flows JavaScript library to your domain please use baseUrl or fetcher as part of the dynamic flows setup


See example backend implementation

Example Request
curl -X POST 'https://api.sandbox.transferwise.tech/v3/spend/profiles/{{profileId}}/dispute-form/flows/step/{{scheme}}/{{reason}}?transactionId={{transactionId}}' \
-H 'Authorization: Bearer {{API token}}' \
-H 'Content-Type: application/json' \
-d '{
"email": support@partner.com
}'

Response

Returns information required to populate the form with the correct information. Note how the action field contains the url and method to the next step

Example Response
{
"key": "TROUBLE_WITH_GOODS_SERVICES",
"type": "form",
"title": "There's a problem with the goods or service I ordered",
"actions": [],
"schemas": [],
"layout": [
{
"type": "decision",
"options": [
{
"title": "I never got the goods or service I ordered",
"action": {
"url": "/v3/spend/profiles/12345/dispute-form/flows/visa/no-goods-or-services?transactionId=6789",
"method": "GET"
},
"disabled": false,
"description": "Choose this if the order was cancelled or never arrived"
},
{
"title": "Something is wrong with the goods or service I ordered",
"action": {
"url": "/v3/spend/profiles/12345/dispute-form/flows/visa/something-wrong-what-was-received?transactionId=6789",
"method": "GET"
},
"disabled": false
},
{
"title": "I think there might be an issue with the merchant",
"action": {
"url": "/v3/spend/profiles/12345/dispute-form/flows/visa/scam?transactionId=6789",
"method": "GET"
},
"disabled": false,
"description": "Choose this if you haven't heard from the merchant, or have found scam reviews"
}
]
}
]
}

Submitting the dynamic form

POST /v3/spend/profiles/{{profileId}}/dispute-form/flows/{{scheme}}/{{reason}}

Submits the dynamic form

schemetext

The network of the card that was used to make this transaction. It will be supplied by the dynamic form.

reasontext

Dispute reason code supplied by the dynamic form.

Setting up the API

You will need to implement a POST API with the following format:
POST https://{{yourApiUrl}}/v3/spend/profiles/{{profileId}}/dispute-form/flows/{{scheme}}/{{reason}}

This API should forward the call to POST https://{{wiseUrl}}/v3/spend/profiles/{{profileId}}/dispute-form/flows/{{scheme}}/{{reason}} along with the request body. This is required as the dynamic form returned by us will automatically be configured to call your POST API In order to redirect the Dynamic Flows JavaScript library to your domain please use baseUrl or fetcher as part of the dynamic flows setup


See example backend implementation

Example Request - the body is supplied by the dynamic form
curl -X POST 'https://api.sandbox.transferwise.tech/v3/spend/profiles/{{profileId}}/dispute-form/flows/{{scheme}}/{{reason}}' \
-H 'Authorization: Bearer {{API token}}' \
-H 'Content-Type: application/json' \
-d '{
"form": {
"agreedDetails": {
"agreed": "true"
},
"charged": {
"value": 8,
"currency": "EUR"
},
"expected": {
"value": 5,
"currency": "EUR"
},
"goods": "A book",
"details": "I have not received the book."
},
"disclaimer": {
"filesUploaded": true,
"allAnswered": true
},
"transaction": {
"transactionId": "1045232",
"amount": "50.5 AUD",
"mcc": "5999",
"status": "PENDING",
"merchantName": "Amazon",
"email": "support@partner.com"
}
}'

Response

The response to this last call of the dynamic flow will include the x-df-exit: true HTTP header. This header is used by the JavaScript framework to add an option to exit the dynamic flow and redirect the user to a different page (or exit a WebView depending on the client's implementation). In order to intercept the last page on the frontend, an onClose function should be added to JavaScript, for example:

const onClose = () => {
console.log("DF is exiting");
window.location.href = "https://www.google.com/";
};
Example Response
{
"key": "final",
"type": "form",
"title": "Done!",
"actions": [
{
"title": "Continue",
"exit": true,
"$id": "continue"
}
],
"schemas": [],
"layout": [
{
"width": "md",
"components": [
{
"url": "https://wise.com/web-art/assets/illustrations/email-success-large%402x.png",
"type": "image"
}
],
"type": "box"
},
{
"margin": "lg",
"align": "center",
"type": "info",
"markdown": "Thanks for reporting this transaction. It's pre-authorised right now, but as soon as it becomes \"spent\" we'll begin our investigation."
},
{
"type": "button",
"action": {
"$ref": "continue"
}
}
]
}

Example backend implementation for dynamic forms disputes (Java)


Partner Implementation
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@Slf4j
@RestController
@RequestMapping(value = "/v3/spend/profiles/{profileId}/dispute-form", produces = MediaType.APPLICATION_JSON_VALUE)
public class MyPartnerProxy {
private static String TARGET_BASE_URL = "https://api.sandbox.transferwise.tech";
private static String ACCESS_TOKEN = "Bearer 492b992e-85dd-4671-8095-b0d1d2235d07";
private static String STEP_URL = "/v3/spend/profiles/{profileId}/dispute-form/flows/step/{scheme}/{reason}";
private static String SUBMIT_URL = "/v3/spend/profiles/{profileId}/dispute-form/flows/{scheme}/{reason}";
private WebClient webClient;
public MyPartnerProxy() {
this.webClient = WebClient.builder()
.baseUrl(TARGET_BASE_URL)
.defaultHeader("Authorization", ACCESS_TOKEN)
.build();
}
@GetMapping("/flows/step/{scheme}/{reason}")
public ResponseEntity<Object> getStep(final @Valid @PathVariable("profileId") Long profileId,
final @PathVariable("scheme") String scheme,
final @PathVariable("reason") String reason,
final @RequestParam("transactionId") String transactionId) {
ObjectNode data = JsonNodeFactory.instance.objectNode();
data.put("email", "support@partner.com");
Object stepResponse = webClient.post()
.uri(STEP_URL, uriBuilder -> uriBuilder
.queryParam("transactionId", transactionId)
.build(profileId, scheme, reason)
)
.body(Mono.just(data), ObjectNode.class)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Object.class)
.block();
return ResponseEntity.ok(stepResponse);
}
@PostMapping("/flows/{scheme}/{reason}")
public ResponseEntity<Object> submit(final @Valid @PathVariable("profileId") Long profileId,
final @PathVariable("scheme") String scheme,
final @PathVariable("reason") String reason,
@RequestBody final Object payload) {
Object submitResponse = webClient.post()
.uri(SUBMIT_URL, uriBuilder -> uriBuilder
.build(profileId, scheme, reason)
)
.body(Mono.just(payload), Object.class)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Object.class)
.block();
return ResponseEntity.ok(submitResponse);
}
}

Customizing your Dynamic Form

Wise's Dynamic Forms uses Bootstrap for responsive UI, based on a 12-grid column layout. The defaults for currency selection, amount and file upload are col-xs-12, col-sm-6, and col-xs-12 for the rest. The CSS classes used are listed below. These class selectors can be used to override existing CSS

To override the existing CSS, add a new stylesheet and place the reference to that file after a Bootstrap CSS reference (if any), so that it will override any previous style properties. Alternatively, add !important to a property/value to override ALL previous styling rules for that property. Avoid using this for all properties, as the stylesheet will be large and more difficult to debug. In this code example, modify the CSS file imports in App.js to view the different overriding style sheets.

Please note that some of the form elements also use core Bootstrap classes, so be careful when you modify the styles.

An interactive demo can be found here.

Input labels

classDescription
.control-label.d-inlineAll input labels
.d-inlineText, selection and file upload input labels
.d-inline-blockText input labels
.np-checkbox__textCheckbox text

Input

classElements
.form-groupForm input and labels
.form-controlText input
.d-inline-flexText in dropdown selection
.form-control-placeholderDropdown selection placeholder text
.tw-form-controltextarea input
.droppable, .tw-droppable-md or .droppable-mdFile upload
.droppable-default-card or .droppable-card-contentContent for file upload
.droppable-complete-cardCompleted file upload
.m-b-3Text in file upload block

Button

classElements
.btn.np-dropdown-toggleDropdown selection
.sr-onlyRadio buttons
.promoted-selectionTo select names
.tw-radio-button.checked .tw-radio-checkFor checked buttons
.tw-radio-checkAll radio buttons
.np-radio__textText for radio buttons
.btn.btn-primary.btn-smFile upload buttons
.btn.btn-sm.btn-accentCancel button during file upload
.np-checkbox, .checkboxCheckbox button
.btn.btn-md.np-btn-block.btn-priority-2"Continue" button

Icon

classElements
.chevron-color, or tw-icon-chevron-upArrow icon for dropdown selection
.circle, .circle-sm , .circle-inverse or .tw-icon-uploadFile upload icon
.tw-icon-info-circleInformation icon for alert
.np-checkbox-buttonCheckbox selection icon

Alert

classElements
.alert-detach.alert-dangerAlert for form validation
.d-flex.alert-neutral"You may be responsible for dispute administration fees" alert

Others

classElements
.text-xs-center.m-b-3"I was charged the wrong amount or currency"
.np-drawer-header--title"Search ..." title in currency selection
.np-select-filterSearch bar in currency selection
legend"Please upload" above file upload
.np-popover__content or .np-bottom-sheet--contentPopover content upon clicking information icon beside "Confirmation of the correct price"