Refund an Order
A refund for an order can be made with a single API request to the POST /refunds endpoint. To refund an order, the following requirements must be met:
- You are using a payment method which supports refunds. These are:
- Payment Initiation - EUR only + you must have enabled Bank payment refunds in the Partner System
- Card Payments, Apple Pay, Google Pay, MobilePay, BLIK, BNPL, Financing - refunds are enabled by default
- The funds have arrived to the merchant’s settlement account in Montonio. This typically takes 1 business day.
Additionally, the following constraints have been put in place for refunds:
- The refund can only be made to the original payer
- The total amount of refunds cannot exceed the
grandTotalof the order - The minimum amount for a refund is 0.05 €
Complete Example
This is a complete example of how to refund an order. The process is as follows:
- (optional) Get the Order to check its availableForRefund amount from GET /orders/:orderUuid
- Generate a JWT containing the data for a refund
- Sign the JWT with your
Secret Key - POST the JWT to the API to create a refund
We use JWT to securely pass the refund data to the API and use the JWT’s signature to verify the integrity of the request.
/** * We recommend using the jsonwebtoken NPM package to generate * Json Web Tokens. You can install it with npm: * > npm install jsonwebtoken */const jwt = require('jsonwebtoken');const axios = require('axios');const { randomUUID } = require('crypto');
// For production use https://stargate.montonio.com/apiconst MONTONIO_BASE_URL = 'https://sandbox-stargate.montonio.com/api';const MONTONIO_ACCESS_KEY = 'YOUR_ACCESS_KEY';const MONTONIO_SECRET_KEY = 'YOUR_SECRET_KEY';const MONTONIO_ORDER_UUID = '12228dce-2f7c-4db5-8d28-5d82a19aa3b6';
// 1. Compose the data for the refund// We suggest saving the idempotency key in your database to prevent duplicate refundsconst idempotencyKey = randomUUID();const payload = { "accessKey": MONTONIO_ACCESS_KEY, "orderUuid": MONTONIO_ORDER_UUID, "amount": 1.00, // Please make sure to round the amount to 2 decimal places "idempotencyKey": idempotencyKey}
// 2. Generate the token and sign it with your Secret Keyconst token = jwt.sign( payload, MONTONIO_SECRET_KEY, { algorithm: 'HS256', expiresIn: '10m' });
// 3. Make the requestaxios.post(MONTONIO_BASE_URL + '/refunds', { data: token}).then(response => { console.log(response.data);}).catch(error => { console.log(error.response.data);});Now let’s break it down into step-by-step instructions.
1. Token Contents
First, you need to compose the refund data for the JWT (JSON Web Token) payload.
Token payload
| Key | Required | Type | Description |
|---|---|---|---|
| accessKey | yes | string | Your Access Key obtained from the Partner System. |
| orderUuid | yes | string | The UUID of the order. |
| amount | yes | number | The refundable amount up to 2 decimal places (e.g 19.99). |
| idempotencyKey | yes | string | A unique key that you generate for each refund request. This key is used to recognize subsequent retries of the same request. How you generate the keys is up to you but we recommend using V4 UUIDs. |
| iat | yes | number | The timestamp of when the token was generated. The value must be in seconds since the Unix Epoch. (e.g. 1675860045) |
| exp | yes | number | The timestamp of when the token expires. The value must be in seconds since the Unix Epoch. We recommend setting that to 10 minutes from the time of issuing the token. (e.g. 1675860645) |
2. Signing the JWT
Now, you need to generate and sign a JWT (JSON Web Token) using your Secret Key obtained from the Partner System.
The exact implementation of how to generate the JWT varies by programming language and you can see some examples in the Complete Example section.
- The payload of the JWT is the object described in 1. Token Contents.
- The JWT is signed with your
Secret Keyusing HMAC SHA256 (HS256). - The headers of the JWT must contain the
algandtypkeys. They are described below.
Token headers
| Key | Required | Type | Description |
|---|---|---|---|
| alg | yes | string | Must be set to HS256 |
| typ | yes | string | Must be set to JWT |
We recommend using popular community maintained libraries for JWT generation. You can browse the libraries for your programming language on the jwt.io website.
3. Submitting the Token
After you have generated the token, you can submit it to Montonio’s API to refund the order. The API endpoint is:
POST /refundsThe payload is a JSON string in the following format:
{ "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3NLZXkiOiJZT1VSX0FDQ0VTU19LRVkiLCJvcmRlclV1aWQiOiIxMjIyOGRjZS0yZjdjLTRkYjUtOGQyOC01ZDgyYTE5YWEzYjYiLCJpZGVtcG90ZW5jeUtleSI6IjI4NWIwMDc5LTQwODAtNGJmNC05MTQ4LThjMzUyZTJjN2Y4NyIsImFtb3VudCI6MjV9.G5vVKe256PsZ-_6hHCt2FA-lC_XN2CAp_Q9wN1RTWG8"}Example response:
{ "uuid": "97b20084-319a-4cce-92f5-56d3b41a986a", "amount": 25, "status": "PENDING", "currency": "EUR", "createdAt": "2023-05-23T08:37:55.534Z", "type": "PARTIAL_REFUND"}Some exceptions that can be thrown by the API:
| Code | Description |
|---|---|
| 400 | Order uuid [xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx] already has a refund with same idempotency key |
| 400 | Refund amount [1000] exceeds the total amount refundable [10] |
| 400 | amount is under the min allowed amount: 0.05EUR |
| 401 | STORE_NOT_FOUND - double check your access key |
| 403 | INVALID_TOKEN - double check your secret key |
After the refund is successfully created, you can check its status by calling the GET /orders/:orderUuid endpoint. The refund will be listed in the refunds array of the order.
Webhook notification
When the status of a refund changes, Montonio sends a POST request to the notificationUrl you specified when creating the original Order. The request body contains a signed JWT in the refundToken field. For general information about webhooks, see Listen to webhooks.
{ "refundToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}Refund Token contents
The refundToken is a JWT signed with your Secret Key using HS256. Decoding and verifying it gives you the following fields:
| Key | Type | Description |
|---|---|---|
| refundUuid | string | The UUID of the refund. |
| refundStatus | string | The current status of the refund. See Refund statuses below. |
| refundStatusDescription | string | null | Additional detail about the status. Present when the refund is in PENDING (retrying) or REJECTED status. See Status descriptions below. |
| accessKey | string | Your Access Key. |
| refundAmount | number | The refund amount. |
| orderUuid | string | The UUID of the associated order. |
| iat | number | Token issued-at timestamp (seconds since Unix Epoch). |
| exp | number | Token expiration timestamp. Refund tokens are valid for 7 days. |
Example decoded token:
{ "refundUuid": "a721af46-2dcb-4223-a227-5f85d1606cfe", "refundStatus": "SUCCESSFUL", "refundStatusDescription": null, "accessKey": "MY_ACCESS_KEY", "refundAmount": 33.09, "orderUuid": "4a9115b7-8e55-48f4-bd7e-febc2402e8a0", "iat": 1692180523, "exp": 1692785323}Verifying the Refund Token
Verify the refundToken the same way you verify an orderToken — using your Secret Key:
import jwt from 'jsonwebtoken';
const { refundToken } = req.body;const decoded = jwt.verify(refundToken, 'MY_SECRET_KEY');
if (decoded.refundStatus === 'SUCCESSFUL') { // Refund completed — update your system accordingly} else if (decoded.refundStatus === 'REJECTED') { // Refund failed — check refundStatusDescription for details}<?phpuse \Firebase\JWT\JWT;use \Firebase\JWT\Key;
$refundToken = json_decode(file_get_contents('php://input'))->refundToken;$decoded = JWT::decode($refundToken, new Key('MY_SECRET_KEY', 'HS256'));
if ($decoded->refundStatus === 'SUCCESSFUL') { // Refund completed — update your system accordingly} else if ($decoded->refundStatus === 'REJECTED') { // Refund failed — check refundStatusDescription for details}?>import jwtimport json
body = json.loads(request.body)refund_token = body['refundToken']decoded = jwt.decode(refund_token, 'MY_SECRET_KEY', algorithms=['HS256'])
if decoded['refundStatus'] == 'SUCCESSFUL': # Refund completed — update your system accordingly passelif decoded['refundStatus'] == 'REJECTED': # Refund failed — check refundStatusDescription for details passRefund statuses
| Status | Description |
|---|---|
| PENDING | The refund has been created and is awaiting processing. If a previous attempt failed (e.g. due to insufficient funds), the system will retry automatically. |
| PROCESSING | The refund is currently being processed. |
| SUCCESSFUL | The refund has been completed successfully. |
| REJECTED | The refund has permanently failed. Check refundStatusDescription for the reason. |
| CANCELED | The refund was canceled. |
Refund status descriptions
When a refund is in PENDING (retrying) or REJECTED status, the refundStatusDescription field provides additional detail:
| Description | Meaning |
|---|---|
| INSUFFICIENT_FUNDS | The settlement account lacks sufficient funds to process the refund. |
| REFUND_EXCEEDS_ORDER_PAID_AMOUNT | The refund amount exceeds the paid amount of the order. |
| DECLINED | The payment provider declined the refund. |
| EXPIRED_OR_CANCELLED_CARD | The customer’s card has expired or been cancelled. |
| LOST_OR_STOLEN_CARD | The customer’s card has been reported lost or stolen. |
| EXPIRED | The refund could not be processed after all retry attempts were exhausted. |
| OTHER | The refund failed for an unspecified reason. |