Skip to main content

Leon API Integration Guide: RFQ & Quote Management via Webhook

Version: 20 August 2026 API: Leon GraphQL API (Beta) Prepared by: Leon Software Support


What this guide does

Lets your system push charter Requests For Quote (RFQ) into Leon, read back the operator's priced quotes, and receive the operator's accept / reject decision — including the final price and the Flight Quotation PDF — as an HTTP callback to your own endpoint.


Before you start

Make sure you have:

  • A Leon account with API access enabled — contact Leon Support if you don't have it yet
  • An API Key created in Leon (this produces a RefreshToken) — see Step 1a
  • The API Key granted these resources: GRAPHQL_SALES_QUOTE_EDIT, GRAPHQL_SALES_QUOTE_REQUEST_SEE, GRAPHQL_ACFT, GRAPHQL_AIRPORT, GRAPHQL_CONTACT
  • The Sales module active on the operator's Leon account
  • Two publicly reachable HTTPS endpoints for the accept and reject callbacks — see Step 2, they must already be live before you create your first RFQ
  • The operator's oprId (their Leon subdomain — ask any user: "What address do you use to sign in to Leon?")

⚠️ This guide uses the API Key authentication method, which is only valid for a single operator's internal use. If you are building software that will be used by more than one Leon operator (a broker platform, marketplace, or any distributed SaaS product), you must use OAuth 2.0 Code Grant instead. API Key is not an option in that case, and integrations using API Key are not visible in the Leon Addons panel. The rest of this guide is unchanged apart from Step 1 — contact Leon Support for the OAuth registration form.

API endpoints:

EnvironmentURL
Sandbox (develop and test here)https://{oprId}.sandbox.leon.aero/api/graphql/
Productionhttps://{oprId}.leon.aero/api/graphql/

All steps at a glance

  1. Authenticate → get access_token
  2. Stand up your webhook endpoints → get two live HTTPS URLs
  3. List quotable aircraft → get aircraft registration values
  4. Create the RFQ → get quoteRequestNid and your X- prefixed external ID
  5. Read the RFQ and its quotes → get quoteRealizationNid + prices
  6. Poll for new and changed quotes → get a fresh timestamp for the next run
  7. (Optional) Download the Flight Quotation PDF → get base64 file content
  8. Accept a quote → Leon POSTs the accept payload to your endpoint
  9. Reject or cancel a quote → Leon POSTs the reject payload to your endpoint
  10. Handle the incoming webhook payload → your system is updated

Step 1 — Authenticate

⚠️ This method is only for integrations built for a single operator's internal use. If you plan to distribute this integration to other Leon operators, use OAuth instead.

Step 1a — Create an API Key in Leon

The operator creates an API Key in Leon (Settings → API Keys). Full instructions: https://wiki.leonsoftware.com/leon/api-keys

Grant the key the resources listed in Before you start. This produces a RefreshToken.

Step 1b — Exchange the Refresh Token for an Access Token

curl -X POST -d 'refresh_token={RefreshToken}' https://{oprId}.leon.aero/access_token/refresh/

Fill in:

PlaceholderWhere to get it
{oprId}The operator's Leon subdomain
{RefreshToken}From the API Key created in Step 1a

Response: the access token, returned as plain text.

Save this → the returned string — this is your access_token. Valid for 30 minutes.

⚠️ Token rules: reuse the access token for its full 30-minute life — do not mint a new token per request. Refresh proactively when fewer than 5 minutes remain rather than waiting for a 401.

Every request from here on uses:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Step 2 — Stand up your webhook endpoints

Leon will call these URLs later with the operator's decision. They must exist before you create your first RFQ.

Build two HTTPS endpoints, for example:

  • https://your-system.example.com/leon/quote-accepted
  • https://your-system.example.com/leon/quote-rejected

Each endpoint must satisfy all of the following:

