Skip to content

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.

Terminal window
# Environment: uat (development) or production
AERTICKET_ENVIRONMENT=uat
# API credentials provided by AERTICKET
AERTICKET_LOGIN=your_api_login
AERTICKET_PASSWORD=your_api_password
# Optional timeout/retry overrides
# AERTICKET_UAT_TIMEOUT=30
# AERTICKET_UAT_RETRY_ATTEMPTS=3
Terminal window
./vendor/bin/sail artisan aerticket:test-connection
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";
}
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);
ServicePurpose
AerticketCabinetServiceHTTP client, authentication, environment switching
AerticketSearchServiceFlight search
AerticketSearchUpsellServiceFare upgrade search
AerticketVerifyServiceFare verification
AerticketBookServiceBooking creation
CheckoutFlightBookingServicePer-leg checkout flight booking (international and domestic, economy and business)
AerticketTicketIssueServiceTicket issuance
AerticketFastlaneTicketingServiceFast-track ticket issuance
AerticketRePriceServiceBooking re-pricing
AerticketVoidServiceBooking void
AerticketAncillaryServiceAncillary services (baggage, meals)
AerticketFareRulesServiceFare rules and restrictions
AerticketRetrieveServicePNR retrieval
AerticketCancelServiceBooking cancellation
FlightRouteValidationServiceValidate flight route availability for supplier tours
Terminal window
# Environment: "uat" or "production" (determines base URLs automatically)
AERTICKET_ENVIRONMENT=uat
# Credentials (shared across environments)
AERTICKET_LOGIN=your_api_login
AERTICKET_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=single

Base 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/

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

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();
}
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):

  1. Admin action transitions booking to flight_booking_in_progress
  2. One CreateCheckoutFlightBookingJob dispatched per unbooked leg to aerticket-bookings queue
  3. Each job calls CheckoutFlightBookingService::bookLegForCheckout($booking, $legIndex):
    • International legs: bookInternationalLeg() re-searches round-trip via EconomyFlightSearchService::searchInternationalOnly() (economy) or BusinessFlightSearchService::searchBusinessFlightsRaw() (business), matches by flight numbers + price
    • Domestic legs: bookDomesticLeg() searches one-way via AerticketSearchService, matches by flight numbers + price
  4. Verifies fare availability via AerticketVerifyService
  5. Books via AerticketBookService with instantTicketOrder=false
  6. Creates FlightBooking record with source=CHECKOUT, booking_id, leg_index, and flight_type
  7. Dispatches AerticketRetrieveBookingJob for PNR details
  8. All-legs-booked check: Only transitions to flights_confirmed when ALL legs have FlightBooking records

Business class specifics:

  • Business fares are bundled (1 fare = both legs, itinerary index 1 only), unlike economy mix-and-match
  • BookingUpsell with type flight_upgrade continues to be created for financial tracking

Retry logic:

  • 3 attempts, 2 max exceptions
  • 120s backoff between attempts
  • 420s timeout per attempt
  • ShouldBeUnique with uniqueId = "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 in booking_status_transitions.metadata (including leg_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_errors in 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

use App\Jobs\AerticketIssueTicketJob;
AerticketIssueTicketJob::dispatch(
bookingReference: 'ABC123',
bookingId: $flightBooking->id,
userId: auth()->id()
)->onQueue('aerticket-tickets');

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');
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();
}
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";
}
}
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);

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:

  1. Extracts international legs from the ProductTemplate itinerary via FlightRouteConfigGenerator
  2. Builds a multi-city SearchRequest with 2 adults, sample dates ~60 days out, origin MAD
  3. Calls AerticketSearchService::search()
  4. 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

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 (PassengerNameLength rule)
  • 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

  • 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);
}
ExceptionDescription
AerticketValidationExceptionInvalid input
AerticketTimeoutExceptionRequest timeout
AerticketSearchExceptionSearch error
AerticketVerifyExceptionFare verification error
AerticketBookingExceptionBooking error
AerticketTicketIssueExceptionTicket issuance error
AerticketRePricingExceptionRe-pricing error
AerticketFareExpiredExceptionFare expired (410)
AerticketPriceChangeExceptionPrice changed

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 seconds
  • aerticket_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

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);
}
Terminal window
# 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=Ancillary
Terminal window
php artisan tinker
>>> config('aerticket.credentials.login')

Increase timeout in .env:

Terminal window
AERTICKET_TIMEOUT=60

Check logs:

Terminal window
./vendor/bin/sail artisan pail --filter="apihubflowid"

The apihubflowid is required when contacting AerTicket support.