Skip to main content

Refund Workflow

Refund Workflow Diagram

1. Create Refund Request

To quote a refund you must create a RefundRequestDisplay, which is an object that describes an intent to quote a ticket refund.

To do so, use mutation/refunds/createRefundRequests. The only required information is the TicketNum (13 digit ticket number). Additional optional fields can be included (e.g. waiver or free text which make tickets fully refundable regardless of their specific fare rule).

This mutation will trigger a process that retrieves all the necessary information for our algorithms to calculate the refundable amount of any TicketNum. Our algorithm will fetch the ticket details (coupon details, fare rules, tax rules, reservation history, and more).

This process runs asynchronously in an internal queue, which means that the mutation will return immediately with either:

  • An empty response (Unit), meaning the request has been received correctly and the quote is being calculated. This would look like:
{
"data": {
"refunds": {
"createRefundRequests": {}
}
}
}
  • An error in case the request was not received properly. For example, if authentication was not done properly:
{
"data": {
"refunds": null
},
"errors": [
{
"message": "Effect failure",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"refunds"
],
"extensions": {
"authError": "Something went wrong when authenticating"
}
}
]
}

If the mutation was successful, an object called RefundRequestDisplay will be created with a RequestInProcess status. This object can be retrieved using query/refunds/searchRefundRequests (detailed in the next section).

When the quote process has completed, the RequestInProcess status will change. You can react to this event using 2 strategies:

  1. Receive a webhook notification which includes the new status and the TicketNum. If the RefundRequestDisplay status is of type RefundCandidateDisplay.status it means that a refund quote object, also known as RefundCandidateDisplay, was created and can be queried using query/refunds/getRefundCandidate. If you are planning to use webhooks, first ask our integrations team to enable the webhook for you.
  2. Polling query/refunds/searchRefundRequests, this is the least efficient option and therefore is not recommended. Too many searchRefundRequests requests will slow down the API. Use it only for testing purposes or if your infrastructure does not support the above option.
note

PNRs can also be used to create a RefundRequestDisplay, but we do not recommend it as PNRs are purged from GDSs faster/earlier than TicketNums, and therefore the success rate will be lower.

Example:
mutation {
refunds {
createRefundRequests(
refundRequests: [
{
ticketNum: "1231234567891",
isEmd: false
},
{
ticketNum: "2231234567891",
waiver: "COVID19"
isEmd: false
},
{
ticketNum: "3231234567891",
metadata: "This metadata will only be returned and is useful for stateless flows"
isEmd: false
}
]
)
}
}
note

Example responses for multiple scenarios can be found at the end of the document.

2. Search Refund Requests

To fetch a RefundRequestDisplay use the query/refunds/searchRefundRequests. You can use a single TicketNum or multiple TicketNums. Results can then be filtered using a set of parameters (e.g. date, IATA code, user code, etc). Each RefundRequestDisplay has a status property which is an Either object (meaning it has two properties - left or right - and only one of them must be not null). You can interpret them as:

  • Left value is not null, which means that a RefundCandidateDisplay doesn’t exist, the possible values are:

  • Right value is not null, this means that a RefundCandidateDisplay exists and the value is a RefundCandidateDisplay.status. By default, the RefundCandidateDisplay.status will be REVIEW, but it also can be SKIP. All possible values are:

    • Review: the refund candidate is awaiting review and confirmation.
    • Skip: a refund candidate is skipped automatically if the refund amount is less than 0 or no unused flight segments were found. A user or client may also skip any refund candidate manually.
    • Ready: the refund candidate is confirmed and can be sent for processing.
    • Refunded: a refund request was created in the specified refund route (either BSP or GDS).
    • Error: there was an error when trying to create the refund request.
Examples:

Search multiple tickets:

query {
refunds {
searchRefundRequests(
ticketNums: ["1231234567891", "2231234567891", "3231234567891"]
) {
ticketNum
requestedBy # Check query/users/getUsers to map the IDs
metadata
eitherRefundStatus {
left
right
}
}
}
}

Search by date range:

query {
refunds {
searchRefundRequests(
fromDate: "2022-01-01"
toDate: "2022-01-01"
) {
ticketNum
requestedBy
metadata
eitherRefundStatus {
left
right
}
}
}
}

2.1 Refund request webhook