RequirementDetail
Answers GET with a status code in 200–301Leon performs a live GET health check on the URL at the moment you create the RFQ (5s connect + 5s total timeout). If the check fails, the whole createRequestWithWebhook mutation is rejected with Invalid url response - should return code between 200 and 301.
Accepts POST with a JSON bodyThis is how the actual payload arrives
Parses the body as JSON regardless of the Content-Type headerLeon sends the JSON string without setting Content-Type, so cURL defaults to application/x-www-form-urlencoded. Do not gate your parser on the header.
Treats the URL itself as the secretLeon sends no signature, HMAC, JWT, or auth header. Use a long unguessable path or query token, and restrict by source IP if you can.
Responds quickly with a status in 200–301Any other status makes the operator's Accept/Reject action fail with a MARKETPLACE_INTEGRATION_ERROR. There is no automatic retry — the operator has to repeat the action manually.

A valid TLS certificate is required; self-signed certificates will fail the health check.

Save this → your two URLs — you'll pass them as acceptWebhook and rejectWebhook in Step 4.


Step 3 — List quotable aircraft

The RFQ names the aircraft you want quoted by tail registration, so fetch the operator's active fleet first. Cache this — it changes rarely.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

query ListQuotableAircraft {
aircraftList(onlyActive: true) {
aircraftNid
registration
acftTypeName
paxCapacity
isActive
}
}

📖 Full type reference → · Query aircraftList

Fill in: no variables.

Response (relevant fields only):

{
"data": {
"aircraftList": [
{ "aircraftNid": 123, "registration": "SP-ABC", "acftTypeName": "Citation XLS", "paxCapacity": 8, "isActive": true }
]
}
}

Save this → the registration string of each aircraft you want quoted — you'll pass it in aircraftList in Step 4.


Step 4 — Create the RFQ

Creates a quote request in the operator's Sales panel and registers your two callback URLs against it.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

mutation CreateRfqWithWebhook($quoteRequest: QuoteRequestWithWebhook!) {
sales {
createRequestWithWebhook(quoteRequest: $quoteRequest) {
nid
id
createdDate
status { name }
acceptWebhook
rejectWebhook
legs {
nid
adep { icao }
ades { icao }
std
sta
paxNo
isTBA
}
aircraftList { nid tail type maxPax }
buyer { name email }
}
}
}

📖 Mutation reference → · Input type QuoteRequestWithWebhook · Return type QuoteRequest

Fill in — top level of $quoteRequest:

FieldRequiredWhere to get itExample
marketplaceYesEnum naming the source of the request. Use Internal for your own system unless Leon told you otherwise. See MarketplaceEnum"Internal"
aircraftListYesArray of registration strings from Step 3["SP-ABC"]
itineraryYesArray of legs — see the leg table belowsee JSON
requestedByYesThe client asking for the quote — see the requester table belowsee JSON
representativeNoContact person acting for the client — displayName is required if you send this objectsee JSON
assigneeNoLeon login NID of the sales person to assign4571
messageNoFree-text note shown to the operator"Pet on board"
acceptWebhookNo, but required for this integrationYour accept URL from Step 2"https://…/quote-accepted"
rejectWebhookNo, but required for this integrationYour reject URL from Step 2"https://…/quote-rejected"
quoteRequestIdNo, but strongly recommendedYour own identifier for this RFQ"CRM-2026-00412"

Fill in — each entry in itinerary (RfqItineraryInput):

FieldRequiredNotesExample
dateYesYYYY-MM-DD"2026-09-01"
timeNoHH:MM. Omit together with isTBA: true"10:00"
departureOrArrivalYesWhether date/time describe departure or arrival. See DepartureOrArrivalEnum"DEPARTURE"
adepYesDeparture airport code (ICAO/IATA)"EPWA"
adesYesArrival airport code"EGLL"
paxNumberYesPassenger count on this leg4
isTBANotrue when times are not fixed yetfalse
cargoNoCargo weight — see CargoInput

Fill in — requestedBy (QuoteRequestWithWebhookRequester):

FieldRequiredNotes
displayNameYesCompany or person name; Leon matches or creates a client record from this
idNoYour own client identifier
emails, phone, mobilePhoneNoContact details
postCode, city, street, countryCodeNoAddress; countryCode is ISO 3166-1 alpha-2, e.g. "PL"

