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:

  1. The quote-stage event values delivered by the refund_request.status_updated webhook (section 2); event progress, not an entity status.
  2. The webhook delivery status (section 5); whether Deal Engine reached your endpoint, lowercase, nothing to do with refunds.
  3. Coupon / segment / tax statuses (section 6): an airline-side vocabulary with its own per-PSS mapping.

How to read this page

  1. Branch on the five-value enum for any refund state. Sections 1 and 3.
  2. Treat unknown values as non-terminal and keep polling. Stop only on REFUNDED and ERROR.
  3. 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

VocabularyExact field / query that returns itEnumerated 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 valuesrefund_request.status_updated webhook → data.statusYes; 5 values, section 2
Webhook delivery statuswebhookEvents { items { status } }, and the filters.status argumentPartly; one observed value, section 5
Coupon / segment / tax statusrefundCandidate.extraParams.segments[].couponStatuses[], .segmentStatus, refundCandidateTaxes[].statusMapping lives on Refund lifecycle & statuses

1. The refund status enum

StatusMeaningTerminal?Who can set it
NEWReceived; the ticket is in Deal Engine and quotation has not produced a validated outcome yetNoEngine
REVIEWThe quotation output needs the customer's validation before it can be processedNoEngine or client
READYDeal Engine is confident the refund meets the functional rules agreed with you in your FSDNoEngine or client
REFUNDEDExecuted successfully in the airline systemYesEngine only
ERRORExecution failed after acceptanceYesEngine only
  • Fulfillment only executes candidates in READY. NEW and REVIEW are never refunded.
  • REFUNDED and ERROR are 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.
  • ERROR carries failure detail in its error history. Contact support with the ticket number when it is not actionable on your side.
  • REFUNDED is 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

FromToActorTrigger
(none)NEWEngineCandidate created from a quote
NEWREVIEWEngine or clientThe quotation needs your validation
NEWREADYEngine or clientQuotation satisfies your FSD rules
REVIEWREADYClientValidation done; updateQuote
READYREVIEWClientKill switch (1.3)
READYREFUNDEDEngine onlyprocessRefund accepted, then fulfillment succeeded in the PSS
READYERROREngine onlyprocessRefund accepted, then fulfillment failed
REFUNDED / ERROR(nothing)N/ATerminal

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 READYREVIEW is the kill switch

Fulfillment 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)

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.

StatusMeaningStage outcome
NEWA quote was requested and is processingIn progress
TICKETS_FROM_PNR_INSERTEDQuote created by PNR; each ticket from the PNR was inserted for its own quoteIn progress; expect a further event per ticket
REFUND_CANDIDATE_READYQuote created successfully (ticket already existed in Deal Engine)Success; a refund candidate now exists
INSERTED_AND_CREATED_REFUND_CANDIDATE_READYQuote created successfully after first inserting the ticketSuccess; a refund candidate now exists
ERROR_PROCESSING_REQUESTError creating the quote; no refund candidate was createdFailure

Status values are delivered uppercase, exactly as shown.

  • Use REFUND_CANDIDATE_READY / INSERTED_AND_CREATED_REFUND_CANDIDATE_READY as the trigger to read the quote from document(ticketNum); do not wait a fixed interval. See Request parameters explained.
  • On ERROR_PROCESSING_REQUEST a fresh quote request can be safely submitted; Deal Engine has already exhausted its internal retries on transient failures. See Error handling.
  • ERROR_PROCESSING_REQUEST is not the candidate status ERROR. Matching on "ERROR" here never fires.

3. refund_candidate.status_updated: the enum, delivered by webhook

The payload's status carries the section 1 enum verbatim, uppercase.

StatusDocumented firing condition
NEWNot documented
REVIEWOn status transition
READYOn status transition
REFUNDEDAutomatically when fulfillment finishes; always notified
ERRORAutomatically 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-terminal READY and REVIEW deliveries 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 webhookEvents and do not affect the refund itself.

Payload shape, signing and verification: Webhooks guide and Webhook API Reference.

4. Optional explicit REJECTED quotation status

REJECTED is not part of the enum and not present by default. Rejection behaviour is configured per customer:

ConfigurationHow a rejection surfacesWhat to branch on
Default (no explicit rejection configuration)Written to the refund's notes; the quote returns 0 amount to be refundednotes text plus a zero refund amount
Explicit rejection status (enabled on request)A dedicated REJECTED status is added to your quotation statusesThe quote status directly
  • Ask Deal Engine to enable the explicit REJECTED quotation status if you need a machine-checkable rejection signal. Until then, inspecting notes + 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.

FieldTypeObserved value
WebhookEvent.statusString; "Delivery status"delivered (lowercase, in the reference example and the filters.status example)
WebhookEvent.responseCodeIntHTTP response code from your endpoint (200 in the example)
WebhookEvent.errorStringError 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.