Whenever the Refund Request status changes, a webhook notification can be triggered and received by your application endpoint to activate a reactful flow. This notification will look like:

{
"data": {
"ticket_num": "0000000000000",
"status": "REFUND_CANDIDATE_READY"
},
"version": "1.0",
"type": "refund_request.status_updated",
"event_id": "12323124151",
"created": "2025-02-18T15:45:21+00:00"
}

The status will reflect the new current status of the Refund Request, which can have 3 values:

  • REFUND_CANDIDATE_READY: the refund quote process ended succesfully and a RefundCandidateDisplay can be retrieved using query/refunds/getRefundCandidate.
  • INSERTED_AND_CREATED_REFUND_CANDIDATE_READY: equal to REFUND_CANDIDATE_READY.
  • ERROR_PROCESSING_REQUEST: an error occured during the refund quote process, automatic flows generally end here and our support team can be contacted if needed to review the case.

3. Get Refund Candidate

To retrieve a RefundCandidateDisplay use the query/refunds/getRefundCandidate specifying a single TicketNum. This object contains all the information of a refund quote, including coupon details, fare rules and the itemized refund amounts. Although the RefundCandidateDisplay has all the information needed to process the refund, overrides can be done using the mutation of the next section (4 UpdateRefundCandidate).

Common elements that are generally requested include:

Refund amounts
query {
refunds {
getRefundCandidate(ticketNum: "1231234567891") {
ticketSummary {
ticketNum
issueDate
parentTicketNum # if not null then the ticket is a reissue
}
refundCandidateSummary {
amountsSummary {
baseFare { # base fare paid by passenger
amount
currencyCode
}
fareToRefund {
amount
currencyCode
}
taxesPaid {
amount
currencyCode
}
taxesToRefund {
amount
currencyCode
}
cancellationPenalty {
amount
currencyCode
}
fixedCancellationFee { # this penalty is additional to cancellationPenalty, can be customized
amount
currencyCode
}
adminFee { # this penalty is additional to cancellationPenalty, can be customized
amount
currencyCode
}
totalRefund {
amount
currencyCode
}
cashRefund { # amount of totalRefund that should be refunded to CASH
amount
currencyCode
}
cardRefund { # amount of totalRefund that should be refunded to CARD
amount
currencyCode
}
agencyFee { # does not affect totalRefund, it's customizable and serves as metadata for the user
currencyCode
}
}
}
}
}
}
{
"data": {
"refunds": {
"getRefundCandidate": {
"ticketSummary": {
"ticketNum": "1231234567891",
"issueDate": "2022-01-20",
"parentTicketNum": null
},
"refundCandidateSummary": {
"amountsSummary": {
"baseFare": {
"amount": 874.15,
"currencyCode": "BRL"
},
"fareToRefund": {
"amount": 874.15,
"currencyCode": "BRL"
},
"taxesPaid": {
"amount": 519.16,
"currencyCode": "BRL"
},
"taxesToRefund": {
"amount": 519.16,
"currencyCode": "BRL"
},
"cancellationPenalty": {
"amount": 664.495,
"currencyCode": "BRL"
},
"fixedCancellationFee": {
"amount": 0,
"currencyCode": "BRL"
},
"adminFee": {
"amount": 0,
"currencyCode": "BRL"
},
"totalRefund": {
"amount": 728.81,
"currencyCode": "BRL"
},
"cashRefund": {
"amount": 728.81,
"currencyCode": "BRL"
},
"cardRefund": {
"amount": 0,
"currencyCode": "BRL"
},
"agencyFee": null
}
}
}
}
}
}

Refund Candidate Badges

Refund Candidate Badges are flags that give key insights into certain attributes of the refund calculation.