Variables JSON:

{
"quoteRequest": {
"marketplace": "Internal",
"quoteRequestId": "CRM-2026-00412",
"aircraftList": ["SP-ABC"],
"acceptWebhook": "https://your-system.example.com/leon/quote-accepted",
"rejectWebhook": "https://your-system.example.com/leon/quote-rejected",
"message": "Client requests catering and ground transport quote separately.",
"requestedBy": {
"id": "CLIENT-8891",
"displayName": "Northwind Charter Ltd",
"emails": ["ops@northwind.example.com"],
"phone": "+48221234567",
"city": "Warsaw",
"countryCode": "PL"
},
"representative": {
"id": "CONTACT-551",
"displayName": "Anna Kowalska",
"firstName": "Anna",
"lastName": "Kowalska",
"emails": ["anna.kowalska@northwind.example.com"]
},
"itinerary": [
{
"date": "2026-09-01",
"time": "10:00",
"departureOrArrival": "DEPARTURE",
"adep": "EPWA",
"ades": "EGLL",
"paxNumber": 4,
"isTBA": false
},
{
"date": "2026-09-04",
"time": "16:30",
"departureOrArrival": "DEPARTURE",
"adep": "EGLL",
"ades": "EPWA",
"paxNumber": 4,
"isTBA": false
}
]
}
}

Response (relevant fields only):

{
"data": {
"sales": {
"createRequestWithWebhook": {
"nid": "77412",
"id": "X-CRM-2026-00412",
"status": { "name": "New Request" },
"acceptWebhook": "https://your-system.example.com/leon/quote-accepted",
"rejectWebhook": "https://your-system.example.com/leon/quote-rejected",
"legs": [
{ "nid": "20551", "adep": { "icao": "EPWA" }, "ades": { "icao": "EGLL" }, "std": 1788254400, "sta": 1788262200, "paxNo": 4, "isTBA": false }
],
"aircraftList": [{ "nid": 123, "tail": "SP-ABC", "type": "Citation XLS", "maxPax": 8 }],
"buyer": { "name": "Northwind Charter Ltd", "email": "ops@northwind.example.com" }
}
}
}
}

⚠️ Leon prefixes your quoteRequestId with X-. You sent CRM-2026-00412; the stored identifier returned in id is X-CRM-2026-00412. Always use the full prefixed value when looking the request up later (Step 5).

Save this →

  • nid — the quoteRequestNid, used in Steps 5, 7 and 9
  • id — the X- prefixed external ID, used for lookups by your own reference
  • std / sta are UTC Unix timestamps in seconds

Step 5 — Read the RFQ and its quotes

Returns the operator's quotes (Leon calls them realizations) with prices, aircraft, and per-leg schedule. Call this after the operator has had time to quote, or after Step 6 tells you something changed.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

query GetQuoteRequestWithQuotes($quoteRequestNid: QuoteRequestNid!) {
sales {
getQuoteRequest(quoteRequestNid: $quoteRequestNid) {
nid
id
status { name }
lastExternalUpdate
legs { nid adep { icao } ades { icao } std sta paxNo }
realizations {
nid
acft { aircraftNid registration acftTypeName }
price
currency
quoted
quotedDateTime
booked
isUsed
status
isOutdated
pricing {
currency
totalPrice
quoteRoundedPrice
VATRate
VATAmount
}
legs {
nid
adep { icao }
ades { icao }
stdUTC
staUTC
blockTime
isFuelStop
}
notes { customerNote cancellationPolicy legalNote }
}
}
}
}

📖 Query getQuoteRequest · Types QuoteRequest, QuoteRealization, Pricing

Fill in:

VariableWhere to get itExample
$quoteRequestNidnid from the Step 4 response"77412"

Variables JSON:

{ "quoteRequestNid": "77412" }

Response (relevant fields only):

