AerTicket Integration
Interface to AerTicket API for flight search, verification, booking, ticket issuance, void, ancillaries, retrieval, and cancellation. Supports both UAT and Production environments with automatic authentication, retry logic, and comprehensive error handling.
Quick Start
Section titled “Quick Start”Installation
Section titled “Installation”Environment Variables (.env)
Section titled “Environment Variables (.env)”# Environment: uat (development) or productionAERTICKET_ENVIRONMENT=uat
# API credentials provided by AERTICKETAERTICKET_LOGIN=your_api_loginAERTICKET_PASSWORD=your_api_password
# Optional timeout/retry overrides# AERTICKET_UAT_TIMEOUT=30# AERTICKET_UAT_RETRY_ATTEMPTS=3Test Connection
Section titled “Test Connection”./vendor/bin/sail artisan aerticket:test-connectionBasic Flight Search
Section titled “Basic Flight Search”use App\Services\AerticketCabinetService;use App\Services\Flights\Aerticket\AerticketSearchService;
$cabinet = app(AerticketCabinetService::class);$search = new AerticketSearchService($cabinet);
$results = $search->search([ 'origin' => 'BCN', 'destination' => 'MAD', 'departure_date' => '2025-12-15', 'return_date' => '2025-12-20', 'adults' => 2, 'cabin_class' => 'economy',]);
foreach ($results->getFares() as $fare) { echo "Price: {$fare->getTotalPrice()} {$fare->getCurrency()}\n";}Multi-City Flight Search
Section titled “Multi-City Flight Search”use App\Services\Flights\Aerticket\AerticketSearchService;use App\Services\Flights\Aerticket\DTOs\Request\PassengerType;use App\Services\Flights\Aerticket\DTOs\Request\SearchOptions;use App\Services\Flights\Aerticket\DTOs\Request\SearchRequest;
// Define multi-city segments (2-6 segments allowed)$segments = [ ['departure' => 'BCN', 'destination' => 'BKK', 'date' => '2025-12-20'], ['departure' => 'BKK', 'destination' => 'CNX', 'date' => '2025-12-25'], ['departure' => 'CNX', 'destination' => 'BCN', 'date' => '2026-01-05'],];
// Build passenger types$passengers = [ PassengerType::adult(2), PassengerType::child(1),];
// Create multi-city search request$searchRequest = SearchRequest::multiCity( segments: $segments, passengerTypeList: $passengers, searchOptions: new SearchOptions(cabinClassList: ['ECONOMY']));
// Execute search$searchService = new AerticketSearchService();$response = $searchService->search($searchRequest);Service Components
Section titled “Service Components”| Service | Purpose |
|---|---|
AerticketCabinetService | HTTP client, authentication, environment switching |
AerticketSearchService | Flight search |
AerticketSearchUpsellService | Fare upgrade search |
AerticketVerifyService | Fare verification |
AerticketBookService | Booking creation |
CheckoutFlightBookingService | Per-leg checkout flight booking (international and domestic, economy and business) |
AerticketTicketIssueService | Ticket issuance |
AerticketFastlaneTicketingService | Fast-track ticket issuance |
AerticketRePriceService | Booking re-pricing |
AerticketVoidService | Booking void |
AerticketAncillaryService | Ancillary services (baggage, meals) |
AerticketFareRulesService | Fare rules and restrictions |
AerticketRetrieveService | PNR retrieval |
AerticketCancelService | Booking cancellation |
FlightRouteValidationService | Validate flight route availability for supplier tours |
Low-Volume Endpoints and Daily Budgets
Section titled “Low-Volume Endpoints and Daily Budgets”AerticketSearchUpsellService (/search-upsell) and AerticketAncillaryService::availableFareAncillaries() (/available-fare-ancillaries) are contractually capped by Aerticket at 100 calls per day each. Cache-population traffic (the DynamicFlightCachePopulatorService upsell/ancillary flow) enforces a lower internal ceiling via config/aerticket.php → daily_budgets so ad-hoc operator actions retain headroom at the end of the day. Customer-facing calls (booking, verification) are never budget-limited.
| Bucket | Config key | Env var | Default |
|---|---|---|---|
search_upsell | aerticket.daily_budgets.search_upsell | AERTICKET_SEARCH_UPSELL_DAILY_LIMIT | 80 |
available_fare_ancillaries | aerticket.daily_budgets.available_fare_ancillaries | AERTICKET_ANCILLARIES_DAILY_LIMIT | 80 |
Counters are keyed aerticket:daily_budget:<bucket>:<YYYY-MM-DD> and reset at local midnight via RateLimiter decay. Setting a limit to 0 disables the check for that bucket.
When a budget is exhausted, AerticketDailyBudgetExceededException is thrown. Callers are expected to catch it and degrade gracefully (skip the upsell/ancillary tier, keep caching the rest of the fares) rather than propagate it.
Source: backend/config/aerticket.php, backend/app/Exceptions/Services/Flights/Aerticket/Exceptions/AerticketDailyBudgetExceededException.php
Related: Dynamic Flight Cache — Baggage Upsell Resolution
Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”# Environment: "uat" or "production" (determines base URLs automatically)AERTICKET_ENVIRONMENT=uat
# Credentials (shared across environments)AERTICKET_LOGIN=your_api_loginAERTICKET_PASSWORD=your_api_password
# Optional: Override default timeout settings (seconds)# AERTICKET_UAT_TIMEOUT=30# AERTICKET_PROD_TIMEOUT=30
# Optional: Configure retry settings# AERTICKET_UAT_RETRY_ATTEMPTS=3# AERTICKET_UAT_RETRY_DELAY=100
# Optional: Logging configuration# AERTICKET_LOGGING_ENABLED=true# AERTICKET_LOG_CHANNEL=singleBase URLs are configured automatically per environment in config/aerticket.php:
- UAT:
https://apihub-uat.aerticket-it.de/cabinet/and/api/v1/ - Production:
https://apihub.aerticket-it.de/cabinet/and/api/v1/
Staging Environment Safeguards
Section titled “Staging Environment Safeguards”To prevent accidental low-cost carrier bookings in staging/UAT:
- Flydubai flights (airline code: FZ) are blocked
- Flights requiring instant purchase are blocked
Controlled by AERTICKET_ENVIRONMENT variable - active when set to "uat".
Key Operations
Section titled “Key Operations”Verify Fare
Section titled “Verify Fare”use App\Services\Flights\Aerticket\AerticketVerifyService;
$verify = new AerticketVerifyService($cabinet);
$response = $verify->verify([ 'fare_id' => $fare->getId(), 'session_id' => $searchResponse->getSessionId(),]);
if ($response->isAvailable()) { $updatedPrice = $response->getPrice();}Create Booking (Async)
Section titled “Create Booking (Async)”use App\Jobs\AerticketCreateBookingJob;
AerticketCreateBookingJob::dispatch( fareId: 'fare-123', instantTicketOrder: true, userId: auth()->id())->onQueue('aerticket-bookings');Checkout Flight Booking (Per-Leg, Admin-Triggered)
Section titled “Checkout Flight Booking (Per-Leg, Admin-Triggered)”After successful checkout payment, bookings with flights move to pending_flight_booking. Admin users trigger flight booking from the booking detail page. Each flight leg is booked independently with its own FlightBooking record and PNR.
Trigger: Filament booking actions (Book All Flights / Book Flight per-leg / Retry).
Process (per leg):
- Admin action transitions booking to
flight_booking_in_progress - One
CreateCheckoutFlightBookingJobdispatched per unbooked leg toaerticket-bookingsqueue - Each job calls
CheckoutFlightBookingService::bookLegForCheckout($booking, $legIndex):- International legs:
bookInternationalLeg()re-searches round-trip viaEconomyFlightSearchService::searchInternationalOnly()(economy) orBusinessFlightSearchService::searchBusinessFlightsRaw()(business), matches by flight numbers + price - Domestic legs:
bookDomesticLeg()searches one-way viaAerticketSearchService, matches by flight numbers + price
- International legs:
- Verifies fare availability via
AerticketVerifyService - Books via
AerticketBookServicewithinstantTicketOrder=false - Creates
FlightBookingrecord withsource=CHECKOUT,booking_id,leg_index, andflight_type - Dispatches
AerticketRetrieveBookingJobfor PNR details - All-legs-booked check: Only transitions to
flights_confirmedwhen ALL legs haveFlightBookingrecords
Business class specifics:
- Business fares are bundled (1 fare = both legs, itinerary index 1 only), unlike economy mix-and-match
BookingUpsellwith typeflight_upgradecontinues to be created for financial tracking
Retry logic:
- 3 attempts, 2 max exceptions
- 120s backoff between attempts
- 420s timeout per attempt
ShouldBeUniquewithuniqueId = "checkout-flight:{bookingId}:{legIndex}"
Idempotency: Per-leg check via FlightBooking::where(booking_id, leg_index)->exists() with row-level lock.
Error handling:
- Failures produce structured error context via
CheckoutFlightBookingException::$context, which is stored inbooking_status_transitions.metadata(includingleg_index) and displayed in the admin status timeline - Context is automatically extracted from Aerticket exception types (price change, fare expired, timeout, validation, verify error, booking error) — see Checkout API - Structured error context for details
- API error responses include
provider_errorsin context for debugging with Aerticket support - Admin users receive Filament database notifications on success/failure (per-leg and all-legs-complete)
- Failed bookings can be retried from the booking detail page (top-level or per-leg)
Limitations:
- No instant ticketing (manual ticketing required)
Source: backend/app/Services/Checkout/CheckoutFlightBookingService.php
Related: Checkout API - Admin-Driven Flight Booking
Issue Ticket
Section titled “Issue Ticket”use App\Jobs\AerticketIssueTicketJob;
AerticketIssueTicketJob::dispatch( bookingReference: 'ABC123', bookingId: $flightBooking->id, userId: auth()->id())->onQueue('aerticket-tickets');Fastlane Ticketing
Section titled “Fastlane Ticketing”Faster alternative to regular ticket issuance. Booking must be at least 5 minutes old.
use App\Jobs\AerticketFastlaneTicketingJob;
AerticketFastlaneTicketingJob::dispatch( bookingReference: 'ABC123', bookingId: $flightBooking->id, userId: auth()->id())->onQueue('aerticket-fastlane');Re-Price Booking
Section titled “Re-Price Booking”use App\Services\Flights\Aerticket\AerticketRePriceService;use App\Services\Flights\Aerticket\DTOs\Request\PriceRange;
$rePriceService = new AerticketRePriceService($cabinet);
// Re-pricing with price tolerance (±5 EUR)$priceRange = new PriceRange(min: 5.0, max: 5.0);$response = $rePriceService->rePrice('ABC123', $priceRange);
if ($response->hasNewFares()) { // Price outside tolerance - requires approval $subFareToken = $response->getSubFareToken();}View Ancillaries
Section titled “View Ancillaries”use App\Services\Flights\Aerticket\AerticketAncillaryService;
$ancillaryService = new AerticketAncillaryService($cabinet);
// For bookings (after booking)$response = $ancillaryService->availableBookingAncillaries( pnrLocator: 'ABC123', ancillaryTypes: ['BAGGAGE', 'MEAL']);
foreach ($response->getAncillariesByType() as $type => $ancillaries) { foreach ($ancillaries as $ancillary) { echo "{$ancillary->name}: {$ancillary->getFormattedPrice()}\n"; }}Cancel Booking
Section titled “Cancel Booking”use App\Services\Flights\Aerticket\AerticketCancelService;
$cancel = new AerticketCancelService($cabinet);
// Normal cancellation$result = $cancel->cancelBooking('ABC123');
// Force cancellation (even if tickets issued)$result = $cancel->cancelBooking('ABC123', forceCancellation: true);Validate Flight Route
Section titled “Validate Flight Route”Performs a test multi-city search to verify that a supplier tour’s international flight legs have actual flight availability. Used in the admin panel when creating or editing supplier tours.
How it works:
- Extracts international legs from the ProductTemplate itinerary via
FlightRouteConfigGenerator - Builds a multi-city
SearchRequestwith 2 adults, sample dates ~60 days out, origin MAD - Calls
AerticketSearchService::search() - Returns structured result with success/failure, route summary, fare count, and cheapest fare
Error types: no_legs, airport_resolution, no_results, timeout, validation, api_error
Admin integration:
- Create wizard: Checkbox in Step 1 triggers validation on Next (blocks progression on failure)
- Edit/View pages: Header button with confirmation modal
Source: backend/app/Services/Flights/FlightRouteValidationService.php
Related: Supplier Tours
Passenger Validation
Section titled “Passenger Validation”Name Validation
Section titled “Name Validation”Per AerTicket API v3.17 specification:
- ASCII letters and spaces only (
/^[a-zA-Z\s]+$/) — no accents, numbers, or symbols - No
+character (causes booking failures) - Last name minimum 2 characters
- Each name maximum 57 characters
- Combined firstName + lastName: 2-57 characters total (
PassengerNameLengthrule) - Valid titles: MR, MRS, MS, CHD, INF, DR MR, etc.
These rules are enforced in both the Filament admin (camelCase fields: firstName/lastName) and the checkout API (snake_case fields: first_name/last_name). The PassengerNameLength rule auto-detects the naming convention.
Usage: See PassengerValidationRules::firstName() and ::lastName() for the rule arrays.
Source: backend/app/Rules/PassengerValidationRules.php, backend/app/Rules/PassengerNameLength.php
Age Category Validation
Section titled “Age Category Validation”- Infant (INF): 0-23 months
- Child (CHD): 2-15 years
- Adult (ADT): 16+ years
use App\Enums\AgeCategory;
$ageCategory = AgeCategory::fromDateOfBirth($dateOfBirth);
if (!$ageCategory->isValidForDateOfBirth($dateOfBirth)) { $errorMessage = $ageCategory->getValidationErrorMessage($dateOfBirth);}Error Handling
Section titled “Error Handling”Exception Types
Section titled “Exception Types”| Exception | Description |
|---|---|
AerticketValidationException | Invalid input |
AerticketTimeoutException | Request timeout |
AerticketSearchException | Search error |
AerticketVerifyException | Fare verification error |
AerticketBookingException | Booking error |
AerticketTicketIssueException | Ticket issuance error |
AerticketRePricingException | Re-pricing error |
AerticketFareExpiredException | Fare expired (410) |
AerticketPriceChangeException | Price changed |
AerticketDailyBudgetExceededException | Internal per-day call budget exhausted for search_upsell or available_fare_ancillaries. Caller degrades gracefully. |
Laravel Context Enrichment (Nightwatch)
Section titled “Laravel Context Enrichment (Nightwatch)”All Aerticket API requests and exceptions automatically add context data to Laravel’s Context facade, which is included in Nightwatch error reports for debugging.
Transport-level context (added by AerticketCabinetService):
aerticket_endpoint- API operation (e.g., “verify-fare”, “search”, “create-booking”)aerticket_environment- “uat” or “production”aerticket_timeout- Configured timeout in secondsaerticket_apihubflowid- Aerticket’s internal tracking ID from response header
Domain-level context (added by exception classes):
aerticket_fare_id- Fare ID (on booking/verify exceptions)aerticket_booking_reference- PNR locator (on retrieve/cancel/ticket exceptions)aerticket_error_details- Error details array when available
This context is automatically captured in Nightwatch reports when exceptions occur, making it easier to debug issues with AerTicket support.
Source: backend/app/Services/AerticketCabinetService.php, backend/app/Exceptions/Aerticket/*.php
Example Error Handling
Section titled “Example Error Handling”try { $results = $search->search($data);} catch (AerticketValidationException $e) { return response()->json(['error' => $e->getMessage()], 422);} catch (AerticketTimeoutException $e) { return response()->json(['error' => 'Request timed out'], 504);} catch (AerticketSearchException $e) { Log::error('Search failed', ['error' => $e->getMessage()]); return response()->json(['error' => 'Search failed'], 500);}Local fixture replay
Section titled “Local fixture replay”AerTicket /search calls take 5–30s and consume daily API budget, which makes iterating on post-search ranking, signature-match, and cache-update logic painful. The fixture system captures real responses to JSON files on disk and replays them in ~30ms on subsequent calls. The replayed Illuminate\Http\Client\Response is byte-equivalent to a live one — same status, same headers, same body — so callers can’t tell the difference.
Toggle the system with AERTICKET_FIXTURES_MODE in .env. Captures land in storage/app/aerticket-fixtures/ and are gitignored (PNRs, fareIds, and apihubflowids are PII).
| Mode | Behaviour |
|---|---|
off (default) | Production. Always hit AerTicket. Never read or write fixtures. |
replay | Read-only. Hit on the captured fixture, fail loudly on miss. Best for deterministic tests and “find every code path that still tries to reach AerTicket” debug sessions. |
replay_or_record | Replay if a fixture exists; otherwise hit live and save the response for next time. First run = slow, every run after = ~30ms. Best for the dev loop. |
record | Always hit live, always overwrite the fixture. Refresh stale captures without manually purging. |
Successful responses (2xx) are persisted; transient 5xx errors don’t poison the cache.
Capture a flight
Section titled “Capture a flight”The capture command is flight-centric — it operates on DynamicFlightCache rows (the same unit the admin “Re-cache” button works on):
# A specific cache rowsail artisan aerticket:fixtures:capture --flight=1629
# Every row of a route on a datesail artisan aerticket:fixtures:capture --route=24 --date=2026-09-15
# Every PENDING rowsail artisan aerticket:fixtures:capture --pending
# Skip the reset confirmation promptsail artisan aerticket:fixtures:capture --flight=1629 --forceInternally the command:
- Resolves the entry/entries from the flag.
- Resets non-
PENDINGrows toPENDINGviaDynamicFlightCachePopulatorService::resetEntryForRecache(with a confirmation prompt; same operation the admin Re-cache button performs — drops sibling fare positions 2..N, repoints orphanedoffer_flightsto position 1, clears segments). This avoids the unique-constraint collision on(flight_cache_id, leg_sequence, itinerary_index, segment_number). - Forces
aerticket.fixtures_mode = recordfor the run. - Calls
DynamicFlightCachePopulatorService::executeSearches($entries)— same path the populator job uses, so any side calls (/search-upsell,/availableFareAncillaries) trigger naturally and get captured.
A typical economy round-trip capture produces:
- 1 ×
search-{hash12}.json— the main flight response (~5–6 MB). - N ×
search-upsell-{hash12}.json— one per no-bag fare-family the populator probed for bag (~330 KB each). - M ×
available-fare-ancillaries-{hash12}.json— fallback per fare when no upsell sibling exists (~2 KB each).
Inspect what’s captured
Section titled “Inspect what’s captured”sail artisan aerticket:fixtures:list+----------------------------+----------------------------------------------+----------+---------------------------+| Endpoint | File | Size | Captured at |+----------------------------+----------------------------------------------+----------+---------------------------+| search | search-562f0a4a66fd.json | 5.6 MB | 2026-04-29T09:10:03+00:00 || search-upsell | search-upsell-187482a3f255.json | 331.6 KB | 2026-04-29T09:10:18+00:00 || available-fare-ancillaries | available-fare-ancillaries-d454529a5a45.json | 1.8 KB | 2026-04-29T09:10:33+00:00 |+----------------------------+----------------------------------------------+----------+---------------------------+13 fixture(s) at /var/www/html/storage/app/aerticket-fixturesMode: replay_or_recordRefresh or wipe
Section titled “Refresh or wipe”# Refresh a single flight against live AerTicket (overwrites the existing fixture)AERTICKET_FIXTURES_MODE=record sail artisan aerticket:fixtures:capture --flight=1629 --force
# Wipe everything — confirms first unless --forcesail artisan aerticket:fixtures:purgeVerifying replay
Section titled “Verifying replay”Three ways to confirm a request is hitting the fixture instead of AerTicket:
-
Tail the log
Terminal window sail artisan pail --filter=AerticketReplay logs:
[INFO] Aerticket fixture replay hit endpoint=search hash=562f0a4a66fd mode=replay_or_recordLive calls log
Making API requestfollowed seconds later byAPI response received. -
Telescope HTTP Client tab at
localhost/telescope/client-requests. Replay shows zero new outbound requests toapihub.aerticket-it.de. -
Stopwatch. Live re-cache: 30s+. Pure replay: 3–8s (the time is now DB writes, not HTTP). The browser’s network tab shows the same.
Recipe: dev loop on flight-pipeline code
Section titled “Recipe: dev loop on flight-pipeline code”AERTICKET_FIXTURES_MODE=replay_or_record
# Capture once (slow)sail artisan aerticket:fixtures:capture --flight=1629
# Iterate on FlightRankingPolicy / EconomyFlightSearchService / cache-update etc.# Every reload of /es/checkout/<offer> hitting that flight returns from disk in ~30ms.
# When AerTicket data drifts and you need fresh prices:sail artisan aerticket:fixtures:capture --flight=1629 --force
# Find code paths that still try to talk to AerTicket — switch to strict replay:# .envAERTICKET_FIXTURES_MODE=replay# A miss now throws, pinpointing the offending call site.How requests are matched
Section titled “How requests are matched”AerticketFixtureStore::hashKey($endpoint, $data) computes a SHA-256 of endpoint + sorted-keys-JSON(normalized payload). The normalization:
- Drops
apihubflowid,internalRequestId,requestId,requestSentAt. - Sorts
segmentListbydepartureDateso equivalent round-trip requests with different leg order share a hash.
Requests that don’t match any captured fixture either fail (replay) or fall through to live + record (replay_or_record).
Configuration knobs
Section titled “Configuration knobs”config/aerticket.php:
'fixtures_mode' => env('AERTICKET_FIXTURES_MODE', 'off'),'fixtures_path' => env('AERTICKET_FIXTURES_PATH', storage_path('app/aerticket-fixtures')),Keep AERTICKET_FIXTURES_MODE=off in production. The default is off precisely so a leaked dev .env can’t accidentally serve cached responses to real customers.
Testing
Section titled “Testing”# All AerTicket tests./vendor/bin/sail artisan test --filter=Aerticket
# Connection test./vendor/bin/sail artisan aerticket:test-connection
# Re-pricing tests./vendor/bin/sail artisan test --filter=RePrice
# Ancillary tests./vendor/bin/sail artisan test --filter=AncillaryTroubleshooting
Section titled “Troubleshooting”Authentication Failure (401)
Section titled “Authentication Failure (401)”php artisan tinker>>> config('aerticket.credentials.login')Connection Timeout
Section titled “Connection Timeout”Increase timeout in .env:
AERTICKET_TIMEOUT=60Missing apihubflowid
Section titled “Missing apihubflowid”Check logs:
./vendor/bin/sail artisan pail --filter="apihubflowid"The apihubflowid is required when contacting AerTicket support.
Related Documentation
Section titled “Related Documentation”- Async Booking Workflow - Queue-based booking
- Flight Search UI - FilamentPHP interface
- Airport Search - Airport lookup service
- Queue System - Queue infrastructure