Refunds
Refunds allow the reversal of payments made using either Pay By Bank, Card or Wallets. Multiple partial refunds can also be created up to the amount of the original payment.
Please note that refunds can only be made for pay-ins, and will be to the same destination where the pay-in originated. The destination for a refund cannot be changed.
Creating a Refund
To issue a refund for a particular payment, you'll need to ensure that the following conditions are met:
- A refund must be associated with a Stitch payment request for Pay By Bank and Card.
- The payment must be in the completed state.
- The refund amount must be less than or equal to the original payment amount.
Refund creation is protected by a client token. You'll need to follow the steps described in the client token guide
to obtain a client token with the client_refund
scope.
To create the request, a GraphQL mutation is used to specify:
- The requested refund amount
- The refund reason
- A nonce to determine uniqueness
- A reference that the beneficiary of the refund will see on their statement (for Pay By Bank refunds)
- The ID of the payment request being refunded
The GraphQL API URL https://api.stitch.money/graphql can be used for all refund requests (whether on test or live clients).
As shown below, the refund mutation will be slightly different per product, depending on whether you are refunding a Pay by Bank, Card, or Wallet payment.
- Initiate a Pay By Bank Refund
- Initiate a Card Refund
- Initiate a Wallet Refund
For Pay By Bank refunds, please note that the funds to refund the transaction will be debited from an associated intermediary float account.
Although a refund may be created at any point in time, Stitch will only process it once the original payment amount has cleared. In some cases, it could take up to 3 days for money to reflect in the destination account.
If the refund is made immediately after the payment is completed, it could take significantly longer to reflect (i.e. 3 days to clear in our account and another 3 days to clear in the destination beneficiary's account).
The refund clearing type allows you to specify either "INSTANT" or "DEFAULT". Leaving it empty will default to the latter. See the table below for more information.
Refund Clearing Types
Type | Description |
---|---|
INSTANT | Attempt to clear this payment with the bank immediately, within bank processing hours. If submitted after bank cutoff, it will rollover to the next business day. This method may incur an additional cost. Note: za_bank_zero and za_olympus_mobile do not support the INSTANT type. |
DEFAULT | Attempt to clear this payment with the bank on the same day. If submitted after the bank's cutoff time, it will rollover to the next business day. It can take up to 3 days to clear in the destination beneficiary's account. |
You will need the refund id
from the response to look up the status of the request in the API, so it should be retained for
later usage.
The refund id
will also be included in the webhook subscription payload. This is illustrated in the refund webhooks
section below.
Refunds for card payments can be completed using the ID of the original payment. Refunds will be deducted from the total amount of the next settlement.
The originalPaymentId
input in the mutation accepts either a payment initiation request ID or a card transaction ID.
Refunds for wallet payments can be completed using the ID of the original payment.
The originalPaymentId
input in the mutation accepts either a Apple Pay, Samsung Pay or Google Pay™ transaction ID.
Retrieving Refund Status
When receiving a callback from the refund creation, you will likely want to check the status of the refund. Using the
query below you can retrieve the status of a given refund by id
.
To retrieve the status of a refund, as described above, you'll need a client token with the client_refund
scope.
- Pay By Bank Refund Status
- Card Refund Status
- Wallet Refund Status
As an alternative to querying a refund by its id
, it is possible to query by the nonce
value that was provided when creating
the refund. This is done by specifying the nonce
as a filter, as shown in the example below:
Retrieving All Refunds
Should you want to list all refunds attempted on your client, you can run the following query, provided you have a client
token with the client_refund
scope.
You can also further refine the above query to only show refunds in a certain status. For example, the below query will
list all refunds in the status RefundError
. For more options, you can filter
at the top level on the refunds
node, just like in the example below.
Refund Statuses
- Pay By Bank Refund Statuses
- Card Refund Statuses
- Wallet Refund Statuses
When a Pay By Bank refund is initially created, the request's status will be RefundPending
. Once the refund is successfully requested at the bank,
to be made back to the payer, this status will change to RefundSubmitted
.
Once successfully processed at the bank and funds have left the intermediary account, the status will be updated to RefundCompleted
.
The RefundPaused
status indicates that we can no longer continue processing the refund until certain conditions are met.
These conditions are outlined in the table below.
Reason | Description |
---|---|
insufficient_funds | There are insufficient funds in the account. |
The RefundError
status indicates that the refund has failed, and includes a reason
explaining the cause. Possible failure reasons are detailed below:
Reason | Description |
---|---|
bank_error | There was a problem communicating with the processing bank. |
bank_processing_error | An internal error occurred inside the bank while trying to process the transaction. |
insufficient_funds | The account from which the payout was to be made had insufficient_funds for longer than the allowed duration |
restricted_account | There was a restriction on the account. This could be anything from FICA compliance to the account being manually frozen. |
inactive_account | The account is inactive. This can be remedied by reactivating it. |
exceeded_limit | A limit was exceeded. |
invalid_account | The account was not found. It may have been closed or cannot process this type of transaction. |
beneficiary_bank_processing_error | The recipient bank did not accept the transaction. Common causes are the beneficiary bank not responding to an RTC payment within the required 60 seconds, or not being enrolled for RTC. |
invalid_transaction_details | The details (e.g. account number or branch code) were rejected by the bank. |
payment_not_received | The payment associated with the refund has not yet been received. |
invalid_destination | (Deprecated) The provided beneficiary account is invalid. |
internal_error | (Deprecated) An unknown error has occurred processing the payout into the provided beneficiary account. |
When a card refund is initially created, the request's status will be TransactionPending
. Once the refund is successfully processed,
this status will change to TransactionSuccess
.
For card refunds, we'll process the request and immediately return the "final" status for the request. The response from the Stitch API will always contain the final status.
The TransactionFailure
status indicates that the refund failed to be initiated successfully, and includes a reason
explaining the cause.
Some of the possible failure reasons are detailed below:
Reason | Description |
---|---|
exceedsOriginalAmount | The amount you're trying to refund exceeds the original amount in the transaction |
invalidTransactionError | The card transaction you're trying to initiate a refund for isn't completed |
transactionDisputed | The refund processing has been declined due to the card transaction being disputed |
When a wallet refund is initially created, the request's status will be TransactionPending
. Once the refund is successfully processed,
this status will change to TransactionSuccess
.
For wallet refunds, we'll process the request and immediately return the "final" status for the request. The response from the Stitch API will always contain the final status.
The TransactionFailure
status indicates that the refund failed to be initiated successfully, and includes a reason
explaining the cause.
Some of the possible failure reasons are detailed below:
Reason | Description |
---|---|
exceedsOriginalAmount | The amount you're trying to refund exceeds the original amount in the transaction |
invalidTransactionError | The wallet transaction you're trying to initiate a refund for isn't completed |
Subscribing to Refund Webhooks
To receive a webhook upon payment completion or failure you will need to create a subscription for your client. Please note that this will always send a signed webhook for your refund requests. You can read more about how to verify the signature within the webhook event in our more detailed guide here
- Pay By Bank Refund Webhook Subscription
- Card Refund Webhook Subscription
- Wallet Refund Webhook Subscription
If the subscription is successfully created, the body returned by the request will look like the sample in the Example Response
tab in widget above. The value in the secret
field will be used to verify the webhook event signature, and should be kept
in a secure place like an environment variable.
For more information on receiving webhook events, listing active webhook subscriptions, unsubscribing from webhooks and validating signed webhook subscriptions, please visit the Webhooks page.
Webhook Statuses
The refund
webhook will be dispatched for each of the following status updates:
RefundSubmitted
RefundCompleted
RefundError
RefundPaused
Example Payload
{
"data": {
"client": {
"refunds": {
"node": {
"__typename": "Refund",
"amount": {
"currency": "ZAR",
"quantity": "1"
},
"beneficiaryReference": "testing",
"created": "2022-10-11T11:54:13.710Z",
"id": "cmVmdW5kLzFmY2Q0N2YwLWYyMTItNGFlYy04ZjA2LTY4ZDZhYzQzZDg5Mg==",
"nonce": "6525e472-b70b-4c38-83e9-ee6367919180",
"paymentInitiation": null,
"paymentInitiationRequest": {
"__typename": "PaymentInitiationRequest",
"amount": {
"currency": "ZAR",
"quantity": "1"
},
"bankBeneficiaries": [
{
"__typename": "BankBeneficiary",
"accountNumber": "1223273660",
"bankAccountNumber": "1223273660",
"bankId": "nedbank",
"name": "FizzBuzz Co."
}
],
"beneficiaries": [
{
"__typename": "BankBeneficiary",
"accountNumber": "1223273660",
"bankAccountNumber": "1223273660",
"bankId": "nedbank",
"name": "FizzBuzz Co."
}
],
"beneficiaryReference": "KombuchaFizz",
"created": "2022-10-11T11:52:49.230Z",
"createdAt": "2022-10-11T11:52:49.230Z",
"currency": "ZAR",
"events": [],
"externalReference": "5596d0a6-8072-4426-ab59-38ad45ef671c",
"id": "cGF5cmVxLzIxZmMyMWFiLTE1N2UtNGI3MC1iMDNiLTY3MDZmYTkxM2IyNw==",
"payerConstraints": null,
"payerReference": "Joe-Fizz-01",
"paymentConfirmation": null,
"quantity": "1",
"refunds": [],
"state": {
"__typename": "PaymentInitiationRequestCompleted",
"amount": {
"currency": "ZAR",
"quantity": 1
},
"beneficiary": {
"__typename": "BankBeneficiary",
"accountNumber": "1223273660",
"bankAccountNumber": "1223273660",
"bankId": "nedbank",
"name": "FizzBuzz Co."
},
"date": "2022-10-11T11:53:48.466Z",
"payer": {
"__typename": "PaymentInitiationBankAccountPayer",
"accountName": "FNB Premier Cheque Account",
"accountNumber": "62120098985",
"accountType": "current",
"bankId": "fnb"
},
"proofOfPayment": null
},
"updated": "2022-10-11T11:53:48.478Z",
"updatedAt": "2022-10-11T11:53:48.478Z",
"url": "https://secure-local.stitchmoney.com/connect/payment-request/21fc21ab-157e-4b70-b03b-6706fa913b27",
"userReference": "Joe-Fizz-01"
},
"reason": "fraudulent",
"status": {
"__typename": "RefundCompleted",
"date": "2022-10-11T11:54:14.482Z",
"expectedSettlement": "2022-10-13T11:54:14.482Z"
}
}
}
}
}
}
Error Handling for Refunds
Errors may arise when a refund is not requested correctly, or with information that aligns with the original payment request to be refunded. An example response of a failed refund creation can be found below:
{
"errors": [
{
"message": "The payment associated with this refund is pending. The payment must be completed before a refund can be issued.",
"locations": [
{
"line": 77,
"column": 3
}
],
"path": ["clientRefundInitiate"],
"extensions": {
"code": "PAYMENT_INCOMPLETE",
"reason": "PENDING"
}
}
],
"data": null
}
The below table describes possible errors, messages, and reasons:
Scenario | Error Code | Message | Reason (if applicable) |
---|---|---|---|
Refund requested for non-existent payment | NOT_FOUND | Could not find paymentRequest for this refund | |
Refund requested with previously-used nonce | NONCE_DUPLICATE | A refund request with this nonce has already been issued. Expected nonce to be unique. | |
Refund requested for payment that is still pending | PAYMENT_INCOMPLETE | The payment associated with this refund is pending. The payment must be completed before a refund can be issued. | PENDING |
Refund requested for payment that is cancelled | PAYMENT_INCOMPLETE | The payment associated with this refund was marked as cancelled. The payment must be completed before a refund can be issued. | CANCELLED |
Refund requested for payment that is expired | PAYMENT_INCOMPLETE | The payment associated with this refund is expired. A refund cannot be issued. | EXPIRED |
Refund amount requested is higher than original payment amount | BAD_USER_INPUT | Expected refund amount to be less than or equal to the original payment amount. | |
Refund amount requested is higher than total amount of all refunds for payment | BAD_USER_INPUT | Expected sum total of requested refunds amount to be less than or equal to the original payment amount. | |
No intermediary account is setup to process refunds from | ACCOUNT_NOT_FOUND | No intermediary account details have been configured for this client. Please contact support@stitch.money to resolve this issue. |
Simulating Refund Scenarios
To simulate some common scenarios when using your test client, see the table below with requirements to simulate each scenario.
Simulation Scenario | Simulated Status | Simulation Requirements |
---|---|---|
A successful refund | RefundCompleted | -> amount.quantity < 400 -> Wait approximately 2 minutes before querying the refund status |
A failed refund due to a bank processing error | RefundError , with reason bank_processing_error | -> amount.quantity = 400 -> Wait approximately 2 minutes before querying the refund status |
A failed refund due to an inactive account | RefundError , with reason inactive_account | -> amount.quantity = 401 -> Wait approximately 2 minutes before querying the refund status |
A failed refund due to an invalid account | RefundError , with reason invalid_account | -> amount.quantity = 402 -> Wait approximately 2 minutes before querying the refund status |
A paused refund | RefundPaused , with reason insufficient_funds | -> amount.quantity > 404 |
Top up of a float account, to allow a paused refund to continue to be successfully processed | RefundPaused initially, which becomes RefundCompleted | -> amount.quantity = 404 -> Wait approximately 3 minutes before querying the refund status |
Postman Collection
Disbursement requests can be created and tested using the Postman collection available here. These can be used by specifying your client credentials and supplying the custom request variables where required.