{
"data": {
"sales": {
"getQuoteRequest": {
"nid": "77412",
"id": "X-CRM-2026-00412",
"status": { "name": "Quoted" },
"realizations": [
{
"nid": "31998",
"acft": { "aircraftNid": 123, "registration": "SP-ABC", "acftTypeName": "Citation XLS" },
"price": 24800.0,
"currency": "EUR",
"quoted": true,
"quotedDateTime": "2026-08-20T09:41:00+00:00",
"booked": false,
"isUsed": false,
"status": "Unanswered",
"isOutdated": false,
"pricing": { "currency": "EUR", "totalPrice": 24800.0, "quoteRoundedPrice": 24800.0, "VATRate": 0, "VATAmount": 0 },
"notes": { "customerNote": "Price includes catering.", "cancellationPolicy": "50% within 72h.", "legalNote": null }
}
]
}
}
}
}

How to read the key fields:

FieldMeaning
status.name on the requestWorkflow stage: New Request, Quoted, Rejected, Option, Contract Sent, Booked, Brief Sent, Done, Invoice Sent, Owner Approval, Canceled, Opportunity, Ready to book. More values may be added — treat unknown values gracefully.
quotedtrue once the operator has priced this quote. Poll until this flips.
status on the realizationWhether you have answered this quote: Unanswered, Accepted, Rejected. More values may be added.
isOutdatedThe itinerary or price changed after the quote was sent — re-read before acting on it
price vs pricing.totalPriceprice is the headline figure shown in Sales; pricing is the detailed breakdown including VAT

Alternative — look up by your own reference instead of nid:

query GetRequestByExternalId($quoteRequestIdList: [QuoteRequestId!]!) {
sales {
getQuoteRequestListByProviderFriendlyId(quoteRequestIdList: $quoteRequestIdList) {
nid
id
status { name }
realizations { nid price currency quoted status }
}
}
}

📖 Query getQuoteRequestListByProviderFriendlyId

{ "quoteRequestIdList": ["X-CRM-2026-00412"] }

⚠️ Pass the X- prefixed value. "CRM-2026-00412" will not match.

Save this → the nid of the realization you want to act on — this is the quoteRealizationNid used in Steps 7, 8 and 9.


Step 6 — Poll for new and changed quotes

Instead of re-reading every open RFQ, ask Leon what changed since your last run. This is the recommended sync strategy — no webhook infrastructure needed for it, and you catch up automatically after downtime.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

query PollModifiedQuotes($dateTime: DateTime!) {
sales {
getModifiedQuotesList(dateTime: $dateTime) {
timestamp
deleted
created {
nid
price
currency
quoted
status
quoteRequest { nid id status { name } }
}
changed {
nid
price
currency
quoted
status
isOutdated
quoteRequest { nid id status { name } }
}
}
}
}

📖 Query getModifiedQuotesList · Return type QuotesChanges

Fill in:

VariableWhere to get itExample
$dateTimeThe timestamp saved from your previous run. On the very first run, use a recent point in time."2026-08-20T08:00:00+00:00"

Variables JSON:

{ "dateTime": "2026-08-20T08:00:00+00:00" }

Response (relevant fields only):

{
"data": {
"sales": {
"getModifiedQuotesList": {
"timestamp": 1787209260,
"deleted": [31877],
"created": [],
"changed": [
{ "nid": "31998", "price": 24800.0, "currency": "EUR", "quoted": true, "status": "Unanswered", "isOutdated": false,
"quoteRequest": { "nid": "77412", "id": "X-CRM-2026-00412", "status": { "name": "Quoted" } } }
]
}
}
}
}

Loop design:

  1. Store lastSyncTimestamp after every successful run
  2. Call this query with dateTime = lastSyncTimestamp
  3. Process created, changed, and deleted (an array of realization NIDs that no longer exist)
  4. Set lastSyncTimestamp = the returned timestampuse Leon's value, not your own clock, to avoid gaps from clock drift

⚠️ The lookback window is capped at 7 days. Poll at least once a day; a longer gap means changes are lost and you must re-read the affected requests with Step 5. Results are ordered by change time ascending. This query returns quotes with a modified price or itinerary — not every field change.