query {
refunds {
getRefundCandidate(ticketNum: "1231234567891") {
ticketSummary {
ticketNum
}
badges {
usageBadge {
status
message
}
fareRefBadge {
status
message
}
quoteExpiryBadge { # Indicates if a quote is still valid at the time it was fetched
status
message
}
quoteSafetyBadge { # Indicates if the quote is safe to refund
status
message
}
guaranteedRefundBadge {
status
message
}
}
}
}
}
{
"data": {
"refunds": {
"getRefundCandidate": {
"ticketSummary": {
"ticketNum": "1231234567891"
},
"badges": {
"usageBadge": {
"status": "Success", # Success = FULLY OPEN, Warning = PARTIALLY OPEN, Danger = USED
"message": "Ticket Usage"
},
"fareRefBadge": {
"status": "Success",
"message": "Fare is Refundable"
# Possible values are:
# Fare is Refundable
# Fare probably Refundable (same as Fare is Refundable)
# Fare is NOT Refundable
# Fare probably NOT Refundable (same as Fare is NOT Refundable)
# Fare is Refundable, taken from a more general rule (same as Fare is Refundable)
# Fare probably Refundable, taken from a more general rule (same as Fare is Refundable)
# Fare probably NOT Refundable, taken from a more general rule (same as Fare is NOT Refundable)
# Unknown Fare Refundability
},
"quoteExpiryBadge": {
"status": "Success",
"message": "Quote is up to date"
# Possible values are:
# Quote is up to date
# Quote is not up to date, review fare rule conditions
},
"quoteSafetyBadge": {
"status": "Success",
"message": "Quote is safe to refund"
# Possible values are:
# Quote is safe to refund (this is SAFE)
# No open segment (this is considered SAFE as the quote is correct, but nothing can be refunded)
# Unable to determine refundability (this is NOT safe)
# A tax was not calculated properly, please review (this is NOT safe)
# Review fare details, fare construction and tax calculation (this is NOT safe)
# Quote is out of date (this is NOT safe)
},
"guaranteedRefundBadge": null
}
}
}
}
}

Refund waivers and free text

The waiver and freeText fields are used to add additional context to the refund application sent to the airline.

By default, if a RefundCandidateDisplay has a non-null waiver or freeText then the quote is considered involuntary, which means that the fare rule is ignored and the corresponding amounts of the unused flight segments will be refunded. This logic can be disabled by changing the waiverForceFareRefundWithoutPenalty to false.

The waiver must have a maximum of 14 characters, and on fulfillment is added directly to the waiver fields of BSPLink, Sabre, Amadeus and Travelport refund forms.

The freeText is mapped to different refund application fields depending on the refund route:

  • BSPLink —> Refund Reason (the fulfillment process already adds a standard refund message, the freeText would be added as additional context so most of the times no freeText is needed). For context, our standard refund messages include:
    • Refund with freeText: Requesting refund without penalty for the reason.
    • FULL refund without penalty: Requesting full refund.
    • PARTIAL refund without penalty: Requesting partial refund of fare and taxes.
    • FULL refund with penalty: Requesting full refund, fare rules indicate a refund penalty.
    • PARTIAL refund with penalty: Requesting partial refund of fare and taxes, fare rules indicate a refund penalty.
    • TAX refund: Requesting refund of airport use taxes.
  • Sabre —> Free text (the Sabre refund screen WFR only allows 40 non-special characters, and no spaces are allowed either)
  • Amadeus —> Refund Remark
  • Travelport —> Refund Remark

Finally, the waiver and freeText can be added by default if you provide us with the business logic, and we’ll integrate it into our algorithm. It’s important to mention that by default, involuntary schedule changes or no-protections detected in the reservation history will result in the waiver REQ INV WAIVER being added into RefundCandidateDisplay in order to force an involuntary refund. This works as a flag for your API client to detect, this way you can contact the airline to request the correct waiver for the refund application, and update it into the RefundCandidateDisplay.

query {
refunds {
getRefundCandidate(ticketNum: "1231234567891") {
ticketSummary {
ticketNum
}
refundCandidateSummary {
waiver
freeText
waiverForceFareRefundWithoutPenalty
}
}
}
}
{
"data": {
"refunds": {
"getRefundCandidate": {
"ticketSummary": {
"ticketNum": "1231234567891"
},
"refundCandidateSummary": {
"waiver": "REQ INV WAIVER",
"freeText": null,
"waiverForceFareRefundWithoutPenalty": true
}
}
}
}
}

4. Update Refund Candidate

Some fields of the RefundCandidateDisplay can be modified, usually through a custom frontend UI or Deal Engine’s UI. For a robotic refund flow, this mutation is generally used to confirm a refund quote. This confirmation is triggered when the passenger or agent decides to go ahead and process the refund, but before doing so, the RefundCandidateDisplay status must be changed to READY.

To update a RefundCandidateDisplay use the mutation/refunds/updateRefundCandidate You will need to add the TicketNum and add the transformation that you require:

Change RefundCandidateDisplay status
mutation {
refunds {
updateRefundCandidate(
ticketNum: "1231234567891"
refundCandidateStatus: { from: REVIEW, to: READY }
taxes: []
segments: []
rules: []
)
}
}
Change RefundCandidateDisplay waiver, free text and refund route
mutation {
refunds {
updateRefundCandidate(
ticketNum: "1231234567891"
waiverCode: { from: "REQ INV WAIVER", to: "COVID19" }
freeText: { from: null, to: "UN/TK" }
refundRoute: { from: BSP, to: GDS }
waiverForceFareRefundWithoutPenalty: { from: false, to: true }
taxes: []
segments: []
rules: []
)
}
}

5. Process Refund

After a RefundCandidateDisplay is changed to READY, the refund quote can be fulfilled using mutation/refunds/processRefundNow Similar to createRefundRequests, this will return immediately and will be queued for an asynchronous process. The same stategies can be used to retrieve the next status (using query/refunds/getRefundCandidate instead of query/refunds/searchRefundRequests).

The RefundCandidateDisplay status will change to REFUNDED when a Refund Application is created on BSPLink or submitted via GDS.

Please note that refund routes are configured in the tool during integration and are set by the airline, refund type or any other parameter. Our technology will automatically select the right refund route given any specific ticket.

Alternatively, the RefundCandidateDisplay status can also change to ERROR, indicating the refund fulfillment process failed.

Example:
mutation {
refunds {
processRefundNow(ticketNum: "1231234567891") {
ticketSummary {
ticketNum
}
}
}
}

5.1 Update Refund Display webhook

A webhook notification can also be enable for RefundCandidateDisplay status changes which will look like:

{
"data": {
"ticket_num": "0000000000000",
"status": "REFUNDED"
},
"version": "1.0",
"type": "refund_candidate.status_updated",
"event_id": "12323124151",
"created": "2025-02-18T15:56:05+00:00"
}

The status will follow the RefundCandidateDisplay status enums.

6. Request Refund Report

The query/refunds/searchRefundDetails will return all the information of the processed refunds and can be used for reporting or auditing purposes. Keep in mind that RefundCandidateDisplays are mutable and amounts can change with time, and therefore should not be used for reporting or auditing purposes.

Refund details can be requested by TicketNums, refund application request dates (requestedAt), or refund application update dates (updatedAt):

Example using TicketNums:
query {
refunds {
searchRefundDetails(
dates: {
requestedAt: null
updatedAt: null
}
ticketNums: ["1231234567891", "1231234567892"]
) {
ticketNum
iataCode
commissionPercent
via
product
requestDate
authorizationDate
fopType
totalRefund
totalFare
totalTax
totalPenalty
currencyCode
document
route
status
rejectionDate
updatedDate
settlementPeriod
taxes {
code
amount
currencyCode
}
isVoluntaryRefund
agencyFeeAmount
agencyFeeCurrency
}
}
}
Example using requestedAt:

This is useful if you need a report of refund applications requested within a date range.

query {
refunds {
searchRefundDetails(
dates: {
requestedAt: { fromDate: "2022-09-14", toDate: "2022-09-16" }
updatedAt: null
}
ticketNums: []
) {
ticketNum
iataCode
commissionPercent
via
product
requestDate
authorizationDate
fopType
totalRefund
totalFare
totalTax
totalPenalty
currencyCode
document
route
status
rejectionDate
updatedDate
settlementPeriod
taxes {
code
amount
currencyCode
}
isVoluntaryRefund
agencyFeeAmount
agencyFeeCurrency
}
}
}
Example using updatedAt:

Same as using requestedAt plus any BSPLink refund application that had their refund status updated within a date range.

query {
refunds {
searchRefundDetails(
dates: {
updatedAt: { fromDate: "2022-09-14", toDate: "2022-09-16" }
requestedAt: null
}
ticketNums: []
) {
ticketNum
iataCode
commissionPercent
via
product
requestDate
authorizationDate
fopType
totalRefund
totalFare
totalTax
totalPenalty
currencyCode
document
route
status
rejectionDate
updatedDate
settlementPeriod
taxes {
code
amount
currencyCode
}
isVoluntaryRefund
agencyFeeAmount
agencyFeeCurrency
}
}
}

Additional response examples