FieldWhere you receive itLiteral(s) observed in the docsEnumerated?
decisioncreateQuote (Sync: true) top-level result; refundability.reasonVOLUNTARY, INVOLUNTARY, IRREGULAR OPERATIONS, VOLUNTARY MILES, REJECTEDExplicitly not a fixed enum; see below
productQuote.productARP (Automated Refund Platform)No
refundTyperefundCandidate.refundTypeFULL FARE REFUNDNo
refundRouteQuote.refundRoute; refundCandidate.overrideRefundRoute; settable via updateQuoteGDSNo
fopTypeQuote.fopInfo[].fopTypeNone. Prose says only "Payment type (e.g. card, cash)"; those are descriptions, not confirmed wire valuesNo
refundModeprocessRefund input fulfillmentRequest.refundModeNone. Only ever shown as nullNo

7.1 decision is configurable: how to get your list

decision 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:

  1. At onboarding: Deal Engine provides your active decision list. This is the authoritative source.
  2. Your FSD: the rule set agreed with Deal Engine defines every decision value and every rejection rule that can produce one.
  3. 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

ClassHTTPextensions.codeSymptomYour action
Authentication; bad credentials200 (GraphQL error)INTERNAL_SERVER_ERROR; observed live on sandbox, 2026-07-29"Incorrect email or password"; data.login is nullFix credentials. Do not retry unchanged. Do not branch on extensions.code; see below
Authentication; expired access tokenNot documentedNot documentedCalls fail with an auth errorCall refreshToken. Access token lives 15 min, refresh token 12 h
Authentication; bad refresh token200 (GraphQL error)INTERNAL_SERVER_ERROR"invalid signature"; data.refreshToken is nullRe-authenticate with login
Authorization; permission denied on a permitted operation200 (GraphQL error)FORBIDDENMessage names the action and resourceRequest the permission for your role. Not retryable
Authorization; operation absent from your role's schema200 (GraphQL error)Validation error, not an authorization error; typically GRAPHQL_VALIDATION_FAILED"Cannot query field …", as if the operation did not existThis 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 input200 (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 fieldFix the request. Never retry unchanged
Not found200Not documentedReturns null or an error object with details; both shapes are documented as possibleHandle both. Do not treat null as an error by default
Duplicate recordNot documentedNot documentedCreating a value that must be unique when it already existsTreat as already-created; do not retry blindly
Rate limited429Not applicable (HTTP-level)Rare; throughput is calibrated per client, with no global public limitExponential backoff and retry
Server / transient5xxNot documentedN/ARetry with exponential backoff
Business rejection200, no errorNot applicableA REJECTED decision, or notes + a zero refund amountNever retry. Route to manual review
🚧

Do not branch on extensions.code for login failures. A wrong email or password returns INTERNAL_SERVER_ERROR today; that is what the wire returns, verified against sandbox. The intended code for this case is UNAUTHENTICATED, so the value may change. Branch on the failure class you inferred from path plus data being null, and treat the message as human-readable only.

8.2 processRefund per-ticket results

processRefund 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 messageTop-level successMeaning
All documents processed successfullytrueEvery ticket enqueued
Some documents processed; see resultsfalsePartial success; read results[] per ticket
No valid requests to processfalseNothing enqueued
Per-ticket messagePer-ticket successMeaning
Queued for processingtrueAccepted into the queue; not a refund outcome
Invalid ticket numfalseRejected 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 REJECTED decision 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_REQUEST at 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

  1. Key everything on ticketNum. Every status field, webhook payload and result row is keyed on the 13-digit ticket number.
  2. Terminal set is a closed allow-list of two: REFUNDED and ERROR. Stop only on those. NEW, REVIEW, READY and any value you do not recognise keep the ticket in-flight.
  3. 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).
  4. Do not confuse the two vocabularies. ERROR_PROCESSING_REQUEST on the quote-stage event is a failed quote attempt, not the candidate status ERROR.
  5. 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.
  6. Do not read the outcome from processRefund. Acceptance ≠ outcome, with or without Sync: true.
  7. 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.
  8. Separate "rejected" from "failed". A rejection is a business outcome that must not be retried; an ERROR may be worth investigating with support. Detect rejection via the explicit REJECTED quotation status if enabled, otherwise via notes + a zero refund amount.
  9. 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.
  10. In sandbox, target READY, not REFUNDED. Fulfillment does not run there, so REFUNDED is 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 polling

Acceptance 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.

#ItemImpact
1Whether a transition into NEW fires a refund_candidate.status_updated delivery is not documentedAn exhaustive switch over webhook statuses may never see it, or may be surprised by it
2Event 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
3A failed login returns extensions.code: "INTERNAL_SERVER_ERROR" today; the intended code is UNAUTHENTICATEDBranching on the login error code is unsafe; it may change
4Validation failures use BAD_USER_INPUT in the GraphQL API and BAD_REQUEST in the webhook APISame, for validation
5Coupon 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 documentationParsers must be verified against your FSD and live output
6Refund statuses are uppercase while WebhookEvent.status (delivery) is lowercase (delivered)Case-sensitive comparisons break if you assume one convention
7processRefund per-ticket result values are described as enum values, but the enum is not publishedBranch on the boolean success, not on result strings

Related pages


Did this page help you?
All rights reserved © 2025 deal-engine.com.