Save this → timestamp for the next run, and each nid you need to re-read in detail.


Step 7 — Download the Flight Quotation PDF (optional)

Generates the operator's Flight Quotation document for one or more quotes. Skip this if you only need the numbers — the accept webhook in Step 10 already delivers the PDF as an attachment.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

query GetFlightQuotationPdf(
$quoteRequestNid: QuoteRequestNid!
$quoteRealizationNidList: [QuoteRealizationNid!]
) {
sales {
getFlightQuotationDocument(
quoteRequestNid: $quoteRequestNid
quoteRealizationNidList: $quoteRealizationNidList
) {
... on DocumentFileDataValue {
value { name fileName content }
}
... on ErrorList {
errorList { message category path }
}
}
}
}

📖 Query getFlightQuotationDocument · Types DocumentFileData, ErrorList

Fill in:

VariableWhere to get itExample
$quoteRequestNidnid from Step 4 or 5"77412"
$quoteRealizationNidListRealization nid values from Step 5. Omit to let Leon use the selected realization, falling back to the first one.["31998"]

Variables JSON:

{ "quoteRequestNid": "77412", "quoteRealizationNidList": ["31998"] }

Response (relevant fields only):

{
"data": {
"sales": {
"getFlightQuotationDocument": {
"value": { "name": "Flight Quotation", "fileName": "quotation-77412.pdf", "content": "JVBERi0xLjQK…" }
}
}
}
}

This is a union type: a successful call returns a value object, a failure returns errorList. Branch on which key is present. The document template is chosen automatically if you don't specify one.

Save this → content — base64-encoded file bytes. Decode and store or forward to the client.


Step 8 — Accept a quote

Marks the quote as accepted in Leon and triggers a POST to your acceptWebhook URL with the final price and documents.

Request:

POST https://{oprId}.leon.aero/api/graphql/
Content-Type: application/json
Authorization: Bearer <access_token from Step 1>

Body:

mutation AcceptQuote($messagesList: [ExternalServiceMessageInput!]!) {
sales {
changeRequestStatus {
setWebhookAccept(messagesList: $messagesList) {
... on NonNullQuoteRequestValue {
value {
nid
id
status { name }
usedRealization { nid price currency booked status }
}
}
... on ErrorList {
errorList { message category path }
}
}
}
}
}

📖 Mutation setWebhookAccept · Input type ExternalServiceMessageInput

Fill in — each entry in messagesList:

FieldRequiredWhere to get itExample
quoteRealizationNidYesRealization nid from Step 5"31998"
messageYesText sent along with the acceptance"Accepted — please issue the contract."
attachmentsYes (may be an empty array)Documents to attach — see ExternalServiceMessageAttachmentInput: name plus one of documentTemplateNid, url, or resourceNid[]

Variables JSON:

{
"messagesList": [
{
"quoteRealizationNid": "31998",
"message": "Accepted — please issue the contract.",
"attachments": []
}
]
}

Response (relevant fields only):

{
"data": {
"sales": {
"changeRequestStatus": {
"setWebhookAccept": {
"value": {
"nid": "77412",
"id": "X-CRM-2026-00412",
"status": { "name": "Booked" },
"usedRealization": { "nid": "31998", "price": 24800.0, "currency": "EUR", "booked": true, "status": "Accepted" }
}
}
}
}
}
}

⚠️ This mutation only works on requests created through createRequestWithWebhook. Calling it on a request that came from Avinode, a marketplace, or manual entry returns a provider-type mismatch error.

⚠️ The webhook call is part of this mutation, not a background job. If your endpoint returns anything outside 200–301, the whole mutation comes back with an errorList entry categorised as MARKETPLACE_INTEGRATION_ERROR and nothing is retried. Retry the mutation yourself once your endpoint is healthy.

Note: when the operator clicks Accept on this RFQ inside Leon's Sales panel, Leon fires exactly the same mutation — so your endpoint receives the same payload whether the trigger was your API call or a human in the UI.

