Status & error reference
One place for every status value and error a client can receive, and (critically) which field or query returns it.
There is one status vocabulary. NEW, REVIEW, READY, REFUNDED and ERROR mean the same thing wherever you read them: on a refund candidate, on Quote.status, on Document.status, on Request.status, and in the status field of a webhook payload. They are always uppercase. A value you recognise on one of those fields is the same state on all of them.
Three things on this page are not that vocabulary, and are the only distinctions you still have to keep straight:
- The quote-stage event values delivered by the
refund_request.status_updatedwebhook (section 2); event progress, not an entity status. - The webhook delivery status (section 5); whether Deal Engine reached your endpoint, lowercase, nothing to do with refunds.
- Coupon / segment / tax statuses (section 6): an airline-side vocabulary with its own per-PSS mapping.
How to read this page
- Branch on the five-value enum for any refund state. Sections 1 and 3.
- Treat unknown values as non-terminal and keep polling. Stop only on
REFUNDEDandERROR. - Where a value set is not published, this page says so explicitly rather than guessing. Get the authoritative list for your tenant by introspecting the schema (see GraphQL conventions) or by asking Deal Engine.
Per-tenant configuration applies to rules, not to the status enum. Decisions, rejection behaviour and the non-refundable coupon list are configured per customer and recorded in your FSD (Functional Specification Document). The five status values are not; see Refund lifecycle & statuses.
Vocabulary map
| Vocabulary | Exact field / query that returns it | Enumerated below? |
|---|---|---|
| Refund status (the enum) | createQuote (with Sync: true) → refundCandidate.status; document(ticketNum) { status } and { quote { status } }; documents(filter) { edges { node { status } } }; requests(filter) { edges { node { status } } }; both webhook payloads' status. Settable via updateQuote(request: { refundCandidateStatus: { from, to } }) | Yes; 5 values, section 1 |
| Quote-stage event values | refund_request.status_updated webhook → data.status | Yes; 5 values, section 2 |
| Webhook delivery status | webhookEvents { items { status } }, and the filters.status argument | Partly; one observed value, section 5 |
| Coupon / segment / tax status | refundCandidate.extraParams.segments[].couponStatuses[], .segmentStatus, refundCandidateTaxes[].status | Mapping lives on Refund lifecycle & statuses |
1. The refund status enum
| Status | Meaning | Terminal? | Who can set it |
|---|---|---|---|
NEW | Received; the ticket is in Deal Engine and quotation has not produced a validated outcome yet | No | Engine |
REVIEW | The quotation output needs the customer's validation before it can be processed | No | Engine or client |
READY | Deal Engine is confident the refund meets the functional rules agreed with you in your FSD | No | Engine or client |
REFUNDED | Executed successfully in the airline system | Yes | Engine only |
ERROR | Execution failed after acceptance | Yes | Engine only |
- Fulfillment only executes candidates in
READY.NEWandREVIEWare never refunded. REFUNDEDandERRORare written only by the fulfillment engine. A client can never set a terminal status.- Terminal statuses never transition again; re-submitting the same ticket does not change them.
ERRORcarries failure detail in its error history. Contact support with the ticket number when it is not actionable on your side.REFUNDEDis unreachable in sandbox; fulfillment does not run there. See Testing & environments.
Why a refund sits in REVIEW: Deal Engine lacks an ingredient it needs to guarantee the quotation; for example the reservation was purged so the full quotation cannot be determined, or the fare rules are private and unreachable from the PSS. Full explanation: Refund lifecycle & statuses §2.
1.1 Legal transitions
| From | To | Actor | Trigger |
|---|---|---|---|
| (none) | NEW | Engine | Candidate created from a quote |
NEW | REVIEW | Engine or client | The quotation needs your validation |
NEW | READY | Engine or client | Quotation satisfies your FSD rules |
REVIEW | READY | Client | Validation done; updateQuote |
READY | REVIEW | Client | Kill switch (1.3) |
READY | REFUNDED | Engine only | processRefund accepted, then fulfillment succeeded in the PSS |
READY | ERROR | Engine only | processRefund accepted, then fulfillment failed |
REFUNDED / ERROR | (nothing) | N/A | Terminal |
1.2 Setting it
The quoteUpdateRequest input accepts refundCandidateStatus as a { from, to } envelope, alongside fields such as waiver, refundRoute and taxes. Payload conventions: API usage & integration.
mutation UpdateQuote($request: quoteUpdateRequest!, $requote: Boolean) {
updateQuote(request: $request, requote: $requote)
}{
"request": {
"ticketNum": "1282132019309",
"refundCandidateStatus": { "from": "REVIEW", "to": "READY" }
}
}1.3 READY → REVIEW is the kill switch
READY → REVIEW is the kill switchFulfillment re-verifies that a candidate is still READY immediately before executing it. Setting an approved candidate back to REVIEW therefore stops the refund from being executed; that is the supported way to hold a refund you have already approved.
2. Quote-stage event values (refund_request.status_updated)
refund_request.status_updated)Delivered as data.status on the refund_request.status_updated webhook. Despite the event name, its display name is "Quote updated"; it reports the progress of a quote request, not the status of an entity, which is why these values are longer strings and not the five-value enum.
| Status | Meaning | Stage outcome |
|---|---|---|
NEW | A quote was requested and is processing | In progress |
TICKETS_FROM_PNR_INSERTED | Quote created by PNR; each ticket from the PNR was inserted for its own quote | In progress; expect a further event per ticket |
REFUND_CANDIDATE_READY | Quote created successfully (ticket already existed in Deal Engine) | Success; a refund candidate now exists |
INSERTED_AND_CREATED_REFUND_CANDIDATE_READY | Quote created successfully after first inserting the ticket | Success; a refund candidate now exists |
ERROR_PROCESSING_REQUEST | Error creating the quote; no refund candidate was created | Failure |
Status values are delivered uppercase, exactly as shown.
- Use
REFUND_CANDIDATE_READY/INSERTED_AND_CREATED_REFUND_CANDIDATE_READYas the trigger to read the quote fromdocument(ticketNum); do not wait a fixed interval. See Request parameters explained. - On
ERROR_PROCESSING_REQUESTa fresh quote request can be safely submitted; Deal Engine has already exhausted its internal retries on transient failures. See Error handling. ERROR_PROCESSING_REQUESTis not the candidate statusERROR. Matching on"ERROR"here never fires.
3. refund_candidate.status_updated: the enum, delivered by webhook
refund_candidate.status_updated: the enum, delivered by webhookThe payload's status carries the section 1 enum verbatim, uppercase.
| Status | Documented firing condition |
|---|---|
NEW | Not documented |
REVIEW | On status transition |
READY | On status transition |
REFUNDED | Automatically when fulfillment finishes; always notified |
ERROR | Automatically when fulfillment finishes; always notified |
Write your handler so an unexpected status is logged and ignored rather than treated as an error, and do not assume the event only carries terminal outcomes; non-terminalREADYandREVIEWdeliveries do occur.
Delivery notes that affect state machines:
- Events triggered by Deal Engine internal operators are not delivered to client endpoints; a status can therefore change without a webhook arriving. Polling is the backstop.
- Delivery failures are logged in
webhookEventsand do not affect the refund itself.
Payload shape, signing and verification: Webhooks guide and Webhook API Reference.
4. Optional explicit REJECTED quotation status
REJECTED quotation statusREJECTED is not part of the enum and not present by default. Rejection behaviour is configured per customer:
| Configuration | How a rejection surfaces | What to branch on |
|---|---|---|
| Default (no explicit rejection configuration) | Written to the refund's notes; the quote returns 0 amount to be refunded | notes text plus a zero refund amount |
| Explicit rejection status (enabled on request) | A dedicated REJECTED status is added to your quotation statuses | The quote status directly |
- Ask Deal Engine to enable the explicit
REJECTEDquotation status if you need a machine-checkable rejection signal. Until then, inspectingnotes+ amount is the only supported detection. - A rejection is never transient. Never auto-retry a rejection; retrying returns the same result. Route to manual review with the note text. See Performance & uptime.
5. Webhook delivery status
Distinct from every refund status above: this describes whether Deal Engine successfully delivered an event to your endpoint. It is not a refund state.
| Field | Type | Observed value |
|---|---|---|
WebhookEvent.status | String; "Delivery status" | delivered (lowercase, in the reference example and the filters.status example) |
WebhookEvent.responseCode | Int | HTTP response code from your endpoint (200 in the example) |
WebhookEvent.error | String | Error message when delivery failed |
Note the casing contrast: refund statuses are uppercase everywhere, including inside webhook payloads, while this delivery status appears lowercase. Compare case-sensitively and do not normalize one to the other.
6. Coupon, segment and tax statuses
Per-coupon refundability status is a separate, airline-side vocabulary. Deal Engine ships default mappings per PSS (Sabre, Amadeus) and customizes them per customer; including which coupon states count as open versus flown. Your mapping is aligned during the PoC and recorded in your FSD.
The reference table is authoritative on Refund lifecycle & statuses §3 and is not duplicated here: one table, one owner.
When parsing, note that these statuses appear both as short codes (O, OK, C, L, F, …) and as whole words in published samples ("couponStatuses": ["OPEN"], "segmentStatus": "OPEN", refundCandidateTaxes[].status: "OPEN"). Confirm the form your account receives against your FSD and one live response, and parse defensively for both.
7. Value sets referenced as literals but never enumerated
These fields appear in published request and response examples as bare string literals. The literals below are the only ones traceable to this documentation; they are examples, not enums. Do not treat any column as complete.
| Field | Where you receive it | Literal(s) observed in the docs | Enumerated? |
|---|---|---|---|
decision | createQuote (Sync: true) top-level result; refundability.reason | VOLUNTARY, INVOLUNTARY, IRREGULAR OPERATIONS, VOLUNTARY MILES, REJECTED | Explicitly not a fixed enum; see below |
product | Quote.product | ARP (Automated Refund Platform) | No |
refundType | refundCandidate.refundType | FULL FARE REFUND | No |
refundRoute | Quote.refundRoute; refundCandidate.overrideRefundRoute; settable via updateQuote | GDS | No |
fopType | Quote.fopInfo[].fopType | None. Prose says only "Payment type (e.g. card, cash)"; those are descriptions, not confirmed wire values | No |
refundMode | processRefund input fulfillmentRequest.refundMode | None. Only ever shown as null | No |
7.1 decision is configurable: how to get your list
decision is configurable: how to get your listdecision is not a fixed enum: it is configured per customer and per flow. That is a legitimate design, not a documentation gap, but it means a hardcoded list will break. To obtain your active list:
- At onboarding: Deal Engine provides your active decision list. This is the authoritative source.
- Your FSD: the rule set agreed with Deal Engine defines every decision value and every rejection rule that can produce one.
- Sandbox verification: exercise your test tickets and assert the decisions returned against your expected rule outcomes before go-live. See Testing & environments.
Your configuration may add variants (for example VOLUNTARY_BEFORE_DEPARTURE) or informational reasons (for example No open coupons found). Handle an unrecognised decision by routing to manual review; never by treating it as refundable.
Action per standard decision: the decision-to-action matrix on Refund lifecycle & statuses.
8. Error catalog
The API is GraphQL: errors arrive in the errors array, and a partial success can return both data and errors. Always inspect errors even when data is non-null.
{
"errors": [
{ "message": "…", "path": ["login"], "extensions": { "code": "…" } }
],
"data": null
}8.1 Failure classes
| Class | HTTP | extensions.code | Symptom | Your action |
|---|---|---|---|---|
| Authentication; bad credentials | 200 (GraphQL error) | INTERNAL_SERVER_ERROR; observed live on sandbox, 2026-07-29 | "Incorrect email or password"; data.login is null | Fix credentials. Do not retry unchanged. Do not branch on extensions.code; see below |
| Authentication; expired access token | Not documented | Not documented | Calls fail with an auth error | Call refreshToken. Access token lives 15 min, refresh token 12 h |
| Authentication; bad refresh token | 200 (GraphQL error) | INTERNAL_SERVER_ERROR | "invalid signature"; data.refreshToken is null | Re-authenticate with login |
| Authorization; permission denied on a permitted operation | 200 (GraphQL error) | FORBIDDEN | Message names the action and resource | Request the permission for your role. Not retryable |
| Authorization; operation absent from your role's schema | 200 (GraphQL error) | Validation error, not an authorization error; typically GRAPHQL_VALIDATION_FAILED | "Cannot query field …", as if the operation did not exist | This is the usual shape of a permission denial. Do not read it as a typo or a version mismatch; confirm the operation is available to your role. See Backend authentication |
| Validation; missing/invalid input | 200 (GraphQL error) | BAD_USER_INPUT (schema-level, e.g. a required variable not provided) and BAD_REQUEST (webhook API, e.g. Invalid URL format) | Descriptive message naming the field | Fix the request. Never retry unchanged |
| Not found | 200 | Not documented | Returns null or an error object with details; both shapes are documented as possible | Handle both. Do not treat null as an error by default |
| Duplicate record | Not documented | Not documented | Creating a value that must be unique when it already exists | Treat as already-created; do not retry blindly |
| Rate limited | 429 | Not applicable (HTTP-level) | Rare; throughput is calibrated per client, with no global public limit | Exponential backoff and retry |
| Server / transient | 5xx | Not documented | N/A | Retry with exponential backoff |
| Business rejection | 200, no error | Not applicable | A REJECTED decision, or notes + a zero refund amount | Never retry. Route to manual review |
Do not branch onextensions.codefor login failures. A wrong email or password returnsINTERNAL_SERVER_ERRORtoday; that is what the wire returns, verified against sandbox. The intended code for this case isUNAUTHENTICATED, so the value may change. Branch on the failure class you inferred frompathplusdatabeingnull, and treat the message as human-readable only.
8.2 processRefund per-ticket results
processRefund per-ticket resultsprocessRefund does not fail the whole call when one ticket is bad. It returns a per-ticket summary, and success: false at the top level can still accompany tickets that were enqueued successfully.
Top-level message | Top-level success | Meaning |
|---|---|---|
All documents processed successfully | true | Every ticket enqueued |
Some documents processed; see results | false | Partial success; read results[] per ticket |
No valid requests to process | false | Nothing enqueued |
Per-ticket message | Per-ticket success | Meaning |
|---|---|---|
Queued for processing | true | Accepted into the queue; not a refund outcome |
Invalid ticket num | false | Rejected at validation; nothing was enqueued for this ticket |
Branch on the booleans, never on the strings. Deal Engine describes the per-ticket result values as enum values, but that enum is not published and cannot be introspected without authorization, so no list of them appears here. Match on top-level success, then per ticket on results[].success keyed by ticketNum; log message for humans only. See API usage & integration.
8.3 Retry policy
- Retry: network errors, HTTP 5xx, 429; exponential backoff.
- Do not retry: validation errors, authentication failures with fixed credentials, and any business rejection. A
REJECTEDdecision does not change on retry. - Deal Engine already retries transient PSS failures internally (up to 2 retries on network errors) before writing
ERROR. Business refusals are never retried internally. - After
ERROR_PROCESSING_REQUESTat the quote stage, submitting a new quote request is safe.
Details: Error handling and Performance & uptime.
9. How to write a polling state machine safely
Webhooks are the recommended primary signal; polling is the backstop for the gaps (internal-operator changes are not delivered to client endpoints, and delivery can fail).
Rules
- Key everything on
ticketNum. Every status field, webhook payload and result row is keyed on the 13-digit ticket number. - Terminal set is a closed allow-list of two:
REFUNDEDandERROR. Stop only on those.NEW,REVIEW,READYand any value you do not recognise keep the ticket in-flight. - Keep a
default:branch. Log and continue rather than erroring: which statuses fire a webhook is only partly documented (section 3), and decisions are per-tenant (section 7.1). - Do not confuse the two vocabularies.
ERROR_PROCESSING_REQUESTon the quote-stage event is a failed quote attempt, not the candidate statusERROR. - Poll every 30–60 seconds. Execution can take ~2 minutes; faster polling adds nothing. Add a wall-clock timeout and escalate to support rather than polling forever.
- Do not read the outcome from
processRefund. Acceptance ≠ outcome, with or withoutSync: true. - Reconcile on the executed amounts, never on the quote estimate; the fare, taxes, penalty and total to refund recorded when the refund executes, all gross. See Refund amounts & reconciliation.
- Separate "rejected" from "failed". A rejection is a business outcome that must not be retried; an
ERRORmay be worth investigating with support. Detect rejection via the explicitREJECTEDquotation status if enabled, otherwise vianotes+ a zero refund amount. - Make webhook handlers idempotent. Dedupe on
eventId; the same status may arrive more than once, and polling may observe a transition the webhook also reports. - In sandbox, target
READY, notREFUNDED. Fulfillment does not run there, soREFUNDEDis unreachable by design.
Recommended shape
on createQuote accepted:
mark ticket QUOTING
on refund_request.status_updated (or poll):
*_REFUND_CANDIDATE_READY -> read document(ticketNum).quote, mark QUOTED
ERROR_PROCESSING_REQUEST -> mark QUOTE_FAILED (a fresh createQuote is safe)
anything else -> log, stay QUOTING
when QUOTED:
rejected (explicit REJECTED status, or notes + zero amount)
-> mark REJECTED, route to manual review, STOP
status REVIEW -> route to your validation step; on approval
updateQuote REVIEW -> READY
status READY -> call processRefund, mark SUBMITTED
when SUBMITTED (webhook, plus poll every 30-60s):
REFUNDED -> mark DONE, reconcile on executed amounts, STOP
ERROR -> mark FAILED, capture error history, STOP
anything else (READY, REVIEW, unknown)
-> stay SUBMITTED
elapsed > your timeout -> escalate to support with ticketNum, keep pollingAcceptance is not an outcome. A successful processRefund response means the ticket passed basic validation and entered the queue. Sync: true on processRefund only delays the response; the body remains the enqueue acknowledgment. Treat REFUNDED, never the acceptance response, as confirmation. See Request parameters explained.
10. Remaining open items
Each item below is unresolved in the published documentation; do not resolve it by assumption.
| # | Item | Impact |
|---|---|---|
| 1 | Whether a transition into NEW fires a refund_candidate.status_updated delivery is not documented | An exhaustive switch over webhook statuses may never see it, or may be surprised by it |
| 2 | Event names are inverted relative to their display names: refund_request.status_updated is displayed as "Quote updated", and refund_candidate.status_updated as "Request updated" | Integrators subscribe to the wrong event |
| 3 | A failed login returns extensions.code: "INTERNAL_SERVER_ERROR" today; the intended code is UNAUTHENTICATED | Branching on the login error code is unsafe; it may change |
| 4 | Validation failures use BAD_USER_INPUT in the GraphQL API and BAD_REQUEST in the webhook API | Same, for validation |
| 5 | Coupon statuses appear both as short codes and as whole words (OPEN) in published samples; the form your account receives is defined by your PSS mapping and FSD, not by this documentation | Parsers must be verified against your FSD and live output |
| 6 | Refund statuses are uppercase while WebhookEvent.status (delivery) is lowercase (delivered) | Case-sensitive comparisons break if you assume one convention |
| 7 | processRefund per-ticket result values are described as enum values, but the enum is not published | Branch on the boolean success, not on result strings |
Related pages
- Refund lifecycle & statuses; the lifecycle narrative, coupon status table, and decision matrix
- Refund amounts & reconciliation; which amount is authoritative
- Webhooks guide and Webhook API Reference; payloads, signing, delivery history
- API usage & integration; full request/response examples
- Request parameters explained; the
Syncheader and commonly misread fields - Error handling and Authentication & authorization
- GraphQL conventions; filters, pagination, and reading the schema
- Testing & environments; what can and cannot reach a terminal status in sandbox
- What is a quote? · What is a document? · What is a request?
Updated about 18 hours ago