Save this → status.name and usedRealization — confirm they match what your webhook handler recorded in Step 10.


Step 9 — Reject or cancel a quote

Same shape as Step 8, but hits your rejectWebhook URL.

Body:

mutation RejectQuote(
$messagesList: [ExternalServiceMessageInput!]!
$quoteCancellationReason: QuoteCancellationReasonInput
$shouldDeleteTrip: Boolean!
) {
sales {
changeRequestStatus {
setWebhookReject(
messagesList: $messagesList
quoteCancellationReason: $quoteCancellationReason
shouldDeleteTrip: $shouldDeleteTrip
) {
... on NonNullQuoteRequestValue {
value { nid id status { name } }
}
... on ErrorList {
errorList { message category path }
}
}
}
}
}

📖 Mutation setWebhookReject · Input type QuoteCancellationReasonInput

Fill in:

VariableRequiredWhere to get itExample
$messagesListYesSame structure as Step 8see JSON
$quoteCancellationReason.canceledByYes if the object is sentBUYER or SELLER — see QuoteRequestRejectCanceledByEnum"BUYER"
$quoteCancellationReason.cancellationReasonYes if the object is sentFree text"Client changed dates."
$shouldDeleteTripYesWhether to remove a trip already created from this quotefalse

Variables JSON:

{
"messagesList": [
{ "quoteRealizationNid": "31998", "message": "Client withdrew the request.", "attachments": [] }
],
"quoteCancellationReason": { "canceledBy": "BUYER", "cancellationReason": "Client changed dates." },
"shouldDeleteTrip": false
}

Cancel instead of reject: use the identically-shaped setWebhookCancel mutation. Use reject before the quote is confirmed, cancel after. Both send the same payload to your rejectWebhook URL — only Leon's internal status differs (Rejected vs Canceled), so read rejected_data if you need to tell them apart, or key off the mutation you called.

Save this → status.name — confirm the request moved to Rejected or Canceled.


Step 10 — Handle the incoming webhook payload

What your endpoints from Step 2 actually receive.

Transport: POST, JSON body, no Content-Type: application/json header (Leon does not set one, so cURL defaults to application/x-www-form-urlencoded), no authentication header of any kind. Parse the raw body as JSON.

Payload delivered to acceptWebhook

{
"quote": {
"acft": {
"aircraftRegistation": "SP-ABC",
"aircraftNid": 123,
"registrationWithoutSpecialChars": "SPABC"
},
"lift": null,
"opr_id": "northwind",
"opr_nid": 42,
"provider_friendly_id": "X-CRM-2026-00412",
"quote_request_id": "CRM-2026-00412",
"quote_realization_nid": 31998,
"currency_code": "EUR",
"total_price": 24800.00,
"segments": [
{
"start_airport": { "icao": "EPWA", "iata": "WAW" },
"end_airport": { "icao": "EGLL", "iata": "LHR" },
"date_time": { "date": "2026-09-01", "time": "10:00", "departure": true, "local": false },
"block_time_minutes": 120,
"pax_count": 4,
"pax_segment": true,
"show_to_buyer": true
}
],
"attachments": [
{ "mimeType": "application/pdf", "data": "JVBERi0xLjQK…", "name": "quote.pdf", "type": "Flight Quotation" }
]
},
"message": "Accepted — please issue the contract."
}

⚠️ aircraftRegistation is spelled that way in Leon's payload — a typo in the source, not in this guide. Match on the literal key.

Field naming is mixed camelCase and snake_case; do not normalise assumptions across keys.

Payload delivered to rejectWebhook

Same envelope, but without segments, currency_code, total_price, and attachments, and with rejected_data added:

{
"quote": {
"acft": { "aircraftRegistation": "SP-ABC", "aircraftNid": 123, "registrationWithoutSpecialChars": "SPABC" },
"opr_id": "northwind",
"opr_nid": 42,
"provider_friendly_id": "X-CRM-2026-00412",
"quote_request_id": "CRM-2026-00412",
"quote_realization_nid": 31998,
"rejected_data": { "cancelledBy": "seller", "cancellationReason": "Aircraft unavailable." }
},
"message": "Aircraft went AOG."
}

How to match it to your own records:

Payload fieldUse it for
provider_friendly_idYour X- prefixed external ID — the primary key to look up your RFQ
quote_realization_nidThe specific quote, matches nid from Step 5
opr_idWhich operator sent it — essential if you connect more than one

Handler requirements:

  • Respond with 200–301 as fast as possible. A slow or failing response breaks the operator's action in Leon, and there is no retry.
  • Acknowledge first, process asynchronously. Leon sets no send timeout, so a hanging endpoint holds the operator's request open.
  • Make handling idempotent — the operator may repeat a failed action, producing a duplicate delivery for the same quote_realization_nid.
  • Treat the callback as unauthenticated input: validate opr_id against operators you actually integrate with, and never trust total_price without cross-checking via Step 5.

Save this → nothing further — the workflow is complete. Reconcile against Step 5 if you want a second confirmation of final state.


If something goes wrong

SymptomLikely causeFix
401 UnauthorizedAccess token expired (30-minute life) or missingRe-run Step 1b and use the new access_token
Invalid url response - should return code between 200 and 301. on Step 4Your webhook URL didn't answer a GET health check within 5s, or answered with a code outside 200–301Make both URLs answer GET with 200. Check TLS certificate validity, firewall rules, and that the path exists for GET, not just POST
errorList with category MARKETPLACE_INTEGRATION_ERROR on Step 8 or 9Your webhook endpoint returned a code outside 200–301, or was unreachableFix the endpoint, then re-run the mutation. Nothing is retried automatically
Provider type mismatch error on Step 8 or 9The quote request wasn't created via createRequestWithWebhookOnly use the setWebhook* mutations on requests you created through Step 4
Lookup by external ID returns nothingYou passed the unprefixed IDPrefix with X-, e.g. X-CRM-2026-00412
400 Bad Request on Step 4Missing required field or wrong type in $quoteRequestCheck the Fill-in tables — marketplace, aircraftList, itinerary, and requestedBy.displayName are all mandatory, as are date, departureOrArrival, adep, ades, paxNumber on every leg
Empty realizations array in Step 5The operator hasn't created any quotes yetKeep polling with Step 6; the request sits at New Request until the operator quotes it
Step 6 misses changesGap longer than the 7-day lookback window, or you reused your own clock instead of Leon's timestampPoll at least daily and always carry forward the timestamp Leon returns
Webhook body arrives empty in your frameworkYour framework parsed the body as form data because of the missing Content-Type headerRead the raw request body and JSON.parse it explicitly

❌ What this integration cannot do (yet)

You asked forStatusWhyWhat to do
Delivery guarantee on webhook callbacks❌ Not availableLeon sends a single POST with no retry queue and no send timeout; a failed delivery surfaces as a GraphQL error to whoever triggered itTreat Step 6 polling as your source of truth and use the webhook as a low-latency hint, not the only channel
Signed / authenticated webhook payloads❌ Not availableNo HMAC, JWT, or shared-secret header is sent — the URL itself is the only secretUse an unguessable URL path, IP allow-listing if possible, and verify every payload against Step 5 before acting on money
Push notification when a quote is priced (as opposed to accepted/rejected)❌ Not availableThe accept and reject webhooks only fire on the final decision mutationsUse Step 6 (getModifiedQuotesList) polling to detect newly priced quotes
Creating the priced quote itself from outside Leon❌ Not available via this flowPricing is the operator's action in the Sales panel; createRequestWithWebhook creates only the requestIf you need to write quotes and prices programmatically, raise it with Leon Support — a different set of mutations governs operator-side quote creation and is outside the scope of this webhook pattern

Validation note

Every query and mutation in this guide was validated against the live Leon GraphQL schema (schema-beta) before publication. Webhook payload structures, the URL health check behaviour, and the X- prefix rule were confirmed against the Leon backend source.